🦋
SwiftUI: Observableの使い方メモ
Item(前提)
struct Item: Identifiable {
var value: String
var id: String { value }
}
普通にViewModelを定義する
import Observation
@Observable class ContentViewModel {
var items = [Item]()
var flag = false
init() {}
}
変更通知したくない時
プロパティに@ObservationIgnored
をつける。
プロトコルを経由してViewModelを定義する
import Observation
protocol ContentViewModelProtocol: Observable {
var items: [Item] { get set }
var flag: Bool { get set }
}
@Observable class ContentViewModel: ContentViewModelProtocol {
var items = [Item]()
var flag = false
init() {}
}
定義したViewModelを使う
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(item.value)
}
}
.onAppear {
viewModel.items = (0 ..< 5).map { Item(value: $0.description) }
}
}
}
ViewModelを子Viewにも伝搬させる
パターン1:値を読むだけ
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(item.value)
}
ChildView(viewModel: viewModel)
}
.onAppear {
viewModel.items = (0 ..< 5).map { Item(value: $0.description) }
}
}
}
struct ChildView: View {
// ただのvarでOK
var viewModel: ContentViewModel
var body: some View {
Image(systemName: viewModel.flag ? "checkmark" : "xmark")
}
}
パターン2:バインドして値を変更する
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(item.value)
}
ChildView(viewModel: viewModel)
}
.onAppear {
viewModel.items = (0 ..< 5).map { Item(value: $0.description) }
}
}
}
struct ChildView: View {
// @Bindableをつけると値の変更を伝搬できる
@Bindable var viewModel: ContentViewModel
var body: some View {
Toggle(isOn: $viewModel.flag) {
Image(systemName: viewModel.flag ? "checkmark" : "xmark")
}
}
}
補足
関数を叩いて値を変更する分には@Bindable
をつける必要はない。
@Observable class ContentViewModel {
var items = [Item]()
var flag = false
init() {}
// 変更を加える関数
func appendItem() {
items.append(Item(value: items.count.description))
}
}
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(item.value)
}
ChildView(viewModel: viewModel)
}
.onAppear {
viewModel.items = (0 ..< 5).map { Item(value: $0.description) }
}
}
}
struct ChildView: View {
// @Bindableをつけていない
var viewModel: ContentViewModel
var body: some View {
VStack {
Image(systemName: viewModel.flag ? "checkmark" : "xmark")
Button("append item") {
viewModel.appendItem()
}
}
}
}
Viewの外からViewModelを注入する場合の話
レガシーなObservableObject
の場合はStateObject.init()
の引数が@autoclosure
だったので注意が必要だった。
@MainActor @frozen @propertyWrapper @preconcurrency public struct StateObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
@inlinable nonisolated public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
}
Observable
の場合はState.init()
の引数は@autoclosure
ではないので初期化のタイミングのことは難しく考えずに渡しても大丈夫なはず。
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
public init(wrappedValue value: Value)
}
Discussion