Add Snake Game to Example list (#56)
* Init snake game * Replace [Any] state with [UnsafeMutableRawPointer] * Broke ux * Add Width init function * Update Snake Game * Remove ability to switch move direction to opposite * Fix game board bound * Add game over condition * Add color to snake target * Refactor code * Add Gamepad to Snake Game * Remove infinity loop * Refactor code * Refactor code * Add Gameboard to Snake * Reduce render function size * Add Gamemenu * Rename Gamemenu to GameMenu * Format code * Refactor code
This commit is contained in:
parent
a570352abc
commit
604a60d251
|
@ -13,6 +13,14 @@
|
|||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
A62AC6542223F5CD009B3B25 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62AC6532223F5CD009B3B25 /* TextField.swift */; };
|
||||
A62AC65622243CC3009B3B25 /* SnakeGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62AC65522243CC3009B3B25 /* SnakeGame.swift */; };
|
||||
A62AC65922243DCB009B3B25 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62AC65822243DCA009B3B25 /* Cell.swift */; };
|
||||
A62AC65B22244A5A009B3B25 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62AC65A22244A5A009B3B25 /* Game.swift */; };
|
||||
A62AC65F22244A98009B3B25 /* Snake.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62AC65E22244A98009B3B25 /* Snake.swift */; };
|
||||
A6654358222690DA00F01C04 /* Gamepad.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6654357222690DA00F01C04 /* Gamepad.swift */; };
|
||||
A665435A22269F9F00F01C04 /* StartGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = A665435922269F9F00F01C04 /* StartGame.swift */; };
|
||||
A67717202226DC7C0028A6F3 /* Gameboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A677171F2226DC7C0028A6F3 /* Gameboard.swift */; };
|
||||
A67717222226E7FD0028A6F3 /* Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67717212226E7FD0028A6F3 /* Menu.swift */; };
|
||||
A6D5AF87221B131400DBF186 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D5AF86221B131400DBF186 /* Image.swift */; };
|
||||
C449B806DFEE55B6CEE6478C /* libPods-TokamakDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B96B435A9D67621D318616E /* libPods-TokamakDemo.a */; };
|
||||
D11DB6432219C03000013FC3 /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11DB6422219C03000013FC3 /* Timer.swift */; };
|
||||
|
@ -42,6 +50,14 @@
|
|||
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
8CADEBB8BFF6F2621CA49E8F /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
A62AC6532223F5CD009B3B25 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
|
||||
A62AC65522243CC3009B3B25 /* SnakeGame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnakeGame.swift; sourceTree = "<group>"; };
|
||||
A62AC65822243DCA009B3B25 /* Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = "<group>"; };
|
||||
A62AC65A22244A5A009B3B25 /* Game.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = "<group>"; };
|
||||
A62AC65E22244A98009B3B25 /* Snake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Snake.swift; sourceTree = "<group>"; };
|
||||
A6654357222690DA00F01C04 /* Gamepad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gamepad.swift; sourceTree = "<group>"; };
|
||||
A665435922269F9F00F01C04 /* StartGame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartGame.swift; sourceTree = "<group>"; };
|
||||
A677171F2226DC7C0028A6F3 /* Gameboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gameboard.swift; sourceTree = "<group>"; };
|
||||
A67717212226E7FD0028A6F3 /* Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = "<group>"; };
|
||||
A6D5AF86221B131400DBF186 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
|
||||
A9EEF813955DAEEFE1D52ED4 /* Pods-TokamakDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TokamakDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TokamakDemo/Pods-TokamakDemo.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
C6DA99382B6892EAB361742F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
|
||||
|
@ -152,9 +168,25 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A62AC65722243DAC009B3B25 /* SnakeGame */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A62AC65522243CC3009B3B25 /* SnakeGame.swift */,
|
||||
A62AC65822243DCA009B3B25 /* Cell.swift */,
|
||||
A62AC65A22244A5A009B3B25 /* Game.swift */,
|
||||
A62AC65E22244A98009B3B25 /* Snake.swift */,
|
||||
A6654357222690DA00F01C04 /* Gamepad.swift */,
|
||||
A665435922269F9F00F01C04 /* StartGame.swift */,
|
||||
A677171F2226DC7C0028A6F3 /* Gameboard.swift */,
|
||||
A67717212226E7FD0028A6F3 /* Menu.swift */,
|
||||
);
|
||||
path = SnakeGame;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D1F7185122159D6E004E5951 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A62AC65722243DAC009B3B25 /* SnakeGame */,
|
||||
D1BFAF782215800A00845EA0 /* Counter.swift */,
|
||||
D1F7185E2215A5D0004E5951 /* Constraints.swift */,
|
||||
D1F7185222159E09004E5951 /* Controls.swift */,
|
||||
|
@ -281,16 +313,24 @@
|
|||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
|
||||
A62AC6542223F5CD009B3B25 /* TextField.swift in Sources */,
|
||||
D1F7185F2215A5D0004E5951 /* Constraints.swift in Sources */,
|
||||
A62AC65922243DCB009B3B25 /* Cell.swift in Sources */,
|
||||
D11DB6432219C03000013FC3 /* Timer.swift in Sources */,
|
||||
A62AC65622243CC3009B3B25 /* SnakeGame.swift in Sources */,
|
||||
D1F7185322159E09004E5951 /* Controls.swift in Sources */,
|
||||
D1DEEC2922009E8000C525EE /* ModalRouter.swift in Sources */,
|
||||
A6D5AF87221B131400DBF186 /* Image.swift in Sources */,
|
||||
D1BFAF772215795900845EA0 /* Router.swift in Sources */,
|
||||
A62AC65F22244A98009B3B25 /* Snake.swift in Sources */,
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
|
||||
D1F7185D2215A4A1004E5951 /* LayerProps.swift in Sources */,
|
||||
A62AC65B22244A5A009B3B25 /* Game.swift in Sources */,
|
||||
D1BFAF792215800A00845EA0 /* Counter.swift in Sources */,
|
||||
D1F718612215A617004E5951 /* Modals.swift in Sources */,
|
||||
A67717222226E7FD0028A6F3 /* Menu.swift in Sources */,
|
||||
A665435A22269F9F00F01C04 /* StartGame.swift in Sources */,
|
||||
A67717202226DC7C0028A6F3 /* Gameboard.swift in Sources */,
|
||||
D1BB3D302223F6B400C30062 /* Animation.swift in Sources */,
|
||||
A6654358222690DA00F01C04 /* Gamepad.swift in Sources */,
|
||||
D1F7185522159EAD004E5951 /* DatePickers.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// GameCell.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/25/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct GameCell: PureLeafComponent {
|
||||
struct Props: Equatable {
|
||||
let color: Color
|
||||
let size: Double
|
||||
let location: Point
|
||||
}
|
||||
|
||||
static func render(props: Props) -> AnyNode {
|
||||
let location = props.location
|
||||
let size = props.size
|
||||
return View.node(
|
||||
.init(
|
||||
Style(
|
||||
Rectangle(
|
||||
Point(x: size * location.x, y: size * location.y),
|
||||
Size(width: size, height: size)
|
||||
),
|
||||
backgroundColor: props.color
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// Game.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/25/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct Game: Equatable {
|
||||
enum State {
|
||||
case initial
|
||||
case gameOver
|
||||
case isPlaying
|
||||
}
|
||||
|
||||
var state = State.initial
|
||||
|
||||
enum Direction {
|
||||
case up
|
||||
case down
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
var currentDirection = Direction.up
|
||||
|
||||
var snake: [Point] = [
|
||||
Point(x: 10.0, y: 10.0),
|
||||
Point(x: 10.0, y: 11.0),
|
||||
Point(x: 10.0, y: 12.0),
|
||||
]
|
||||
|
||||
var target: Point = Point(x: 0.0, y: 1.0)
|
||||
|
||||
let mapSize: Size
|
||||
|
||||
private func moveHead(_ point: Point) -> Point {
|
||||
var x = snake[0].x
|
||||
var y = snake[0].y
|
||||
switch currentDirection {
|
||||
case .left:
|
||||
x -= 1
|
||||
if x < 0 {
|
||||
x = mapSize.width - 1
|
||||
}
|
||||
case .up:
|
||||
y -= 1
|
||||
if y < 0 {
|
||||
y = mapSize.height - 1
|
||||
}
|
||||
case .right:
|
||||
x += 1
|
||||
if x >= mapSize.width {
|
||||
x = 0
|
||||
}
|
||||
case .down:
|
||||
y += 1
|
||||
if y >= mapSize.height {
|
||||
y = 0
|
||||
}
|
||||
}
|
||||
return Point(x: x, y: y)
|
||||
}
|
||||
|
||||
public init(mapSize: Size) {
|
||||
self.mapSize = mapSize
|
||||
}
|
||||
|
||||
public init(state: State, mapSize: Size) {
|
||||
self.state = state
|
||||
self.mapSize = mapSize
|
||||
}
|
||||
}
|
||||
|
||||
extension Game {
|
||||
mutating func tick() {
|
||||
let head = moveHead(snake[0])
|
||||
let isHeadOnTarget = head == target
|
||||
|
||||
if snake.contains(head) {
|
||||
state = .gameOver
|
||||
return
|
||||
}
|
||||
|
||||
snake.insert(head, at: 0)
|
||||
|
||||
if !snake.isEmpty && !isHeadOnTarget {
|
||||
snake.removeLast()
|
||||
}
|
||||
|
||||
if isHeadOnTarget {
|
||||
var newTarget = target
|
||||
|
||||
repeat {
|
||||
newTarget = Point(
|
||||
x: Double(Int.random(in: 0..<Int(mapSize.width))),
|
||||
y: Double(Int.random(in: 0..<Int(mapSize.height)))
|
||||
)
|
||||
} while snake.contains(newTarget) || newTarget == target
|
||||
|
||||
target = newTarget
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// Gameboard.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/27/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct Gameboard: PureComponent {
|
||||
struct Props: Equatable {
|
||||
let game: State<Game>
|
||||
let cellSize: Double
|
||||
}
|
||||
|
||||
typealias Children = [AnyNode]
|
||||
|
||||
static func render(props: Props, children: Children) -> AnyNode {
|
||||
let game = props.game
|
||||
|
||||
return View.node(
|
||||
[
|
||||
View.node(
|
||||
.init(Style(
|
||||
[
|
||||
Center.equal(to: .parent),
|
||||
Width.equal(
|
||||
to: props.cellSize * game.value.mapSize.width
|
||||
),
|
||||
Height.equal(
|
||||
to: props.cellSize * game.value.mapSize.height
|
||||
),
|
||||
],
|
||||
borderColor: .black,
|
||||
borderWidth: 2
|
||||
)),
|
||||
[
|
||||
View.node(
|
||||
GameCell.node(.init(
|
||||
color: .red,
|
||||
size: props.cellSize,
|
||||
location: game.value.target
|
||||
))
|
||||
),
|
||||
View.node(
|
||||
game.value.snake.map { (location) -> AnyNode in
|
||||
let props = GameCell.Props(
|
||||
color: .black,
|
||||
size: props.cellSize,
|
||||
location: location
|
||||
)
|
||||
return GameCell.node(props)
|
||||
}
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// Gamepad.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/27/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct Gamepad: PureComponent {
|
||||
struct Props: Equatable {
|
||||
let game: State<Game>
|
||||
}
|
||||
|
||||
typealias Children = [AnyNode]
|
||||
|
||||
static func render(props: Props, children: Children) -> AnyNode {
|
||||
let game = props.game
|
||||
let isVerticalMoveEnabled = ![.up, .down]
|
||||
.contains(game.value.currentDirection)
|
||||
let isHorizontalMoveEnabled = ![.left, .right]
|
||||
.contains(game.value.currentDirection)
|
||||
|
||||
return StackView.node(.init(
|
||||
alignment: .center,
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually
|
||||
), [
|
||||
Button.node(
|
||||
.init(
|
||||
Style(Width.equal(to: .parent, multiplier: 0.5)),
|
||||
isEnabled: isVerticalMoveEnabled,
|
||||
onPress: Handler {
|
||||
game.set { $0.currentDirection = .up }
|
||||
}
|
||||
),
|
||||
"⬆️"
|
||||
),
|
||||
StackView.node(.init(
|
||||
[Width.equal(to: .parent)],
|
||||
axis: .horizontal,
|
||||
distribution: .fillEqually
|
||||
), [
|
||||
Button.node(
|
||||
.init(
|
||||
isEnabled: isHorizontalMoveEnabled,
|
||||
onPress: Handler {
|
||||
game.set { $0.currentDirection = .left }
|
||||
}
|
||||
),
|
||||
"⬅️"
|
||||
),
|
||||
|
||||
Button.node(
|
||||
.init(
|
||||
isEnabled: isHorizontalMoveEnabled,
|
||||
onPress: Handler {
|
||||
game.set { $0.currentDirection = .right }
|
||||
}
|
||||
),
|
||||
"➡️"
|
||||
),
|
||||
]),
|
||||
Button.node(
|
||||
.init(
|
||||
Style(Width.equal(to: .parent, multiplier: 0.5)),
|
||||
isEnabled: isVerticalMoveEnabled,
|
||||
onPress: Handler { game.set { $0.currentDirection = .down } }
|
||||
),
|
||||
"⬇️"
|
||||
),
|
||||
] + children)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// Menu.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/27/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct GameMenu: PureComponent {
|
||||
struct Props: Equatable {
|
||||
let game: State<Game>
|
||||
}
|
||||
|
||||
typealias Children = [AnyNode]
|
||||
|
||||
static func render(props: Props, children: Children) -> AnyNode {
|
||||
let game = props.game
|
||||
|
||||
let restartedGameState = Game(
|
||||
state: .isPlaying,
|
||||
mapSize: game.value.mapSize
|
||||
)
|
||||
|
||||
switch game.value.state {
|
||||
case .isPlaying:
|
||||
return View.node()
|
||||
case .gameOver:
|
||||
return StackView.node(
|
||||
.init(
|
||||
Edges.equal(to: .parent),
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
spacing: 10.0
|
||||
),
|
||||
Button.node(
|
||||
.init(onPress: Handler { game.set { $0 = restartedGameState } }),
|
||||
"Game over! Restart the game"
|
||||
)
|
||||
)
|
||||
case .initial:
|
||||
return StackView.node(
|
||||
.init(
|
||||
Edges.equal(to: .parent),
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
spacing: 10.0
|
||||
),
|
||||
Button.node(
|
||||
.init(onPress: Handler { game.set { $0.state = .isPlaying } }),
|
||||
"Start the game"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// Snake.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/25/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct Snake: LeafComponent {
|
||||
struct Props: Equatable {
|
||||
let cellSize: Double
|
||||
let mapSizeInCells: Size
|
||||
}
|
||||
|
||||
static func render(props: Props, hooks: Hooks) -> AnyNode {
|
||||
let game = hooks.state(
|
||||
Game(
|
||||
mapSize: props.mapSizeInCells
|
||||
)
|
||||
)
|
||||
let timer = hooks.ref(type: Timer.self)
|
||||
let speed = hooks.state(10.0)
|
||||
|
||||
hooks.finalizedEffect([
|
||||
AnyEquatable(game.value.state),
|
||||
AnyEquatable(speed.value),
|
||||
]) {
|
||||
guard game.value.state == .isPlaying else { return {} }
|
||||
|
||||
timer.value = Timer.scheduledTimer(
|
||||
withTimeInterval: 1 / speed.value,
|
||||
repeats: true
|
||||
) { _ in
|
||||
game.set { $0.tick() }
|
||||
}
|
||||
return {
|
||||
timer.value?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
switch game.value.state {
|
||||
case .isPlaying:
|
||||
return StackView.node(
|
||||
.init(
|
||||
Edges.equal(to: .safeArea),
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
spacing: 10.0
|
||||
), [
|
||||
Gameboard.node(.init(game: game, cellSize: props.cellSize)),
|
||||
|
||||
Gamepad.node(
|
||||
.init(game: game),
|
||||
[
|
||||
Stepper.node(
|
||||
.init(
|
||||
maximumValue: 100.0,
|
||||
minimumValue: 1.0,
|
||||
stepValue: 1.0,
|
||||
value: speed.value,
|
||||
valueHandler: Handler(speed.set)
|
||||
)
|
||||
),
|
||||
Label.node(
|
||||
.init(alignment: .center),
|
||||
"\(speed.value)X"
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
case .gameOver, .initial:
|
||||
return GameMenu.node(.init(game: game))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// SnakeGame.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/25/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct SnakeGame: LeafComponent {
|
||||
typealias Props = Null
|
||||
|
||||
static func render(props: Props, hooks: Hooks) -> AnyNode {
|
||||
let snakeProps = Snake.Props(
|
||||
cellSize: 10,
|
||||
mapSizeInCells: Size(width: 20.0, height: 20.0)
|
||||
)
|
||||
|
||||
return View.node(
|
||||
.init(
|
||||
Style(
|
||||
Edges.equal(to: .safeArea),
|
||||
backgroundColor: .white
|
||||
)
|
||||
),
|
||||
Snake.node(snakeProps)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// StartGame.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/27/19.
|
||||
// Copyright © 2019 Tokamak. All rights reserved.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct StartGame: PureLeafComponent {
|
||||
struct Props: Equatable {
|
||||
let game: State<Game>
|
||||
}
|
||||
|
||||
static func render(props: StartGame.Props) -> AnyNode {
|
||||
let game = props.game
|
||||
let isVerticalMoveEnabled = ![.up, .down]
|
||||
.contains(game.value.currentDirection)
|
||||
let isHorizontalMoveEnabled = ![.left, .right]
|
||||
.contains(game.value.currentDirection)
|
||||
|
||||
return StackView.node(.init(
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually
|
||||
), [
|
||||
Button.node(
|
||||
.init(
|
||||
isEnabled: isVerticalMoveEnabled,
|
||||
onPress: Handler {
|
||||
game.set { $0.currentDirection = .up }
|
||||
}
|
||||
),
|
||||
"⬆️"
|
||||
),
|
||||
StackView.node(.init(
|
||||
axis: .horizontal,
|
||||
distribution: .fillEqually
|
||||
), [
|
||||
Button.node(
|
||||
.init(
|
||||
isEnabled: isHorizontalMoveEnabled,
|
||||
onPress: Handler {
|
||||
game.set { $0.currentDirection = .left }
|
||||
}
|
||||
),
|
||||
"⬅️"
|
||||
),
|
||||
|
||||
Button.node(
|
||||
.init(
|
||||
isEnabled: isHorizontalMoveEnabled,
|
||||
onPress: Handler {
|
||||
game.set { $0.currentDirection = .right }
|
||||
}
|
||||
),
|
||||
"➡️"
|
||||
),
|
||||
]),
|
||||
Button.node(
|
||||
.init(
|
||||
isEnabled: isVerticalMoveEnabled,
|
||||
onPress: Handler { game.set { $0.currentDirection = .down } }
|
||||
),
|
||||
"⬇️"
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ enum AppRoute: String, CaseIterable {
|
|||
case image
|
||||
case textField = "Text Field"
|
||||
case animation
|
||||
case snakeGame = "Snake Game"
|
||||
}
|
||||
|
||||
extension AppRoute: CustomStringConvertible {
|
||||
|
@ -65,6 +66,8 @@ struct Router: NavigationRouter {
|
|||
result = TextFieldExample.node()
|
||||
case .animation:
|
||||
result = Animation.node()
|
||||
case .snakeGame:
|
||||
result = SnakeGame.node()
|
||||
}
|
||||
|
||||
return NavigationItem.node(
|
||||
|
|
|
@ -86,7 +86,7 @@ public protocol PureComponent: CompositeComponent {
|
|||
static func render(props: Props, children: Children) -> AnyNode
|
||||
}
|
||||
|
||||
extension PureComponent {
|
||||
public extension PureComponent {
|
||||
static func render(
|
||||
props: Props,
|
||||
children: Children,
|
||||
|
|
|
@ -29,4 +29,10 @@ public struct Width: Equatable {
|
|||
target: target, constant: constant, multiplier: multiplier
|
||||
))
|
||||
}
|
||||
|
||||
public static func equal(to constant: Double) -> Constraint {
|
||||
return .width(Width(
|
||||
target: .own, constant: constant, multiplier: 0
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ typealias Effect = () -> Finalizer
|
|||
|
||||
protocol HookedComponent: class {
|
||||
/// State cells of this component indexed by order of `hooks.state` calls
|
||||
var state: [Any] { get set }
|
||||
var state: [UnsafeMutableRawPointer] { get set }
|
||||
|
||||
/// Effect cells of this component indexed by order of `hooks.effect` calls
|
||||
var effects: [(observed: AnyEquatable?, Effect)] { get set }
|
||||
|
@ -31,7 +31,10 @@ public final class Hooks {
|
|||
/** Closure assigned by the reconciler before every `render` call. Queues
|
||||
a state update with this reconciler.
|
||||
*/
|
||||
let queueState: (_ index: Int, _ updater: Updater<Any>) -> ()
|
||||
let queueState: (
|
||||
_ index: Int,
|
||||
_ updater: (UnsafeMutableRawPointer) -> ()
|
||||
) -> ()
|
||||
|
||||
weak var component: HookedComponent?
|
||||
|
||||
|
@ -44,7 +47,10 @@ public final class Hooks {
|
|||
|
||||
init(
|
||||
component: HookedComponent,
|
||||
queueState: @escaping (_ index: Int, _ updater: Updater<Any>) -> ()
|
||||
queueState: @escaping (
|
||||
_ index: Int,
|
||||
_ updater: (UnsafeMutableRawPointer) -> ()
|
||||
) -> ()
|
||||
) {
|
||||
self.component = component
|
||||
self.queueState = queueState
|
||||
|
@ -53,7 +59,7 @@ public final class Hooks {
|
|||
/** For a given initial state return a current value of this state
|
||||
(initialized from `initial` if current was absent) and its index.
|
||||
*/
|
||||
func currentState(_ initial: Any) -> (current: Any, index: Int) {
|
||||
func currentState<T>(_ initial: T) -> (current: T, index: Int) {
|
||||
defer { stateIndex += 1 }
|
||||
|
||||
guard let component = component else {
|
||||
|
@ -62,9 +68,13 @@ public final class Hooks {
|
|||
}
|
||||
|
||||
if component.state.count > stateIndex {
|
||||
return (component.state[stateIndex], stateIndex)
|
||||
let pointer = component.state[stateIndex]
|
||||
return (pointer.assumingMemoryBound(to: T.self).pointee, stateIndex)
|
||||
} else {
|
||||
component.state.append(initial)
|
||||
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
pointer.initialize(to: initial)
|
||||
|
||||
component.state.append(UnsafeMutableRawPointer(pointer))
|
||||
return (initial, stateIndex)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,14 +54,9 @@ extension Hooks {
|
|||
let (value, index) = currentState(initial)
|
||||
|
||||
let queueState = self.queueState
|
||||
return State(value as? T ?? initial) { (updater: Updater<T>) in
|
||||
return State(value) { (updater: Updater<T>) in
|
||||
queueState(index) {
|
||||
// There's no easy way to downcast elements of `[Any]` to `T`
|
||||
// and apply `inout` updater without creating copies, working around
|
||||
// that with pointers.
|
||||
withUnsafeMutablePointer(to: &$0) {
|
||||
$0.withMemoryRebound(to: T.self, capacity: 1) { updater(&$0[0]) }
|
||||
}
|
||||
updater(&$0.assumingMemoryBound(to: T.self).pointee)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,11 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
|
|||
let type: AnyCompositeComponent.Type
|
||||
|
||||
// HookedComponent implementation
|
||||
var state = [Any]()
|
||||
|
||||
/// There's no easy way to downcast elements of `[Any]` to `T`
|
||||
/// and apply `inout` updater without creating copies, working around
|
||||
/// that with pointers.
|
||||
var state = [UnsafeMutableRawPointer]()
|
||||
var effects = [(observed: AnyEquatable?, Effect)]()
|
||||
var effectFinalizers = [Finalizer]()
|
||||
var refs = [AnyObject]()
|
||||
|
@ -37,6 +41,12 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
|
|||
super.init(node)
|
||||
}
|
||||
|
||||
deinit {
|
||||
for s in state {
|
||||
s.deallocate()
|
||||
}
|
||||
}
|
||||
|
||||
override func mount(with reconciler: StackReconciler<R>) {
|
||||
let renderedNode = reconciler.render(component: self)
|
||||
|
||||
|
|
|
@ -23,12 +23,12 @@ public final class StackReconciler<R: Renderer> {
|
|||
rootComponent.mount(with: self)
|
||||
}
|
||||
|
||||
func queue(updater: (inout Any) -> (),
|
||||
func queue(updater: (UnsafeMutableRawPointer) -> (),
|
||||
for component: MountedCompositeComponent<R>,
|
||||
id: Int) {
|
||||
let scheduleReconcile = queuedRerenders.isEmpty
|
||||
|
||||
updater(&component.state[id])
|
||||
updater(component.state[id])
|
||||
queuedRerenders.insert(component)
|
||||
|
||||
guard scheduleReconcile else { return }
|
||||
|
|
|
@ -10,6 +10,31 @@ import XCTest
|
|||
|
||||
@testable import Tokamak
|
||||
|
||||
struct Game {
|
||||
enum State {
|
||||
case initial
|
||||
case gameOver
|
||||
case isPlaying
|
||||
}
|
||||
|
||||
var state = State.initial
|
||||
|
||||
enum Direction {
|
||||
case up
|
||||
case down
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
var currentDirection = Direction.up
|
||||
|
||||
var points = [Point]()
|
||||
|
||||
var target = Point(x: 42, y: 42)
|
||||
|
||||
let mapSize = Size(width: 42, height: 42)
|
||||
}
|
||||
|
||||
extension Button: RefComponent {
|
||||
public typealias RefTarget = TestView
|
||||
}
|
||||
|
@ -26,6 +51,7 @@ struct Test: LeafComponent {
|
|||
static func render(props: Null, hooks: Hooks) -> AnyNode {
|
||||
let state1 = hooks.custom()
|
||||
let state2 = hooks.custom()
|
||||
let state3 = hooks.state(Game())
|
||||
let ref = hooks.ref(type: TestView.self)
|
||||
|
||||
return StackView.node([
|
||||
|
@ -38,6 +64,14 @@ struct Test: LeafComponent {
|
|||
Button.node(.init(onPress: Handler { state2.set { $0 + 1 } }),
|
||||
"Increment"),
|
||||
Label.node("\(state2.value)"),
|
||||
Button.node(
|
||||
.init(
|
||||
onPress: Handler {
|
||||
state3.set { $0.points.append(Point(x: 42, y: 42)) }
|
||||
}
|
||||
),
|
||||
"Increment"
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +88,8 @@ final class HooksTests: XCTestCase {
|
|||
let button1Handler = button1Props.handlers[.touchUpInside]?.value,
|
||||
let button2Props = stack.subviews[2].props(Button.Props.self),
|
||||
let button2Handler = button2Props.handlers[.touchUpInside]?.value,
|
||||
let button3Props = stack.subviews[4].props(Button.Props.self),
|
||||
let button3Handler = button3Props.handlers[.touchUpInside]?.value,
|
||||
let button1Ref = stack.subviews[0].node.ref as? Ref<TestView?>
|
||||
else {
|
||||
XCTAssert(false, "components have no handlers")
|
||||
|
@ -67,6 +103,8 @@ final class HooksTests: XCTestCase {
|
|||
button2Handler(())
|
||||
button2Handler(())
|
||||
|
||||
button3Handler(())
|
||||
|
||||
let e = expectation(description: "rerender")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
Loading…
Reference in New Issue