LoginSignup
9
2

More than 3 years have passed since last update.

SwiftUIの@Stateは画面再表示時に初期化されない問題(Xcode11.1で解消されました)

Last updated at Posted at 2019-10-07

@capibara1969 さんありがとうございます! @Stateが初期化されない事象は、Xcode11.1で解消されました。
@State以外についても記載しているので記事としては残しておきます。初期化されないと記載の部分は読み替えてください。

モチベーション

sheet を使って表示した画面の @State プロパティは、画面再表示時に 前回の値を保持してしまっている(するのが想定の挙動?)
iOSのモーダル表示のようなものだと考えていたら挙動が異なるので、
@Binding@ObservedObject のプロパティも含めて挙動を確認します。

SwiftUIは現在は情報がネットに少ないので、つまづいた部分を共有します。
そして有識者にフィードバックをいただければ尚ありがたいです。

環境

PC: Catalina (10.15)
Xcode: 11.0 (11A420a)
iOS: 13.0
実行環境: iPhone11 シミュレーター

確認用アプリ

2画面の構成で、sheet (SwiftUI) と、present modal (UIKit) で画面を開き、開いた先の画面でカウントアップするアプリとなります。

ソースは本記事の末尾に載せておきます。

sheetを使って画面を開く

@State

遷移先画面でカウントアップして、閉じて再表示したときにカウントアップした値がそのまま表示されます。

@Binding

遷移元の@Stateのプロパティを遷移先の画面のプロパティに@Bindingを使って関連付けしています。
@Bindingは 元画面と値を共有するときなどに利用するものなので想定の動きをしています。

@ObservedObject

カウントを保持するための、ObservableObjectに準拠したクラスを作成して、
それを遷移先画面で、@ObservedObject プロパティとして利用しています。

遷移先画面でカウントアップして、閉じて再表示したときにカウントアップした値が初期化されます。

present modal を使って画面を開く

これは、SwiftUIではなくUIKitを使った画面遷移になっております。
注意: 画面遷移が公式で紹介されているようなやり方ではないです。

@State は画面を開くたびに初期化されます。
@Binding は遷移元画面と共有されます。
@ObservedObject は sheetを使った遷移と同様に、画面を開くたびに初期化されます。

この画面遷移だと、以下のようなコードで画面を閉じることができなくなるデメリットもあります。

 private struct AnotherView: View {
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
            Text("閉じる")
        }
     }
  }

ソースコード

struct ContentView: View {
    @State var isShow = false
    @State var count: Int = 0
    var body: some View {
        VStack(alignment: .center, spacing: 20) {
            Text("@Stateカウンター:\(self.count)")
            Button(action: {self.isShow.toggle()}) {
                Text("sheetを使い画面を開く")
            }.sheet(isPresented: $isShow) {
                NavigationView {
                    AnotherView(bCount: self.$count)
                }
            }
            Button(action: {
                let window = UIApplication.shared.windows.first
                let vc = UIHostingController(rootView: NavigationView { AnotherView(bCount: self.$count) })
                window?.rootViewController?.present(vc, animated: true)
            }) {
                Text("modalとして画面を開く")
            }
        }
    }
}

private struct AnotherView: View {
    @Binding var bCount: Int
    @State var sCount: Int = 0
    @ObservedObject var state = CounterState()

    var body: some View {
        Form {
            Section {
                Text("@Binding カウンター:\(self.bCount)")
                Button(action: { self.bCount += 1 }) {
                    Text("カウントアップ")
                }
            }
            Section {
                Text("@State カウンター:\(self.sCount)")
                Button(action: { self.sCount += 1 }) {
                    Text("カウントアップ")
                }
            }
            Section {
                Text("@ObservableObject カウンター:\(self.state.count)")
                Button(action: { self.state.count += 1 }) {
                    Text("カウントアップ")
                }
            }
        }.navigationBarTitle("カウントアップ")
    }
}

final class CounterState: ObservableObject {
    @Published var count: Int = 0
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            ContentView()
        }
    }
}

まとめ

@State は SwiftUIを使う上で欠かせないものなので、特徴を理解して使っていきたい。
UIHostingController を使った presentでの画面遷移も選択肢としてあるかもしれないが、出来れば使用したくない。
画面のプロパティを保持しておきたくない場合は、@ObservedObject として利用するのが現時点での有力な選択肢になりそう。

参考

9
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2