0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TCAの@Sharedを使ってStateを共有する

Posted at

親と子の間Stateを共有する

長い間TCAを使っている方は親と子の間Stateを共有したいシチュエーションに遭遇したと思います。自分の方でも少し整理したので、この記事書くことにしました。間違いがありましたら遠慮なくコメントで指摘お願いします!

親と子の間でStateを共有

これは一番シンプルな使い方ですね:

@Reducer
struct ParentFeature {
  @ObservableState
  struct State {
    @Shared var count: Int           // 1
    var child: ChildFeature.State
    // Other properties
  }
  // ...
}
@Reducer
struct ChildFeature {
  @ObservableState
  struct State {
    @Shared var count: Int          // 2
    // Other properties
  }
  // ...
}
case .presentButtonTapped:
  state.child = ChildFeature.State(count: state.$count)  // 3
  // ...
  1. ParentFeatureの中で@Sharedのcountを定義します
  2. ChildFeatureも同じcountを定義します
  3. ParentFeatureが持つ子のFeature(この場合はchild)を作成する時、$countを使ってParentFeatureのcountの参照をChildFeatureに渡す事ができます。

ChildFeatureのcountを変更するとParentFeatureのcountも変更され、もちろんParentFeatureのcountを変更するとChildFeatureのcountも変更されます(当たり前ですね、参照渡してますので)

アプリの何処でもstateを取れるにしたい(永続性あり)

上の例は参照を子に渡していますが、場合によっては渡さなくてもstateをシェアした、下の例をみましょう:

@Reducer
struct ChildFeature {
  @ObservableState
  struct State {
    @Shared(.inMemory("count")) var count = 0 // 1
    // Other properties
  }
  // ...
}
case .onAppear:
  return .publisher {      // 2
    state.$count.publisher
      .map(Action.countUpdated)
  }


case .countUpdated(let count):
  // Do something with count
  return .none
case .onAppear:
  return .publisher {
    state.$count.publisher
      .map(Action.countUpdated)
  }


case .countUpdated(let count):
  state.count = count + 1 // 3 ❌
  return .none
  1. default値を設定する必要があり、最初の値とします。ここの例は .inMemory使っていますが、TCAは.appStorage.fileStorage複数のパターン用意されてます。.appStorageUserDefault内のデータを取り、.fileStorageは指定したファイル内のデータを取ります。色々用意されてますね
  2. ChildFeatureがcountの変更を受けたい場合、.publisherを使ってcountを観測でき、そして呼び起こすActionも指定できます。.mapなのでTypeが同じであれば指定できる
  3. この場合は無限ループが発生するので要注意ですね(Feature内.publisher@Sharedは同時に入れるのをやめましょう)

これで参照をわざわざ渡さなくても、stateをシェアできる

@Shared stateの初期値

まず@Sharedの定義を見てみると:

/// A property wrapper type that shares a value with multiple parts of an application.
///
/// See the <doc:SharingState> article for more detailed information on how to use this property
/// wrapper.
@dynamicMemberLookup
@propertyWrapper
public struct Shared<Value: Sendable>: Sendable {
  private let reference: any Reference
  private let keyPath: _SendableAnyKeyPath

  init(reference: any Reference, keyPath: _SendableAnyKeyPath) {
    self.reference = reference
    self.keyPath = keyPath
  }
  ...
protocol Reference<Value>: AnyObject, CustomStringConvertible, Sendable {
  associatedtype Value: Sendable
  var value: Value { get set }

  func access()
  func withMutation<T>(_ mutation: () throws -> T) rethrows -> T
  #if canImport(Combine)
    var publisher: AnyPublisher<Value, Never> { get }
  #endif
}

Shared内はreference: Referenceで定義されてますね、そしてReferenceの中にvalueが見つかりました。ReferenceAnyObjectですね!
となるとSharedの内部は実際Referenceの参照を持っていて、Referenceの中に本当のvalueが存在しています。

@Shared state初期化する

通常Stateの初期化メソッドは我々で定義する必要はなく、自動生成されたinitを使えばいいのですが、@Shared含めたStateには自動生成されたinitでは不十分です。その原因は@Sharedのstateは実際value typeではなく、参照型です。親が持ってた子供の状態は、親からあげたのか?それとも子供自身が作れた状態を分けたいですね。なのでcustom initで明白にする必要があります。

valueの根本は親からの場合

public struct State {
  @Shared public var count: Int
  // other fields


  public init(count: Shared<Int>, /* other fields */) { // 1
    self._count = count
    // other assignments
  }
}
  1. この場合custom initは親の参照で初期化します

valueの初期値は自分からの場合

public struct State {
  @Shared public var count: Int
  // other fields


  public init(count: Int, /* other fields */) { // 1
    self._count = Shared(count)                 // 2
    // other assignments
  }
}
  1. この場合custom initは本当のvalue(Int)で初期化します
  2. そして初期化メソッド内で Sharedを作成します

永続性ありの@Shared初期化

public struct State {
  @Shared public var count: Int                                     // 2
  // other fields


  public init(count: Int, /* other fields */) {                     // 1
    self._count = Shared(wrappedValue: count, .appStorage("count")) // 3
    // other assignments
  }
}
  1. 永続性ありの@Sharedのcustom initも本当のValue(Int)を渡します。
  2. 既にお気づきになられたと思いますが、ここの .appStorageは省略可能です、永続性設定は初期化メソッドで指定しましたので(参照だからですかね :smile:
  3. custom init内で永続性ありの@Shared初期化する場合は wrappedValue:がある初期化メソッドを使用する必要があります。実際ここのcountは@autoclosure () -> Intにする方がわかりやすくなると思います。以下の例を考えましょう:
    1. .appStorage内は実際countがありました(例えば前回アプリ起動した時値が保存された)
    2. 当然、初期化メソッドが渡されたcountを使わずに、.appStorage内の値を使ってSharedを作成するのが正しい

なので以下の書き方もできます:

public struct State {
  @Shared public var count: Int
  // other fields


  public init(count: @autoclosure () -> Int, /* other fields */) {
    self._count = Shared(wrappedValue: count(), .appStorage("count"))
    // other assignments
  }
}

まとめ

Shared state(永続性ありのパターン)を使う時の手順:

  1. Feature内共有したいstateを@Sharedを付ける
  2. Stateのcustom initを追加(適切なcustom initを作りましょう)
  3. stateを取りたいFeature内に .publisherを追加して値を取る

これくらい分かれば大体問題無いと思いますが、他にも@SharedReaderとか、withLock とか知る必要がある物あります、どちらも理解しやすいと思います。

参考:
https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/sharingstate/#app-top

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?