例によるSwiftUI(SwiftUI by Example)5 Advanced state

Advanced state

@ObservedObject、@State、および@EnvironmentObjectの違いは何ですか?
What’s the difference between @ObservedObject, @State, and @EnvironmentObject?

SwiftUIでは、すべてのビューが単に状態の関数であることを覚えておくことが重要です。ビューを直接変更するのではなく、状態を操作して結果を決定します。

struct ContentView: View {
    @State private var score = 0
    // more code
}

@State
プロパティラッパーを使用して、SwiftUIにメモリの管理を依頼します。
@Stateプロパティを作成すると言うときは、ビューが存在する限りメモリ内に保持されるように、プロパティの制御をSwiftUIに渡します。その状態が変化すると、SwiftUIはビューに最新の変更を自動的に再読み込みして、新しい情報を反映できるようにします。

特定のビューに属し、そのビューの外部で使用されることのない単純なプロパティに最適です。そのため、そのような状態がそのビューから逃れることのないように特別に設計されているという考えを強化するために、これらのプロパティをプライベートとしてマークすることが重要です。

@ObservedObject
複数のプロパティとメソッドを持つ、または複数のビューで共有される可能性があるカスタムタイプを使用する場合
@ObservedObjectObservableObjectプロトコルに準拠する必要があります。
監視対象オブジェクトが重要なデータが変更されたことをビューに通知する方法はいくつかありますが、最も簡単なのは@Publishedプロパティラッパーを使用することです。

@EnvironmentObject
アプリケーション自体を介してビューで使用できるようになる値です
すべてのビューが読み取る必要のある重要なモデルデータがアプリにある場合は、ビューからビューへと渡すか、すべてのビューが即座にアクセスできる環境に配置することができます。

まとめ
@State単一のビューに属する単純なプロパティに使用します。それらは通常マークされるべきprivateです。
@ObservedObject複数のビューに属する可能性のある複雑なプロパティに使用します。参照型を使用しているときはいつでも@ObservedObject、それを使用する必要があります。
@EnvironmentObject共有データなど、アプリの他の場所で作成されたプロパティに使用します。

@ObservedObjectを使用して外部オブジェクトから状態を管理する方法
How to use @ObservedObject to manage state from external objects

重要な違いが1つあります。それは、settingsプロパティがプライベートとして宣言されていないことです。これは、バインドされたオブジェクトは複数のビューで使用できるため、オープンに共有するのが一般的であるためです。

class UserSettings: ObservableObject {
    @Published var score = 0
}

struct ContentView: View {
    @ObservedObject var settings = UserSettings()

    var body: some View {
        VStack {
            Text("Your score is \(settings.score)")
            Button(action: {
                self.settings.score += 1
            }) {
                Text("Increase Score")
            }
        }
    }
}


objectWillChangeを使用して状態の更新を手動で送信する方法
How to send state updates manually using objectWillChange

Error

import Combine
import SwiftUI

class UserAuthentication: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    var username = "" {
        willSet {
            objectWillChange.send()
        }
    }
}

struct ContentView: View {
    @ObservedObject var settings = UserAuthentication()

    var body: some View {
        VStack {
            TextField("Username", text: $settings.username)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Text("Your username is: \(settings.username)")
        }
    }
}


2020-10-14 14:29:27.072233+0900 SwiftUI-by-Example-1[4001:1778900] [Query] Error for queryMetaDataSync: 2
2020-10-14 14:29:27.073872+0900 SwiftUI-by-Example-1[4001:1778900] [Query] Error for queryMetaDataSync: 2


@EnvironmentObjectを使用してビュー間でデータを共有する方法
How to use @EnvironmentObject to share data between views

アプリ全体のすべてのビューと共有する必要があるデータについては、SwiftUIが提供します。
ビューAでデータを作成し、それをビューB、ビューC、ビューDに渡してから最終的に使用するのではなく、ビューで作成して環境に配置し、ビューB、C、およびDが自動的にアクセスできます。
環境オブジェクトは、祖先ビューによって提供される必要があります。

//-----------------------------------
class UserSettings: ObservableObject {
    @Published var score = 0
}
//-----------------------------------
struct ContentView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        NavigationView {
            VStack {
                // A button that writes to the environment settings
                Button(action: {
                    self.settings.score += 1
                }) {
                    Text("Increase Score")
                }

                NavigationLink(destination: DetailView()) {
                    Text("Show Detail View")
                }
            }
        }
    }
}
//-----------------------------------
struct DetailView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        // A text view that reads from the environment settings
        Text("Score: \(settings.score)")
    }
}

//-----------------------------------
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ///*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
        ContentView().environmentObject(UserSettings())
    }
}

import SwiftUI

@main
struct SwiftUI_by_Example_1App: App {
    var settings = UserSettings()
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(settings)
        }
    }
}


定数バインディングを作成する方法
How to create constant bindings

UIのプロトタイプを作成する場合、またはSwiftUIプレビューに何か意味のあるものを表示するために値を渡す必要がある場合は、定数バインディングを使用すると便利です。ハードコードされた値は変更されませんが、コードが機能するように、通常のバインディングのように使用されます。

Toggle(isOn: .constant(true)) {
    Text("Show advanced options")
}

カスタムバインディングを作成する方法
How to create custom bindings

Binding型を使用して手動でバインディングを作成することもできます。これには、値の読み取りまたは書き込み時に実行するカスタムgetおよびsetクロージャーを提供できます。
Bindingインスタンスにバインドする場合、バインディング名の前にドル記号を使用する必要はありません。

struct ContentView: View {
    @State private var username = ""

    var body: some View {
        let binding = Binding(
            get: { self.username },
            set: { self.username = $0 }
        )

        return VStack {
            TextField("Enter your name", text: binding)
        }
    }
}

カスタムバインディングは、読み取りまたは書き込み中のバインディングにロジックを追加する場合に役立ちます。値を送り返す前に計算を実行したり、値が変更されたときに追加のアクションを実行したりする場合があります。

たとえば、2つのトグルスイッチのスタックを作成して、両方をオフにし、どちらか一方をオンにすることはできますが、両方を同時にオンにすることはできません。一方を有効にすると、もう一方は常に無効になります。これがコードでどのように見えるかです:

struct ContentView: View {
    @State private var firstToggle = false
    @State private var secondToggle = false

    var body: some View {
        let firstBinding = Binding(
            get: { self.firstToggle },
            set: {
                self.firstToggle = $0

                if $0 == true {
                    self.secondToggle = false
                }
            }
        )

        let secondBinding = Binding(
            get: { self.secondToggle },
            set: {
                self.secondToggle = $0

                if $0 == true {
                    self.firstToggle = false
                }
            }
        )

        return VStack {
            Toggle(isOn: firstBinding) {
                Text("First toggle")
            }

            Toggle(isOn: secondBinding) {
                Text("Second toggle")
            }
        }
    }
}

SwiftUIでタイマーを使用する方法
How to use a timer with SwiftUI

コードを定期的に実行する場合は、TimerとonReceive()修飾子を使用する必要があります。


struct ContentView: View {
    @State var currentDate = Date()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        Text("\(currentDate)")
            .onReceive(timer) { input in
                self.currentDate = input
            }
    }
}

このコードは、毎秒起動するタイマーパブリッシャーを作成し、現在の時刻でラベルを更新します。


struct ContentView: View {
    @State var timeRemaining = 10
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        Text("\(timeRemaining)")
            .onReceive(timer) { _ in
                if self.timeRemaining > 0 {
                    self.timeRemaining -= 1
                }
            }
    }
}

ラベルの残り時間を表示するカウントダウンタイマーを作成できます。