【SwiftUI】タップでセルがセクション間を移動するリストを実装したメモ

ObservableObjectやCombineを使いつつ、セクション間を移動できるリストを実装してみました。試行錯誤しながら実装したので、もっと良い方法があるかと思いますが、現時点で学習したことの整理も含め記事にしています。
2020.10.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

概要

大阪オフィスの山田です。絶賛SwiftUI勉強中なので、ObservableObjectやCombineを使いつつ、セクション間を移動できるリストを実装してみました。試行錯誤しながら実装したので、もっと良い方法があるかと思いますが、現時点で学習したことの整理も含め記事にしています。

開発環境

  • Xcode: 11.7
  • macOS: 10.15.4

作るもの

セルにチェックボックスがあり、チェックすると下のセクションに、チェックを外すと上のセクションに移動するリストです。

モデルを作成

まずモデルを作成します。

class Item: Identifiable, ObservableObject {
    let id = UUID()
    let title: String
    @Published var check: Bool

    init(title: String, check: Bool) {
        self.title = title
        self.check = check
    }

    // 表示確認用データを作成
    static func make() -> [Item] {
        return [
            Item(title: "(´・ω・`)", check: true),
            Item(title: "( ᐛ?)パァ", check: true),
            Item(title: "( ᐛ?)ニャア", check: false),
            Item(title: "٩( ᐛ )و.", check: false),
            Item(title: "( ´・ω・`)人(´・ω・` )", check: false)
        ]
    }
}

Identifiableを継承しているのは、ForEachで使うためです。ObservableObjectを継承しているのは、checkプロパティの変更を検出するためです。makeメソッドは検証に使うテストデータを作るメソッドです。

セルのViewとViewModelを作成

class ItemCellViewModel: ObservableObject {
    @Published var item: Item
    @Published var stateSystemName: String = ""
    private var bag = [AnyCancellable]()

    init(item: Item) {
        self.item = item
        item.$check.map {
            $0 ? "checkmark.square" : "square"
        }
        .assign(to: \.stateSystemName , on: self)
        .store(in: &bag)
    }

    func switchItem() {
        item.check.toggle()
    }
}

struct ItemCell: View {
    @ObservedObject var vm: ItemCellViewModel

    var body: some View {
        HStack {
            Image(systemName: vm.stateSystemName)
                .onTapGesture {
                    self.vm.switchItem()
            }
            Text(vm.item.title)
        }
    }
}

ItemCellViewModelはモデルItemを生成時に受け取ります。stateSystemNameは、セルに表示するチェックボックスの画像名です。itemcheckプロパティに変更があった場合に、stateSystemNameを適切なものに変更しています。mapによりBoolをStringに変換して、assignメソッドを使ってstateSystemName変数に代入しています。ItemCellはセル内の画像をタップした際に、ViewModelswitchItemを実行してcheckのON/OFFを切り替えています。

ContentViewとContentViewModelの作成

struct ContentView: View {
    @ObservedObject var vm = ContentViewModel()

    var body: some View {
        List {
            Section(header: Text("Check False")) {
                ForEach(vm.items.filter { $0.check == false }) { item in
                    ItemCell(vm: ItemCellViewModel(item: item))
                }
            }
            Section(header: Text("Check True")) {
                ForEach(vm.items.filter { $0.check == true }) { item in
                    ItemCell(vm: ItemCellViewModel(item: item))
                }
            }
        }
    }
}

class ContentViewModel: ObservableObject {
    let items = Item.make()
    private var bag = [AnyCancellable]()

    init() {
        items.forEach { [weak self] item in
            guard let strongSelf = self else { return }
            item.objectWillChange.sink {
                strongSelf.objectWillChange.send()
            }.store(in: &strongSelf.bag)
        }
    }
}

ContentViewModelItemの配列を持ちます。ContentViewではcheckのon/offによってI Itemをどのセクションに表示するか分けています。ContentViewModelの初期化処理で、配列のItemのうち、どれかに変更があった場合、ContentViewModelobjectWillChenge.send()を実行し、変更があったことを通知するようにしています。この処理を入れないと、ContentViewに変更が通知されないため、チェックボックスのON/OFFを切り替えても、チェックボックスの画像は切り替わりますが、セルがSection間を移動しません。

終わり

今回、この記事を書ききるまで、結構、試行錯誤しました。 ModelをObservableObjectを適用するかどうかを考えたのですが、CoreDataで使うNSManagedObjectObservableObjectが適用されているので、それに倣いました。 EnvironmentObjectを使うことも考えましたが、Viewの階層も深くないし、まだ僕自身の理解も浅いので、現時点では使用を見送りました。 まだまだ勉強不足だなぁ。実装たのしー。

参考