11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Swift] View から分離した @State が動作しない場合の対応

Posted at

初めに

SwiftUI で開発していると

  • @State
  • @Binding
  • @Published

のようなバインディング(アノテーション)を使用することは増えてくるでしょう。

その際に上手く動作しないことがあり、何度か調べたりすることがあったので、残しておきます。

実装

以下のパターンがあったので、順々に説明していきます。

  • Case1: View と状態管理クラスを分離する場合
  • Case2: 状態管理クラスが階層構造の場合

また、今回のまとめでは Swift Playgrounds でテストしたものを使用しています。

Case1: View と状態管理クラスを分離する場合

【前提】

状態管理を含む SwiftUI のコード例は、だいたい View 内に状態管理のプロパティを保持していることが多いです。

sample_1

当たり前ですが、更新されたテキストが TextField に反映されていることがわかります。



ただ、実際にアプリを作る場合、状態管理の部分は View とは分離するでしょう。
sample_2

分離先の StateObject クラスで @State がうまく動作しません。

【解決策】

@State はその画面で保持して使用しないと動作しません。
なので今回の場合は @State@Published にすることで動作します。
sample_3

無事に動作することが確認できます。

Code
StateObject.swift
final class StateObject: ObservableObject {
    @Published var bindingText: String = ""
}
ContentView.swift
struct ContentView: View {
    @ObservedObject var stateObject: StateObject
    
    var body: some View {
        TextField("text", text: $stateObject.bindingText)
            .onAppear {
                stateObject.bindingText = "update text"
            }
    }
}

Case2: 状態管理クラスが階層構造の場合

【前提】

状態を分離したコードで、さらにクラスなどを保持することはあるかと思います。
また、そのクラスから値をバインドすること(階層構造の場合)もあるでしょう。
sample_4

階層構造先のクラスを ObservableObject に対応させ、@ObservedObject で保持していますが、うまく動作しません。

【解決策】

ObservableObjectobjectWillChange を自前で更新することで解決します。
(詳しくは以下を読むと良いでしょう)

書き方は2パターンあるので、それぞれ載せておきます。


- パターン① ObservableObject を使用する

前提にあるコードと同じく、階層構造先の Binding クラスを ObservableObject に準拠させている場合です。

sample_5

階層構造先の objectWillChange をハンドリングして、呼び出し元の objectWillChange にハンドリングすることで、反映することができます。

Code
Binding.swift
final class Binding: ObservableObject {
    @Published var bindingText: String = ""
}
StateObject.swift
final class StateObject: ObservableObject {
    @ObservedObject var binding: Binding = .init()
    private var anyCancellable: AnyCancellable?
    
    init() {
        anyCancellable = binding
            .objectWillChange
            .sink { [weak self] _ in
                self?.objectWillChange.send()
            }
    } 
}
ContentView.swift
struct ContentView: View {
    @ObservedObject var stateObject: StateObject
    
    var body: some View {
        TextField("text", text: $stateObject.binding.bindingText)
            .onAppear {
                stateObject.binding.bindingText = "update text"
            }
    }
}

- パターン② @Published を使用する

@Published で値をバインドさせます。

sample_6

階層構造先から $~~~ で値をハンドリングして、呼び出し元の objectWillChange にハンドリングすることで、反映することができます。

Code
Binding.swift
final class Binding {
    @Published var bindingText: String = ""
}
StateObject.swift
final class StateObject: ObservableObject {
    @Published var binding: Binding = .init()
    private var anyCancellable: AnyCancellable?
    
    init() {
        anyCancellable = binding
            .$bindingText
            .sink { [weak self] _ in
                self?.objectWillChange.send()
            }
    } 
}
ContentView.swift
struct ContentView: View {
    @ObservedObject var stateObject: StateObject
    
    var body: some View {
        TextField("text", text: $stateObject.binding.bindingText)
            .onAppear {
                stateObject.binding.bindingText = "update text"
            }
    }
}

その他

開発の都度、つなぎ目のバインド(objectWillChange)を書くのは大変です。
こちらのブログにあるように、プロトコルを作り継承させ、隠蔽化する方法もあります。

以下、該当箇所を抜粋。

.swift
extension ViewModelObject where Binding.ObjectWillChangePublisher == ObservableObjectPublisher, Output.ObjectWillChangePublisher == ObservableObjectPublisher {

    var objectWillChange: AnyPublisher<Void, Never> {
        return Publishers.Merge(binding.objectWillChange, output.objectWillChange).eraseToAnyPublisher()
    }

}

終わりに

階層構造の場合に上手く動作しなかったのは、最初は原因がわからずかなり手こずりました。

少なからずアーキテクチャーを導入すると、クラスを分離することになるので、今回のようなパターンは出てくるかと思います。

ご指摘などあればお願いいたしますmm

11
11
0

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
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?