Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftUI: 更新検知と値の生存期間

Iceman
June 27, 2019

SwiftUI: 更新検知と値の生存期間

Iceman

June 27, 2019
Tweet

More Decks by Iceman

Other Decks in Programming

Transcript

  1. 任意の型を入れられるものと、 BindableObject を入れるものと2種 類がある 名前 中に入る型 @State T @Binding T

    @ObjectBinding T: BindableObject @EnvironmentObject T: BindableObject @Environment T @Stateなどは任意の型を使用できるが、 struct・enumとclassとで挙動が異なる(後述) 順にみていく
  2. @State 値を保持できる。値が変化したとき値を使用したViewがリビルドされる struct StateBasic : View { @State var count:

    Int = 0 var body: some View { VStack { Text("\(count)") Button(action: { // ボタンを押すと表示が更新 self.count += 1 }) { Text("inc") } } } }
  3. struct StateUnused : View { @State var okane: Int =

    0 var body: some View { VStack { if AccountManager.shared.isPurchased { // 初回でここに入らなかった場合は // 以降okane が変化してもリビルドされない Text("\(okane)") } else { Text("not purchased") } Button(action: { AccountManager.shared.isPurchased = true self.okane += 1 }) { Text("purchase") } } } }
  4. 修正後 struct StateUnusedFix : View { @ObjectBinding var accountManager: AccountManager

    @State var okane: Int = 0 var body: some View { VStack { if accountManager.isPurchased { Text("\(okane)") } else { Text("not purchased") } Button(action: { self.accountManager.isPurchased = true self.okane += 1 }) { Text("purchase") } } } } propertyDelegateで監視されていない値を使って分岐をしてはいけない
  5. private struct IncButton: View { @Binding var count: Int var

    body: some View { Button(action: { self.count += 1 }) { Text("inc") } } } struct BindingBasic : View { @State var count = 0 var body: some View { VStack { Text("\(count)") // ↓ 値への参照を渡して更新してもらう IncButton(count: $count) } } }
  6. @Binding うまく使えばリビルド範囲を制御できるかもしれない? リビルド範囲を狭めることでパフォーマンスに恩恵があるかは不明 struct BindingSibling : View { @State var

    count = 0 var body: some View { // count が変化してもこれ自体はリビルドされない VStack { CounterView(count: $count) // ← この中はリビルド IncButton(count: $count) } } }
  7. @ObjectBinding BindableObject に適合したオブジェクトは@ObjectBindingを使用す ることで更新検知できるようになる public protocol BindableObject : AnyObject, DynamicViewProperty,

    Identifiable, _BindableObjectViewProperty { associatedtype PublisherType : Publisher where Self.PublisherType.Failure == Never var didChange: Self.PublisherType { get } }
  8. 使用例? それらしい使い方だが、これは罠にはまる class CountViewModel: BindableObject { let didChange = PassthroughSubject<CountViewModel,

    Never>() var count: Int = 0 func inc() { count += 1 didChange.send(self) } } struct ObjectBindingBasic : View { @ObjectBinding var viewModel = CountViewModel() var body: some View { VStack { Text("\(viewModel.count)") Button(action: { self.viewModel.inc() }) { Text("inc") } } } }
  9. 値が保持されない問題の回避策? viewModelを@Stateとして保持することでNavigation単位で生存 させられる struct ObjectBindingBasicFix : View { @State var

    viewModel = CountViewModel() var body: some View { VStack { Text("\(viewModel.count)") Button(action: { self.viewModel.inc() }) { Text("inc") } } } } しかし@Stateにclassを保持させること自体は想定されていなさそ うなため、おとなしく親Viewが子ViewのviewModelを管理してあ げるか後述する@EnvironmentObjectを利用したほうがよさそう
  10. @EnvironmentObject struct CounterPage: View { @EnvironmentObject var counter: CounterEnvironment var

    body: some View { // counter が利用できる or 実行時クラッシュ ... } } struct EnvironmentObjectBasic : View { var body: some View { VStack { CounterPage() CounterPage() } .environmentObject(CounterEnvironment()) // ⭐ } } ⭐ の行がなければ実行時クラッシュする
  11. 画面単位でしか生存していないので、NavigationかPresentationを またぐとリセットされる struct EnvironmentObjectPush : View { var body: some

    View { VStack { // どちらもボタンタップでクラッシュする NavigationButton(destination: CounterPage()) { Text("push") } PresentationButton(destination: CounterPage()) { Text("present") } } .environmentObject(CounterEnvironment()) } }
  12. @Environment まず EnvironmentValues に値を生やす extension EnvironmentValues { var counter: CounterEnvironment

    { get { self[CounterEnvironmentKey.self] } set { self[CounterEnvironmentKey.self] =newValue} } } struct CounterEnvironmentKey: EnvironmentKey { static var defaultValue: CounterEnvironment {.init()} } struct CounterEnvironment { var count: Int = 0 }
  13. その後@Environmentと取り出したいプロパティのkeyPathをわた して取り出す struct CounterPage2: View { @Environment(\.counter) var counter: CounterEnvironment

    var body: some View { VStack { Text("\(counter.count)") Button(action: { // Environment はget-only なため変更できない }) { Text("inc") } } } }
  14. 生存期間まとめ 名前 生存期間 @State Navigation単位。Presentationでは値が残る @Binding 参照先のオブジェクトと同じ @ObjectBinding 挙動が不安定だが基本生存しないがち @EnvironmentObject

    Navigation・Presentation単位 @Environment Navigation・Presentation単位 あくまで現時点。リリースされるころには変わってそう Viewの構成によって変わることがある