この記事は公開されてから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
は、セルに表示するチェックボックスの画像名です。item
のcheck
プロパティに変更があった場合に、stateSystemName
を適切なものに変更しています。map
によりBoolをStringに変換して、assign
メソッドを使ってstateSystemName
変数に代入しています。ItemCell
はセル内の画像をタップした際に、ViewModel
のswitchItem
を実行して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)
}
}
}
ContentViewModel
はItem
の配列を持ちます。ContentView
ではcheck
のon/offによってI
Itemをどのセクションに表示するか分けています。ContentViewModel
の初期化処理で、配列のItem
のうち、どれかに変更があった場合、ContentViewModel
でobjectWillChenge.send()
を実行し、変更があったことを通知するようにしています。この処理を入れないと、ContentView
に変更が通知されないため、チェックボックスのON/OFFを切り替えても、チェックボックスの画像は切り替わりますが、セルがSection
間を移動しません。
終わり
今回、この記事を書ききるまで、結構、試行錯誤しました。
ModelをObservableObject
を適用するかどうかを考えたのですが、CoreDataで使うNSManagedObject
もObservableObject
が適用されているので、それに倣いました。
EnvironmentObject
を使うことも考えましたが、Viewの階層も深くないし、まだ僕自身の理解も浅いので、現時点では使用を見送りました。
まだまだ勉強不足だなぁ。実装たのしー。