[SwiftUI] 15Puzzle の作り方 (5: View-ViewModel 接続)

SwiftUI2021

     
⌛️ 2 min.
SwiftUI を使って、15パズルを作っていきます。

環境&対象

以下の環境で動作確認を行なっています。

  • 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 を考慮しつつ、最大限コードを共通化します

全体のステップは、以下です。

15Puzzle とは

これから作っていくのは 15パズル です。スライドパズルと呼ばれることもあるようです。

Wikipedia での説明は、こちら

15puzzle

# 上記の画像も、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()
}

これで、以下のような表示になります。

AppSnap_1

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 と ViewModel の接続
  • View は、async に取得できる情報では描画できないので、ViewModel が sync に提供することが必要
  • View が要求する単位で情報提供できる メソッドを作っておくと便利

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です