実際に遊んでみると Animation に違和感を感じるので修正します。
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つです。
違和感とその原因
アニメーション時間を 2秒にしていることもあり、素早く操作されると以下のような表示になってしまいます。
原因は、アニメーションが終わらないうちに 次の操作が行われ、そのために 複数のアニメーションが並行実行されているためです。
なお、当初 アニメーションをわかりやすくするために、”2秒間” のアニメーションとしていたのですが、実際に操作すると、2秒間は長く感じるので、もっと短くしたくなります。
違和感の解消
違和感の解消方法ですが、シンプルに 「操作によるアニメーション途中には、操作を受け付けない」 とします。
別解としては、操作方法を キューに記録し、アニメーション終了ごとに順番に実行していく という方法も考えられるかと思います。
使用アニメーション変更
smooth アニメーションは、なんとなく 使っていたアニメーションだったので、interactiveSpring に切り替えます。
なお、定義をみるとわかるのですが、デフォルトの duration(アニメーション時間) は、0.15 秒のようです。
struct Puzzle15BoardView: View {
@EnvironmentObject var viewModel: Puzzle15ViewModel
@Namespace var namespace
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)
.accessibilityIdentifier(String(panelNum))
.matchedGeometryEffect(id: panelNum, in: namespace)
.transition(.scale(1.0))
.onTapGesture {
if let movable = movable {
withAnimation {
viewModel.swap(Index2D(row, col), movable)
}
}
}
.disabled(movable == nil)
}
})
}
})
// OLD
//.animation(.smooth(duration: 2), value: viewModel.layout)
// NEW
.animation(.interactiveSpring, value: viewModel.layout)
}
}
アニメーション終了まで操作を受け付けない
アニメーション途中であるかのフラグを作り、 アニメーション途中では、onTapGesture に反応しないようにします。
ViewModel にフラグ
フラグは、ViewModel に追加します。
class Puzzle15ViewModel: ObservableObject {
var model: Puzzle15Model
@Published var layout: Puzzle15Model.PanelLayout
var cancellable: AnyCancellable? = nil
@Published var showComplete = false
@Published var underOperation = false // true であればアニメーション中
// ... omit ...
}
上記の ViewModel のフラグを使って、View の enable を制御します。
//
// Puzzle15BoardView.swift
//
// Created by : Tomoaki Yagishita on 2023/09/06
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct Puzzle15BoardView: View {
@EnvironmentObject var viewModel: Puzzle15ViewModel
@Namespace var namespace
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)
.accessibilityIdentifier(String(panelNum))
.matchedGeometryEffect(id: panelNum, in: namespace)
.onTapGesture {
if let movable = movable {
withAnimation {
viewModel.swap(Index2D(row, col), movable)
}
}
}
.disabled(movable == nil)
.disabled(viewModel.underOperation == true) // !!! NEW !!!
}
})
}
})
.animation(.interactiveSpring, value: viewModel.layout)
}
}
#Preview {
Puzzle15BoardView()
}
こうすることで、(underOperation が正しく設定されば) アニメーション途中には、タップできなくなります。
アニメーション終了まで待つ
問題は、アニメーション終了の検知です。調べた範囲では、アニメーション終了時の call-back 等は無いようです。
ただ、自分でアニメーションを設定しているので、基本的に時間もわかるはずです。
ということで、自分で アニメーション時間分 待つことにしました。
ViewModel で時間を調整します。
class Puzzle15ViewModel: ObservableObject {
// ... omit ...
func swap(_ index1: Index2D,_ index2: Index2D) {
self.underOperation = true
Task {
await model.swap(index1, index2)
let state = await model.isComplete()
if state {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1))
self.showComplete = state
}
}
}
Task { @MainActor in
try? await Task.sleep(for: .seconds(0.2)) // アニメーション時間を待って、false に戻す (少し余裕をみてます)
self.underOperation = false
}
}
手直し後
手直ししたアニメーションが以下です。
アニメーション途中で操作をできなくしているのですが、
そもそも アニメーション時間を短くしたために、アニメーション途中で 次の操作をすることが 難しくなってます。
# アニメーション時間を長くしてみると、その間 クリックできないことがわかります。
まとめ
アニメーションを調整しました
- Animation は、実際の動作を見て調整が必要
- アニメーション途中で、別のアニメーションが同一要素に対して動き始めると違和感になる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
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