Sponsor Link
環境&対象
- macOS Ventura 13.2
- Xcode 14.2
全てを作成してから記事を書いていないので、途中でバージョンアップがあるかもしれません。
LifeGame
LifeGame とは以下のようなルールのもと 世代を進めていき、進化を眺める(?)ゲームです。
・セルは、「生きている」「死んでいる」の2つの状態のいずれかを持つ。
・セルは、時間が進むにつれて、「死んだり」「生きたり」する
・セルは、周囲の8つのセルの状態に応じて、「死んだり」「生きたり」する
・セルの誕生死滅ルール
・生きているセルの周りに、2つ もしくは 3つの生きたセルがあると、そのセルは 次の世代でも生きている
・生きているセルの周りに、1つ以下 もしくは 4つ以上の生きたセルがあると、そのセルは 次の世代で死滅する
・死んでいるセルの周りにちょうど3つの生きたセルがあると、そのセルに次の世代で誕生する
・死んでいるセルの周りに、2つ以下 もしくは 4つ以上の生きたセルがあると、そのセルは 次の世代でも死滅したまま
# セルの”周りのセル”とは、周囲8個のセルのことです。
詳細は、Wikipediaで。
前回まで
前回までに行ったことは以下のことです。
・ボード全体を表現する LGBoard を actor で 作成した
・各セルを表現する LGCell を class で作成した
・LGBoard 内での隣接セルの状態を確認する準備をした
[Swift][SwiftUI] actor を使って、LifeGame を作る(その1: actor でモデルを作る)
・ViewModel として LGViewModel を作成した
・LGBoard を表示するための LGBoardView を作成した
・LGCell を表示するための LGCellView を作成した
[Swift][SwiftUI] actor を使って、LifeGame を作る(その 2: ViewModel/View を作る)
・進化ロジックを実装した
・LGViewCell をアニメーション対応にした
[Swift][SwiftUI] actor を使って、LifeGame を作る(その3: 進化とアニメーション)
・自動で進化する機能を追加した
[Swift][SwiftUI] actor を使って、LifeGame を作る(その4: 複数の初期パターンと自動進化)
・自動進化の速度を設定する機能を追加した
[Swift][SwiftUI] actor を使って、LifeGame を作る(その5:自動進化の速度設定)
今回やること
以下の機能を追加してみます。
・UI 上で新しいパターンを設定する
・新しいパターンの Save/Load できるようにする
今回で完成とします。
仕様概要
コードを書く前に、大体の仕様を検討します。
新しく追加する仕様としては、以下としてみます。
・画面上の ○/× を直接 クリック/タップ することで、toggle できるようにする
・サイズ変更は、(今回の)対象としない
・作成したパターンは、”Save” ボタンを押すことで、名前をつけて保存できる
・保存したパターンは、プルダウンから選択できるようになる。
・プルダウンから選択したパターンが表示されているときに、”削除” ボタンをおすと、パターンを削除できる
上記の機能があれば、新しいパターンを作ったり、削除したりできるはずです。
サイズ変更は、今後の課題として 棚上げしておきます。
実装を見据えた仕様詳細
これまでの実装を考えると以下のような責務分割になりそうです。
・UI 上でのタップは、View
・現在のボード/セルの状態は、LGBoard/LGCell で持つ状態をそのまま使う
・別の View を作成して タップできるようにするのではなく、現在の LGBoardView を使って、実装する
・Save/Load 操作は、ViewModel が管理する
・データの Save/Load は、ViewModel が LGBoard 型を使って行う(LGBoard を Codable にする)
実装
新しいパターンの設定
LGBoardView を修正していくことになります。
具体的には、LGBoardView で Grid 中に表示されている LGCellView をタップしたとき、LGBoard 中の該当セルを toggle することになります。
考慮すべきこととしては、LGBoard は actor なので、tapGesture からそのまま変更を行うことはできません。別途 Task を使って変更することが必要となります。
そして、その変更終了を待って、LGViewModel の更新が必要となりそうです。
LGBoardView から直接 LGBoard の変更を行うのは筋が良くなさそうなので、ViewModel/Model にメソッドを準備することにします。
ViewModel/Model 側準備
ViewModel では、LGBoard 内のセルを インデックス指定で toggle し、その後 LGViewModel 内の cells を更新するというメソッドを作ることとします。名前は、toggleCell としました。
また、LGBoard には、特定のセルの状態を操作するメソッドがありませんでしたので、作成しました。こちらも、toggleCell とし、LGCell には、toggle というメソッドを作りました。これで、View -> ViewModel -> Model とセルの状態を反転するメソッド群を作ったことになります。(すこし整備しすぎかもしれません・・・)
class LGViewModel: ObservableObject {
// ...snip...
func toggleCell(_ index: CellIndex) async {
await board.toggleCell(index)
await updateCells()
}
}
actor LGBoard: ObservableObject {
// ...snip...
func toggleCell(_ index: CellIndex) {
cells[index]?.toggle()
}
}
class LGCell {
// ...snip...
func toggle() {
state.toggle()
}
}
これで、ViewModel/ Model 側の準備はできたので、LGBoardView を変更していきます。
View の実装
素直に(?)、tapGesture を使って実装していきます。
tapGesture に渡す closure で セルを toggle し、ViewModel の 表示用データを更新するようにすればOKのハズです。
struct LGBoardView: View {
@EnvironmentObject var viewModel: LGViewModel
@State private var selectedPattern: Int = 0
var body: some View {
VStack {
Grid(horizontalSpacing: 0, verticalSpacing: 0) {
ForEach(0..<viewModel.numY, id: \.self) { yIndex in
GridRow {
ForEach(0..<viewModel.numX, id: \.self) { xIndex in
let cellIndex = CellIndex(x: xIndex, y: yIndex)
let cellState = viewModel.cells[cellIndex] ?? .alive
LGCellView(cellState: cellState)
.animation(.default, value: cellState)
.onTapGesture {
Task {
await viewModel.toggleCell(cellIndex)
}
}
}
}
}
}
// ...snip...
}
}
}
toggleCell は、async なメソッドとして定義しているので、 Task を使って呼び出さないといけません。
実装結果
以下のような動作になります。
内部動作は、タップ -> ViewModel 経由で Model を変更 -> Model の状態を ViewModel に反映 -> ViewModel の情報から View が更新 という動作になっています。
パターンの Save/Load
パターンを UI を使って、作れるようになったので、こんどは、作ったパターンを保存することを考えていきます。
・パターンを保存するときは、LGBoard を使って保存する
・Codable の仕組みを使って、保存する
Codable への準拠
まずは、LGBoard/LGCell/LGCellStateそれぞれを Codable に準拠させていくことを考えます。
LGBoard の Codable への準拠
enum, class は、非常に簡単に Codable に準拠することができるのですが、問題となるのは、actor として定義されている LGBoard です。
Codable に準拠するためには、init(from:), encode(to:) の実装が必要となります。
ところが、init(from:), encode(to:) のいずれも sync なアクセスを要求します。
sync なアクセスで、actor 内のプロパティにアクセスすることはできないので、actor 自身のデータを serialize することはできないということになります。
ですので、別の解決策を考えます。actor LGBoard の持っている情報量は、少ないので、Codable に頼らず 自前で serialize/de-serialize することを考えます。
async なメソッドで、”その時点での” LGBoard の情報を取得し、その後、serialize します。
LGBoard の状態を取得/復元
メインの情報である 各セルの情報は、LGBoard の boardStatus メソッドで、[CellIndex:LGCellState] として取得できていますので、これに ボードの X/Y 方向の大きさがわかれば、LGBoard を復元できるはずです。
ということで、以下のような BoardData 型を定義して、そのデータ型を保存し、復元に使うように実装してみます。
struct BoardData {
let numX: Int
let numY: Int
let cells:[CellIndex: LGCellState]
}
LGBoard を BoardData を使って初期化する/LGBoard から BoardData を取得する というような実装を追加して、LGBoard -> BoardData のやりとりができるようにします。
actor LGBoard: ObservableObject {
// ... omit ...
init(_ boardData: BoardData ) {
self.numX = boardData.numX
self.numY = boardData.numY
for index in boardData.cells.keys {
cells[index] = LGCell(state: boardData.cells[index] ?? .alive)
}
}
func boardData() async -> BoardData {
let cellStatus = await self.boardStatus()
return BoardData(numX: numX, numY: numY, cells: cellStatus)
}
}
気をつけるべき点は、LGCell を Value として持つ Dictionary を使ってしまうと、LGCell は class として定義されているので取得元の LGBoard と共有されているという点です。
どちらかが LGCell の中身を変更してしまうと、他方にも影響を与えるということになります。
“変更しないように気をつける” という解決策もありますが、”Value タイプを使ってデータを受け取る” という解決策の方が、間違いが起こりにくいです。
ということで、[CellIndex: LGCellState] という型を使っています。(Swift の Dictionary は、Value タイプで、CellIndex, LGCellState それぞれ struct/ enum なので、どちらも Value タイプ です)
LGViewModel での LGBoard の管理
LGBoard の状態を保存/復元 できるようになりましたので、LGViewModel 側での管理を実装していきます。
複数の LGBoard の状態を LGViewModel 内に持つようにします。
class LGViewModel: ObservableObject {
var board: LGBoard
@Published var cells: [CellIndex:LGCellState] = [:]
// !!! NEW !!!
@Published var savedBoards: [(String,BoardData)] = []
}
あとから選択しやすくするために、名前とペアにして保存しておきます。
(ユーザーが名前を設定する想定です)
あとは、ユーザーが “Save”ボタンを押した時のための保存メソッドを作っておきます。
class LGViewModel: ObservableObject {
// ... omit ...
@MainActor
func save(_ name: String) async {
let boardData = await board.boardData()
self.savedBoards.append((name, boardData))
}
}
名前を引数に与えられて、その時点での LGBoard の情報とセットで保存しています。
パターンの保存と復元
これまで 事前に定義してあるパターンから選択できていましたが、同じリストの中に、ユーザーが作成したパターンも表示され、選択できるようにします。
パターンの保存
パターンの保存から実装します。
パターンの保存機能自体は、LGBoard/LGViewModel に追加しましたので、UI を作ります。
つまり、その時点でのボードの状態に名前をつけて保存する UI です。
“Save” ボタンを押したら、名前を聞くシートを表示し、そのシートで “OK” が押下されたら 保存する機能とします。
シートは、.sheet(isPresented:) で表示することにし、シート内には、パターンの名前を入力する TextField と 作成するかどうかの “OK” / “Cancel” ボタンを配置することにします。
せっかくなので(?)、iOS16/macOS13 から導入されたハーフモーダルにしてみます。
以下は、シート部分のコードを抜粋したものです。(全体のコードは記事の最後にまとめます)
.sheet(isPresented: $showSheet) {
VStack {
TextField(text: $patternName, label: { Text("Pattern Name: ") })
.textFieldStyle(.roundedBorder).padding()
HStack {
Button(action: {
guard patternName != "" else { return }
Task {
await viewModel.save(patternName)
patternName = ""
}
showSheet.toggle()
}, label: { Text("OK") }).disabled(patternName=="")
Button(action: { showSheet.toggle() }, label: { Text("Cancel") })
}
}
.presentationDetents([.fraction(0.3)]) // half-modal
.padding()
}
以下のような動作になります。
これで、LGViewModel には、Save指定したパターンが保持されることになります。
パターンの復元
これまでは、特定のパターンのみ 復元することが可能でした。
新しく保存したパターンも、選択できるようにします。
保存したパターンは、LGViewModel が持っていますので、既知のパターンと合わせて Picker で選択するようにします。
これまで、Picker の Tag は、数値でしたが、文字列に変更します。具体的には、パターンの名前にします。
これまでのパターンも新しいパターンも、パターンの名前で選択するようにします。
以下は、該当箇所の Picker と selection を監視している箇所の抜粋です。
Picker(selection: $selectedPattern, content: {
// (1)
Text("Blinker").tag("Blinker")
Text("Beacon").tag("Beacon")
Text("Octagon").tag("Octagon")
// (2)
ForEach(viewModel.savedBoards, id: \.0) { item in
// (3)
Text(item.0)
// (4)
.tag(item.0)
}
}, label: {
Text("select pattern")
})
.onChange(of: selectedPattern) { name in
Task {
// (5)
switch name {
// (6)
case "Blinker":
await viewModel.modelSetup(LGBoard.setupBlinker)
case "Beacon":
await viewModel.modelSetup(LGBoard.setupBeacon)
case "Octagon":
await viewModel.modelSetup(LGBoard.setupOctagon)
default:
// (7)
if let data = viewModel.savedBoards.first(where: {$0.0 == name}) {
await viewModel.modelSetup(data.1)
}
}
}
}
- これまでのパターンにも、String で tag を付与します
- LGViewModel が持っている保存されているパターンも合わせて列挙します
- (String,LGBoard) のタプルで持っているので、String を名称として表示します
- tag にも String を設定します
- 選択を検知して、選択されたパターンをセットします
- 事前設定されているパターンは、これまで通り closure で設定します
- 別途保存されているケースでは、LGViewModel が持っている情報で設定します
現在の実装では、以下のような制限があります。
・保存するパターンに既知のパターンと同じ名前をつけられると うまく処理できない
補足機能
以下の機能も追加します。
・パターン削除
・LGBoard クリア
パターン削除
現在選択されているパターンを削除する機能を実装します。既知のパターンであれば、削除せずに処理を終えます。
処理の実装としては、LGViewModel からデータを削除することになります。
以下の remove メソッドを実装します。
class LGViewModel: ObservableObject {
// ... omit ...
@MainActor
func remove(_ name: String) {
if let index = savedBoards.firstIndex(where: {$0.0 == name}) {
savedBoards.remove(at: index)
}
}
}
View 側には、”Save” ボタンの横に、”ゴミ箱”ボタンをつけます。削除すると、選択中のパターンは無くなってしまうので、選択を(デフォルトの) Blinker に戻します。
Button(action: {
viewModel.remove(selectedPattern)
selectedPattern = "Blinker"
}, label: { Image(systemName: "trash") })
LGBoardクリア
自分でパターンを作り始めると、まっさらの状態(すべて .dead) から設定したくなりました。
ということで、”Clear” ボタンを作ってみました。
Button(action: {
Task {
await viewModel.modelSetup({ _ in
.dead
})
}
}, label: { Text("Clear") })
LGViewModel に作った board 設定用のメソッドで、すべてを .dead にしています。
アプリ最終版
最終的に以下のようなアプリになりました
・パターンを自分で作る機能(タップで、.alive/.dead 調整、Save/Load)
・進化する機能
・時間経過で自動進化する機能(進化の時間調整機能付き)
まとめ
LifeGame を作りました。以下は、これまでの記事でのまとめのまとめです。
- 並行アクセスでデータ競合が発生しそうであれば、actor を使うことを検討する
- actor 内のプロパティは、actor の context からでないと同期的にアクセスできない
- データ競合が発生しないとわかっているプロパティは、nonisolated 指定することで、actor context 外からでも同期的にアクセスできるよう指定できる
- actor へのアクセスは非同期なので、View 向けには 同期アクセスできるデータが必要となる
- 盤状の要素配置は、 Grid/GridLayout が便利
- Concurrency を使っていると、withAnimation 指定できないケースがある
- .animation を使うと、監視する変数を指定してアニメーションできる
- 時間での実行は、Timer.TimerPublisher を使うと、簡単に実装できる
- 状態に応じて、disabled すると ユーザーにわかりやすいハズ
- Slider は線形に値を変化させる
- Binding を実装することで、線形でない値変化をもつ Slider を作れる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
最終的なコード
パターンを複数から選べるようになり、自動進化の時間も調整できるようになりました。
そろそろ、お決まりの(?) パターンに飽きてきたので、別パターンを試せるように改良していきます。
次回は、GUI 上で、パターンを作成する機能と 作成したパターンの保存機能を作ってみます。
LifeGame2022App.swift
//
// LifeGame2022App.swift
//
// Created by : Tomoaki Yagishita on 2022/11/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
@main
struct LifeGame2022App: App {
@StateObject var viewModel: LGViewModel = LGViewModel(numX: 8, numY: 8, LGBoard.setupBlinker)
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(viewModel)
}
}
}
LGBoardView.swift
//
// LGBoardView.swift
//
// Created by : Tomoaki Yagishita on 2023/01/10
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct LGBoardView: View {
@EnvironmentObject var viewModel: LGViewModel
@State private var selectedPattern: String = "Blinker"
@State private var showSheet: Bool = false
@State private var patternName: String = ""
var body: some View {
VStack {
Grid(horizontalSpacing: 0, verticalSpacing: 0) {
ForEach(0..<viewModel.numY, id: \.self) { yIndex in
GridRow {
ForEach(0..<viewModel.numX, id: \.self) { xIndex in
let cellIndex = CellIndex(x: xIndex, y: yIndex)
let cellState = viewModel.cells[cellIndex] ?? .alive
LGCellView(cellState: cellState)
.animation(.default, value: cellState)
.onTapGesture {
Task {
await viewModel.toggleCell(cellIndex)
}
}
}
}
}
}
Button(action: {
Task {
await viewModel.doPrep()
await viewModel.moveToNextGen()
await viewModel.updateCells()
}
}, label: {Text("NextGen")})
.buttonStyle(.bordered)
.disabled(viewModel.timerPubliser != nil)
HStack {
Button(action: {
viewModel.toggleAuto()
}, label: {Text("NextGen(Auto)")
.hidden()
.overlay {
Text(viewModel.timerPubliser == nil ? "NextGen(Auto)" : "Stop" )
}
})
.buttonStyle(.bordered)
HStack {
Image(systemName: "hare")
Text(viewModel.nextGenIntervalString)
Slider(value: Binding(get: {
log10(viewModel.nextGenInterval / (0.01) )
}, set: { newValue in
viewModel.nextGenInterval = 0.01 * pow(10.0, newValue)
}),
in: 1...2.5,
label: { Text("Interval") })
Image(systemName: "tortoise")
}
}
HStack {
Picker(selection: $selectedPattern, content: {
Text("Blinker").tag("Blinker")
Text("Beacon").tag("Beacon")
Text("Octagon").tag("Octagon")
ForEach(viewModel.savedBoards, id: \.0) { item in
Text(item.0)
.tag(item.0)
}
}, label: {
Text("select pattern")
})
.fixedSize()
.disabled(viewModel.timerPubliser != nil)
Button(action: {
showSheet.toggle()
}, label: {Text("Save")})
Button(action: {
viewModel.remove(selectedPattern)
selectedPattern = "Blinker"
}, label: { Image(systemName: "trash") })
Button(action: {
Task {
await viewModel.modelSetup({ _ in
.dead
})
}
}, label: { Text("Clear") })
}
}
.onAppear { Task { await viewModel.updateCells() } }
.onChange(of: selectedPattern) { name in
Task {
switch name {
case "Blinker":
await viewModel.modelSetup(LGBoard.setupBlinker)
case "Beacon":
await viewModel.modelSetup(LGBoard.setupBeacon)
case "Octagon":
await viewModel.modelSetup(LGBoard.setupOctagon)
default:
if let data = viewModel.savedBoards.first(where: {$0.0 == name}) {
await viewModel.modelSetup(data.1)
}
}
}
}
.sheet(isPresented: $showSheet) {
VStack {
TextField(text: $patternName, label: { Text("Pattern Name: ") })
.textFieldStyle(.roundedBorder)
.padding()
HStack {
Button(action: {
guard patternName != "" else { return }
Task {
await viewModel.save(patternName)
}
showSheet.toggle()
}, label: { Text("OK") }).disabled(patternName=="")
Button(action: { showSheet.toggle() }, label: { Text("Cancel") })
}
}
.presentationDetents([.fraction(0.3)])
.padding()
}
}
}
struct LGBoardView_Previews: PreviewProvider {
static var previews: some View {
LGBoardView()
}
}
LGCellView.swift
//
// LGCellView.swift
//
// Created by : Tomoaki Yagishita on 2023/01/10
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct LGCellView: View {
let cellState: LGCellState
var body: some View {
Image(systemName: symbolName)
.resizable().aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.rotation3DEffect(.degrees(cellState == .alive ? 0: 180), axis: (x: 0, y: -1, z: 0))
}
var symbolName: String {
switch cellState {
case .alive:
return "circle"
case .dead:
return "xmark"
}
}
}
struct LGCellView_Previews: PreviewProvider {
static var previews: some View {
HStack {
LGCellView(cellState: .alive)
LGCellView(cellState: .dead)
}
}
}
LGViewModel.swift
//
// LGViewModel.swift
//
// Created by : Tomoaki Yagishita on 2023/01/10
// © 2023 SmallDeskSoftware
//
import Combine
import SwiftUI
class LGViewModel: ObservableObject {
var board: LGBoard
@Published var cells: [CellIndex:LGCellState] = [:]
@Published var savedBoards: [(String,BoardData)] = []
@Published var nextGenInterval: TimeInterval = 2 { // 3 sec. {
didSet {
self.restartAuto()
}
}
@Published var timerPubliser: AnyCancellable? = nil
init(numX: Int, numY: Int,_ closure: @escaping BoardSetupClosure) {
board = LGBoard(numX: numX, numY: numY, closure)
}
func modelSetup(_ closure: BoardSetupClosure ) async {
await board.setup(closure)
await updateCells()
}
func modelSetup(_ boardData: BoardData) async {
board = LGBoard(boardData)
await updateCells()
}
@MainActor
func updateCells() async {
cells = await board.boardStatus()
}
func toggleCell(_ index: CellIndex) async {
await board.toggleCell(index)
await updateCells()
}
var numX: Int {
board.numX
}
var numY: Int {
board.numY
}
func doPrep() async {
await board.doPrep()
}
func moveToNextGen() async {
await board.moveToNextGen()
}
let numberFormatter: NumberFormatter = {
let nf = NumberFormatter()
nf.numberStyle = .decimal
nf.minimumFractionDigits = 2
nf.maximumFractionDigits = 2
return nf
}()
var nextGenIntervalString: String {
numberFormatter.string(from: nextGenInterval as NSNumber)!
}
func restartAuto() {
if timerPubliser == nil { return }
timerPubliser = Timer.TimerPublisher(interval: nextGenInterval, runLoop: .current, mode: .default)
.autoconnect()
.sink(receiveValue: { newDate in
Task {
await self.doPrep()
await self.moveToNextGen()
await self.updateCells()
}
})
}
func toggleAuto() {
if timerPubliser == nil {
timerPubliser = Timer.TimerPublisher(interval: nextGenInterval, runLoop: .current, mode: .default)
.autoconnect()
.sink(receiveValue: { newDate in
Task {
await self.doPrep()
await self.moveToNextGen()
await self.updateCells()
}
})
} else {
timerPubliser = nil
}
}
@MainActor
func save(_ name: String) async {
let boardData = await board.boardData()
self.savedBoards.append((name, boardData))
}
@MainActor
func remove(_ name: String) {
if let index = savedBoards.firstIndex(where: {$0.0 == name}) {
savedBoards.remove(at: index)
}
}
}
LGBoard.swift
//
// LGBoard.swift
//
// Created by : Tomoaki Yagishita on 2022/11/11
// © 2022 SmallDeskSoftware
//
import Foundation
import Distributed
enum LGCellState: CustomStringConvertible, Codable {
case alive, dead
var description: String {
switch self {
case .alive:
return "o"
case .dead:
return "x"
}
}
mutating func toggle() {
self = (self == .alive) ? .dead : .alive
}
}
class LGCell: Codable {
private(set) var state: LGCellState
private(set) var nextState: LGCellState?
init(state: LGCellState) {
self.state = state
self.nextState = nil
}
func toggle() {
state.toggle()
}
func doPrep(_ nextState: LGCellState) {
self.nextState = nextState
}
func moveToNextGen() {
guard let nextState = nextState else { fatalError() }
self.state = nextState
self.nextState = nil
}
}
struct CellIndex: Hashable, Codable {
let x: Int
let y: Int
var north : CellIndex { CellIndex(x: x-1, y: y ) }
var northWest: CellIndex { CellIndex(x: x-1, y: y-1) }
var west: CellIndex { CellIndex(x: x , y: y-1) }
var southWest: CellIndex { CellIndex(x: x+1, y: y-1) }
var south : CellIndex { CellIndex(x: x+1, y: y ) }
var southEast: CellIndex { CellIndex(x: x+1, y: y+1) }
var east: CellIndex { CellIndex(x: x , y: y+1) }
var northEast: CellIndex { CellIndex(x: x-1, y: y+1) }
var nbrIndexes:[CellIndex] {
[north, northWest, west, southWest,
south, southEast, east, northEast]
}
}
extension CellIndex: CustomStringConvertible {
var description: String {
return String("(\(x),\(y))")
}
}
typealias BoardSetupClosure = (CellIndex) -> LGCellState
struct BoardData: Codable {
let numX: Int
let numY: Int
let cells:[CellIndex: LGCellState]
}
actor LGBoard: ObservableObject {
nonisolated let numX: Int
nonisolated let numY: Int
var cells:Dictionary = [:]
init(numX: Int, numY: Int,_ closure: BoardSetupClosure = { index in .alive }) {
self.numX = numX
self.numY = numY
for x in 0.. LGCellState? {
return cells[index]?.state
}
func toggleCell(_ index: CellIndex) {
cells[index]?.toggle()
}
func boardStatus() async -> [CellIndex: LGCellState] {
var newDic: [CellIndex: LGCellState] = [:]
for key in cells.keys {
if let cell = cells[key] {
newDic[key] = cell.state
}
}
return newDic
}
func boardData() async -> BoardData {
let cellStatus = await self.boardStatus()
return BoardData(numX: numX, numY: numY, cells: cellStatus)
}
func countAliveInNbrCells(center: CellIndex) -> Int {
var numOfAlive = 0
for index in center.nbrIndexes {
guard let cell = cells[index] else { continue }
if cell.state == .alive {
numOfAlive += 1
}
}
return numOfAlive
}
func doPrep() {
for index in cells.keys {
guard let cell = cells[index] else { continue }
let nbrNumOfAlive = countAliveInNbrCells(center: index)
let nextState: LGCellState
switch cell.state {
case .alive:
nextState = (nbrNumOfAlive == 2)||(nbrNumOfAlive == 3) ? .alive : .dead
case .dead:
nextState = nbrNumOfAlive == 3 ? .alive : .dead
}
cell.doPrep(nextState)
}
}
func moveToNextGen() {
for key in cells.keys {
cells[key]?.moveToNextGen()
}
}
}
extension LGBoard {
static let setupBlinker: BoardSetupClosure = { index in
let liveCells = [CellIndex(x: 3, y: 3),
CellIndex(x: 3, y: 4),
CellIndex(x: 3, y: 5),
]
if liveCells.contains(index) { return .alive }
return .dead
}
static let setupBeacon: BoardSetupClosure = { index in
let liveCells = [CellIndex(x: 2, y: 2), CellIndex(x: 3, y: 2),
CellIndex(x: 2, y: 3), CellIndex(x: 3, y: 3),
CellIndex(x: 4, y: 4), CellIndex(x: 5, y: 4),
CellIndex(x: 4, y: 5), CellIndex(x: 5, y: 5),
]
if liveCells.contains(index) { return .alive }
return .dead
}
static let setupOctagon: BoardSetupClosure = { index in
let liveCells = [CellIndex(x: 3, y: 0), CellIndex(x: 4, y: 0),
CellIndex(x: 2, y: 1), CellIndex(x: 5, y: 1),
CellIndex(x: 1, y: 2), CellIndex(x: 6, y: 2),
CellIndex(x: 0, y: 3), CellIndex(x: 7, y: 3),
CellIndex(x: 0, y: 4), CellIndex(x: 7, y: 4),
CellIndex(x: 1, y: 5), CellIndex(x: 6, y: 5),
CellIndex(x: 2, y: 6), CellIndex(x: 5, y: 6),
CellIndex(x: 3, y: 7), CellIndex(x: 4, y: 7),
]
if liveCells.contains(index) { return .alive }
return .dead
}
}
Sponsor Link