web-dev-qa-db-ja.com

SwiftUI:ObservableObjectは再描画されてもその状態を保持しません

問題

アプリのコードの見栄えを良くするために、ロジックを含むすべてのビューに対してViewModelを作成します。

通常のViewModelは次のようになります。

class SomeViewModel: ObservableObject {

    @Published var state = 1

    // Logic and calls of Business Logic goes here
}

次のように使用されます:

struct SomeView: View {

    @ObservedObject var viewModel = SomeViewModel()

    var body: some View {
        // Code to read and write the State goes here
    }
}

これは、ビューの親が更新されていない場合に正常に機能します。親の状態が変化すると、このビューは再描画されます(宣言型フレームワークではかなり正常です)。 しかしまた、ViewModelは再作成され、後で状態を保持しません。他のフレームワーク(例:Flutter)と比較すると、これは異常です。

私の意見では、ViewModelはそのままであるか、Stateが持続するはずです。

ViewModelを@Stateプロパティで置き換え、int(この例では)を直接使用すると、永続化されたままになり、再作成されません

struct SomeView: View {

    @State var state = 1

    var body: some View {
        // Code to read and write the State goes here
    }
}

これは明らかに、より複雑な州では機能しません。また、@Stateのクラス(ViewModelなど)を設定すると、期待どおりに機能しなくなります。

質問

  • 毎回ViewModelを再作成しない方法はありますか?
  • @State@ObservedObject Propertywrapperを複製する方法はありますか?
  • なぜ@Stateは再描画で国家を維持しているのですか?

通常、内部ビューでViewModelを作成することは悪い習慣ですが、この動作はNavigationLinkまたはSheetを使用して再現できます。
セル自体に多くのロジックが含まれている非常に複雑なTableViewを考える場合、ParentsViewModelで状態を保持してバインディングを操作することが役に立たない場合があります。
個々のケースには常に回避策がありますが、ViewModelを再作成しない方がはるかに簡単だと思います。

質問の重複

この問題については、非常に具体的なユースケースについての質問がたくさんあります。ここでは、カスタムソリューションについて深く掘り下げることなく、一般的な問題について説明します。

編集(より詳細な例を追加)

データベース、API、またはキャッシュからのリストのように、状態が変化するParentViewがある場合(単純なものについて考えてください)。 NavigationLinkを介して、データを変更できる詳細ページにアクセスできます。データを変更することにより、リアクティブ/宣言型パターンはListViewも更新するように指示します。これにより、NavigationLinkが「再描画」され、ViewModelが再作成されます。

ParentView/ParentViewのViewModelにViewModelを格納できることはわかっていますが、これはIMOを実行する方法としては間違っています。サブスクリプションが破棄または再作成されるため、副作用が発生する可能性があります。

7
KonDeichmann

最後に、アップルが提供するソリューションがあります:@StateObject

@ObservedObject@StateObjectに置き換えることで、最初の投稿で述べたすべてが機能します。

残念ながら、これはiOS 14以降でのみ利用できます。

これはXcode 12 Betaからの私のコードです(2020年6月23日公開)

struct ContentView: View {

    @State var title = 0

    var body: some View {
        NavigationView {
            VStack {
                Button("Test") {
                    self.title = Int.random(in: 0...1000)
                }

                TestView1()

                TestView2()
            }
            .navigationTitle("\(self.title)")
        }
    }
}

struct TestView1: View {

    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            Button("Test1: \(self.model.title)") {
                self.model.title += 1
            }
        }
    }
}

class ViewModel: ObservableObject {

    @Published var title = 0
}

struct TestView2: View {

    @StateObject var model = ViewModel()

    var body: some View {
        VStack {
            Button("StateObject: \(self.model.title)") {
                self.model.title += 1
            }
        }
    }
}

ご覧のとおり、StateObjectは、ObservedObjectがリセットされている間、親ビューの再描画時に値を保持します。

1
KonDeichmann

私はあなたに同意します。これはSwiftUIの多くの主要な問題の1つだと思います。これが私が自分でやっていることです。

struct MyView: View {
  @State var viewModel = MyViewModel()

  var body : some View {
    MyViewImpl(viewModel: viewModel)
  }
}

fileprivate MyViewImpl : View {
  @ObservedObject var viewModel : MyViewModel

  var body : some View {
    ...
  }
}

ビューモデルを適切に構築するか、または渡すことができます。これにより、再構築全体でObservableObjectを維持するビューが得られます。

1
Timothy

毎回ViewModelを再作成しない方法はありますか?

はい、ViewModelインスタンスoutside of SomeViewを保持し、コンストラクタを介して注入します

struct SomeView: View {
    @ObservedObject var viewModel: SomeViewModel  // << only declaration

@ObservedObjectの@State Propertywrapperを複製する方法はありますか?

必要ありません。 @ObservedObject is-aすでにDynamicProperty同様に@State

なぜ@Stateは再描画で国家を維持しているのですか?

つまり、ストレージを保持するためです。ラップされた値、ビューのoutside。 (したがって、上記の最初をもう一度参照してください)

0
Asperi

PassThroughSubjectクラスでカスタムObservableObjectを提供する必要があります。このコードを見てください:

_//
//  Created by Франчук Андрей on 08.05.2020.
//  Copyright © 2020 Франчук Андрей. All rights reserved.
//

import SwiftUI
import Combine


struct TextChanger{
    var textChanged = PassthroughSubject<String,Never>()
    public func changeText(newValue: String){
        textChanged.send(newValue)
    }
}

class ComplexState: ObservableObject{
    var objectWillChange = ObservableObjectPublisher()
    let textChangeListener = TextChanger()
    var text: String = ""
    {
        willSet{
            objectWillChange.send()
            self.textChangeListener.changeText(newValue: newValue)
        }
    }
}

struct CustomState: View {
    @State private var text: String = ""
    let textChangeListener: TextChanger
    init(textChangeListener: TextChanger){
        self.textChangeListener = textChangeListener
        print("did init")
    }
    var body: some View {
        Text(text)
            .onReceive(textChangeListener.textChanged){newValue in
                self.text = newValue
            }
    }
}
struct CustomStateContainer: View {
    //@ObservedObject var state = ComplexState()
    var state = ComplexState()
    var body: some View {
        VStack{
            HStack{
                Text("custom state View: ")
                CustomState(textChangeListener: state.textChangeListener)
            }
            HStack{
                Text("ordinary Text View: ")
                Text(state.text)
            }
            HStack{
                Text("text input: ")
                TextInput().environmentObject(state)
            }
        }
    }
}

struct TextInput: View {
    @EnvironmentObject var state: ComplexState
    var body: some View {
        TextField("input", text: $state.text)
    }
}

struct CustomState_Previews: PreviewProvider {
    static var previews: some View {
        return CustomStateContainer()
    }
}
_

最初に、TextChangerを使用して、CustomStateビューの.onReceive(...)に_.text_の新しい値を渡します。この場合のonReceivePassthroughSubjectではなくObservableObjectPublisherを取得することに注意してください。最後のケースでは、_Publisher.Output_には_perform: closure_のみが含まれ、NewValueは含まれません。その場合、_state.text_は古い値になります。

次に、ComplexStateクラスを見てください。テキストの変更がサブスクライバーに手動で通知を送信するようにobjectWillChangeプロパティを作成しました。 _@Published_ラッパーとほぼ同じです。しかし、テキストを変更すると、両方、およびobjectWillChange.send()textChanged.send(newValue)の両方が送信されます。これにより、状態の変化に対応する方法を正確にViewで選択できるようになります。通常の動作が必要な場合は、状態をCustomStateContainerビューの_@ObservedObject_ラッパーに入れるだけです。次に、すべてのビューを再作成し、このセクションでも更新された値を取得します。

_HStack{
     Text("ordinary Text View: ")
     Text(state.text)
}
_

それらすべてを再作成したくない場合は、@ ObservedObjectを削除してください。通常のテキストビューは更新を停止しますが、CustomStateは停止します。再作成なし。

更新:より詳細な制御が必要な場合は、値を変更するときに、その変更について誰に通知するかを決定できます。より複雑なコードを確認してください:

_//
//
//  Created by Франчук Андрей on 08.05.2020.
//  Copyright © 2020 Франчук Андрей. All rights reserved.
//

import SwiftUI
import Combine


struct TextChanger{
//    var objectWillChange: ObservableObjectPublisher
   // @Published
    var textChanged = PassthroughSubject<String,Never>()
    public func changeText(newValue: String){
        textChanged.send(newValue)
    }
}

class ComplexState: ObservableObject{
    var onlyPassthroughSend = false
    var objectWillChange = ObservableObjectPublisher()
    let textChangeListener = TextChanger()
    var text: String = ""
    {
        willSet{
            if !onlyPassthroughSend{
                objectWillChange.send()
            }
            self.textChangeListener.changeText(newValue: newValue)
        }
    }
}

struct CustomState: View {
    @State private var text: String = ""
    let textChangeListener: TextChanger
    init(textChangeListener: TextChanger){
        self.textChangeListener = textChangeListener
        print("did init")
    }
    var body: some View {
        Text(text)
            .onReceive(textChangeListener.textChanged){newValue in
                self.text = newValue
            }
    }
}
struct CustomStateContainer: View {
    //var state = ComplexState()
    @ObservedObject var state = ComplexState()
    var body: some View {
        VStack{
            HStack{
                Text("custom state View: ")
                CustomState(textChangeListener: state.textChangeListener)
            }
            HStack{
                Text("ordinary Text View: ")
                Text(state.text)
            }
            HStack{
                Text("text input with full state update: ")
                TextInput().environmentObject(state)
            }
            HStack{
                Text("text input with no full state update: ")
                TextInputNoUpdate().environmentObject(state)
            }
        }
    }
}

struct TextInputNoUpdate: View {
    @EnvironmentObject var state: ComplexState
    var body: some View {
        TextField("input", text: Binding(   get: {self.state.text},
                                            set: {newValue in
                                                self.state.onlyPassthroughSend.toggle()
                                                self.state.text = newValue
                                                self.state.onlyPassthroughSend.toggle()
        }
        ))
    }
}

struct TextInput: View {
    @State private var text: String = ""
    @EnvironmentObject var state: ComplexState
    var body: some View {

        TextField("input", text: Binding(
            get: {self.text},
            set: {newValue in
                self.state.text = newValue
               // self.text = newValue
            }
        ))
            .onAppear(){
                self.text = self.state.text
            }.onReceive(state.textChangeListener.textChanged){newValue in
                self.text = newValue
            }
    }
}

struct CustomState_Previews: PreviewProvider {
    static var previews: some View {
        return CustomStateContainer()
    }
}
_

ObjectWillChangeのブロードキャストを停止する手動バインディングを作成しました。ただし、同期を維持するには、この値を変更するすべての場所で新しい値を取得する必要があります。そのため、TextInputも変更しました。

それが必要ですか?

0
Aspid