Sponsor Link
環境&対象
- macOS Sonoma Beta 7
- Xcode 15 Beta 8
- iOS 17 Beta 8
- Swift 5.9
# 特に ベータ版の機能を使用する予定はありません。
全体の方針
以下のような方針で、作っていきます。
– 全体のアーキテクチャは、MVVM(Model-View-ViewModel) です
– TDD で作っていきます。
– Model は、actor で作ります
– スライドパズルは、アニメーションして動作させます
– macOS/iOS それぞれの UI を考慮しつつ、最大限コードを共通化します
- Step1: 全体構想 / MVVM 責務分割
- Step2: View 設計・実装・テスト
- Step3: Model 設計・実装・テスト
- Step4: ViewModel 設計・実装・テスト
- Step5: View-ViewModel 接続
- Step6: アニメーション追加
- Step7: 配置ランダム化
- Step8: 完成判定
- Step9: Animation 手直し
15Puzzle とは
これから作っていくのは 15パズル です。スライドパズルと呼ばれることもあるようです。
Wikipedia での説明は、こちら
# 上記の画像も、Wikipedia から引用しています。
いわゆる ソリティア と呼ばれる 一人で遊ぶタイプのゲームの1つです。
View-ViewModel 接続
作ってきた ViewModel を使って、View を表示するようにしていきます。
Puzzle15AppView と ViewModel
Puzzle15AppView の配下に、ViewModel とか変わるであろう Puzzle15BoardView と 操作用の Button を配置しています。
ですので、Puzzle15AppView が ViewModel を持つようにします。
Puzzle15BoardView には、EnvironmentObject として渡すようにしてみます。
現時点では、Button は動作しないので、Puzzle15AppView への変更はこれだけです。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2023/09/06
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct Puzzle15AppView: View {
@StateObject var viewModel = Puzzle15ViewModel()
var body: some View {
VStack {
Puzzle15BoardView()
.environmentObject(viewModel)
Button(action: {}, label: {
Text("Shuffle")
})
.accessibilityIdentifier("ShuffleButton")
}
.padding()
}
}
#Preview {
Puzzle15AppView()
}
Puzzle15BoardView と ViewModel
次に、Puzzle15ViewModel の情報を使って、Puzzle15BoardView を表示するようにします。
Grid/GridRow 向けに作成した ViewModel の panelRows メソッドを使用します。
//
// Puzzle15BoardView.swift
//
// Created by : Tomoaki Yagishita on 2023/09/06
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct Puzzle15BoardView: View {
@EnvironmentObject var viewModel: Puzzle15ViewModel
var body: some View {
Grid(horizontalSpacing: 2, verticalSpacing: 2, content: {
ForEach(0...3, id: \.self) { row in
GridRow(content: {
let panels = viewModel.panelRows(for: row)
ForEach(0...3, id: \.self) { col in
let panelNum = panels[col].value
let movable = panels[col].movable
Puzzle15PanelView(num: panelNum)
}
})
}
})
}
}
#Preview {
Puzzle15BoardView()
}
これで、以下のような表示になります。

panelRows から返される情報には、そのパネルが タッチ操作で移動できるかも含まれています。
実際にパネルを動かすためのメソッドも ViewModel に swap メソッドとして実装していますので、タッチされたときにそのパネルを移動する機能も追加します。
SwiftUI 的には、onTapGesture を使用することで タッチされた時の動作を指定できます。
スライドできる時には、movable に 移動先の Index2D が保持されているはずなので、その存在を確認して ViewModel の swap メソッドを呼び出します。
また、movable に Index2D が保持されていない時には、そのパネルは スライドできないということなので、.disable を設定して 操作できる対象としないようにします。
//
// Puzzle15BoardView.swift
//
// Created by : Tomoaki Yagishita on 2023/09/06
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct Puzzle15BoardView: View {
@EnvironmentObject var viewModel: Puzzle15ViewModel
var body: some View {
Grid(horizontalSpacing: 2, verticalSpacing: 2, content: {
ForEach(0...3, id: \.self) { row in
GridRow(content: {
let panels = viewModel.panelRows(for: row)
ForEach(0...3, id: \.self) { col in
let value = panels[col].value
let movable = panels[col].movable
Puzzle15PanelView(num: value)
.onTapGesture {
if let movable = movable {
viewModel.swap(Index2D(row, col), movable)
}
}
.disabled(movable == nil)
}
})
}
})
}
}
#Preview {
Puzzle15BoardView()
}
以下のような動作になります。
# アニメーションが一切ないので、パッと切り替わっています。
まとめ
15Puzzle 向けに View と ViewModel を接続し、なんとなくの動作をするところまできました。
- View は、async に取得できる情報では描画できないので、ViewModel が sync に提供することが必要
- View が要求する単位で情報提供できる メソッドを作っておくと便利
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
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版が最新版です。
Sponsor Link