LoginSignup
2

More than 1 year has passed since last update.

TCAでdelegateを処理するパターン

Last updated at Posted at 2021-01-24
1 / 22

はじめに

この発表資料は、
iOSアプリ開発のためのFunctional Architecture情報共有会3の発表資料です。

内容として、TCAがdelegateを処理するパターンを整理し、Core DataのNSFetchedResultsControllerDelegateで自動で差分を検知してくれる仕組みをTCAでも使えるようにするという内容です。


導入

iOSDC2020 iOSアプリ開発のための"The Composable Architecture"がすごく良いので紹介したいで出た質問から

TCAではActionはViewStoreからしか発火できないので、ユーザ操作からしかActionを発火できないですよね?(だからAppDelegateのイベントとかからActionを呼び出せないのどうするんですか?)


ActionはViewStoreからしか呼び出せないわけじゃない

  • ActionはAction自体から呼び出せる
    • そもそもReducerはEffectを返しそこで別のActionにmapすることができる
  • 例えば何かを監視する際にdelegateの実行をトリガーにしてActionを呼び出せる

  1. アプリ起動でView表示
  2. SwiftUIならonAppear { viewStore.send( Action.A ) }
  3. Reducerの処理でDelegateを作成して実行しておく
  4. delegateの処理で実行されてそこからSubscriberがAction.Bを呼び出す

話すこと

ユーザアクションに縛られないActionの発行


参考

  • TCAが用意してくれてるライブラリ
    • LocatoionManager
    • MotionManager
  • TCAのサンプル
    • WebSocketClient
    • SpeechClient

ユーザアクションに縛られず、何かしらを監視しdelegateで処理を呼び出し、SubscriberでActionを発火させてる例がある


余談: よくわからないこと

  • ManagerとClientの名前の違いはどこから?
    • Managerが専用のActionとかErrorを持ってるわけじゃない
    • LocationManager
      • Action
      • Error
    • MotionManager
    • WebSocketClient
      • Action
      • Error
    • SpeechClient
      • Action

共通するパターン

  • 監視開始用クロージャを定義
  • staticな初期化でシングルトン的に作れるようにする(.live)
    • 監視開始用クロージャを上書きする
    • delegateを作成
    • delegateが実行するメソッドからActionを呼び出すようにする
  • Reducerからシングルトンを呼び出す
    • Actionを次のActionにmapできるようにする

実際のコードからこのパターンを理解する


LocationManager

  • ユーザの位置情報をリアルタイムに検知してActionを発火してその位置を教えてくれる
  • ReducerのActionとして位置情報を伝えてくれる

TCA_delegate.001.png


実際のコード


プロパティとしてクロージャを用意

上書き可能なロジックを用意しておく

  • 上書きされる前に実行すると未実装としてクラッシュ
    • これはテストコードでも任意の実装に置き換えも可能
public struct LocationManager {
  ...
  var create: (AnyHashable) -> Effect<Action, Never> = { _ in 
      // ActionはLocationManager.Actionを省略できている
      _unimplemented("create") 
  }
  ...
}

Managerをstaticに作成

  • シングルトン的にしておく
    • classじゃなくてstructでいい
    • 状態の変化を通知するオブジェクトは重複してはいけないし壊されてもいけない
public struct LocationManager {
    ...
}

extension LocationManager {
  public static let live: LocationManager = { () -> LocationManager in
    var manager = LocationManager()

    ... /* 次にここで managerのクロージャを上書きする処理を書く */

    return manager
  }
}

先程用意したcreateプロパティにクロージャを代入

  • やること
    • iOSのCLLocationManagerでロケーションを探せるように
      • delegateをセットしEffectのsubscriberを渡す
    • privateなグローバル変数dependenciesにidをキーにして保持
      • delegateやmanagerやsubscriberを保持
    • AnyCancellableで終了時/キャンセル時にクリーンアップ
extension LocationManager {
  public static let live: LocationManager = { () -> LocationManager in
    var manager = LocationManager()    
    ...
    // ここから追記した __________________
    manager.create = { id in
      Effect.run { subscriber in
        let manager = CLLocationManager()
        var delegate = LocationManagerDelegate(subscriber)
        manager.delegate = delegate

        dependencies[id] = Dependencies(
          delegate: delegate,
          manager: manager,
          subscriber: subscriber
        )

        return AnyCancellable {
          dependencies[id] = nil
        }
      }
    }
    // 追記ここまで 
    ...

    return manager
  }
}

CLLocationManagerDelegateとは
  • Locationが変わった際のprotocolを定義している
    • delegateで特定のメソッドが実行される
public protocol CLLocationManagerDelegate : NSObjectProtocol {

    @available(iOS 6.0, *)
    optional func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
    ...
}

CLLocationManagerDelegateのメソッドでSubscriberがイベントを発火させる

  • let subscriber: Effect<LocationManager.Action, Never>.Subscriberをもたせる
  • subscriber.sendでActionを呼び出す
private class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
  let subscriber: Effect<LocationManager.Action, Never>.Subscriber

  // エラー時
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    subscriber.send(.didFailWithError(LocationManager.Error(error)))
  }
  // location変更時
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    subscriber.send(.didUpdateLocations(locations.map(Location.init(rawValue:))))
  }
  ...
}

Reducerからシングルトンを呼び出す

  • .create(id: LocationManagerId())
    • 実行時のユニークなハッシュをIDに
    • mapで次のアクションAppAction.locationManagerに変換
  • combineでReducerを繋げる
public enum AppAction: Equatable {
  case onApper
  case locationManager(LocationManager.Action)
  ...
}
...
public let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
  switch action {
  ...
  case .onAppear:
    return environment.locationManager.create(id: LocationManagerId())
      .map(AppAction.locationManager) // combineしているlocationManagerReducerを呼び出す
  ...
  }
}
.combined(
  with:
    locationManagerReducer
    .pullback(state: \.self, action: /AppAction.locationManager, environment: { $0 })
)
.signpost()
.debug()

private let locationManagerReducer = Reducer<AppState, LocationManager.Action, AppEnvironment> {
  state, action, environment in
  ...
}

Core DataのNSFetchedResultsControllerDelegateでDB監視を利用する

  • 監視開始用クロージャを定義
  • staticな初期化でシングルトン的に作れるようにする(.live)
    • 監視開始用クロージャを上書きする
    • delegateを作成
    • delegateが実行するメソッドからActionを呼び出すようにする
enum InfomationListCore {
    struct Observer {
        enum Action {
            case fetched([InfomationEntity])
        }

        var create: (AnyHashable, NSManagedObjectContext) -> Effect<Action, Never> = { _, _ in
            .none
        }

        static let live: Self = {
            var observer = Self()

            observer.create = { id, viewContext in
                Effect.run { subscriber in
                    let frc = createFetchedResultsController(viewContext)
                    let delegate = FetchedResultsDelegate(subscriber)

                    dependencies[id] = Dependencies(
                        fetchedResultsController: frc,
                        fetchedResultsDelegate: delegate                        
                    )
                    return AnyCancellable {}
                }
            }
            return observer
        }
    }
}

delegateを実装するclass

  • NSFetchedResultsControllerDelegateを実装するclass
    • delegateメソッドからSubscriberのsendを呼び出す
extension InfomationListCore {

    class FetchedResultsDelegate: NSObject, NSFetchedResultsControllerDelegate {

        private let subscriber: Effect<Observer.Action, Never>.Subscriber

        init(_ subscriber: Effect<Observer.Action, Never>.Subscriber) {
            self.subscriber = subscriber
        }

        // delegateのメソッド
        func controllerDidChangeContent(
            _ controller: NSFetchedResultsController<NSFetchRequestResult>
        ) {
            let contents = controller.fetchedObjects as! [InfomationEntity]
            subscriber.send(.fetched(contents))
        }
    }
}

iOS13からCore Dataのdelegateはsnapshotを取り出せるのもある。snapshotを取り出してCollectionView側で差分をチェックしてくれて反映さそうなのでそっちでもいい。


まとめ

  • 前提
    • ActionはViewStore以外でも動作する
      • ActionはActionを呼べる
        • 同列のActionだけでなく、親から子、子から親でも可能
    • TCAのEffectはCombine.Publisherに準拠している
      • Combine.Publisher/Subscriberはイベントを完了するまで何度でも呼び出せる
  • TCAからdelegateで自動で何かを監視して処理が実行される仕組みを使う
    • その仕組の中でEffectからSubscriberが取り出せるのでイベントを呼び出せる

一つのActionをきっかけにdelegateを作成して監視させれば、そこからActionを何度も呼び出すことができる。

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
2