はじめに
この発表資料は、
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を呼び出せる
- アプリ起動でView表示
- SwiftUIなら
onAppear { viewStore.send( Action.A ) }
- Reducerの処理でDelegateを作成して実行しておく
- 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として位置情報を伝えてくれる
実際のコード
プロパティとしてクロージャを用意
上書き可能なロジックを用意しておく
- 上書きされる前に実行すると未実装としてクラッシュ
- これはテストコードでも任意の実装に置き換えも可能
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で終了時/キャンセル時にクリーンアップ
- iOSのCLLocationManagerでロケーションを探せるように
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だけでなく、親から子、子から親でも可能
- ActionはActionを呼べる
- TCAのEffectはCombine.Publisherに準拠している
- Combine.Publisher/Subscriberはイベントを完了するまで何度でも呼び出せる
- ActionはViewStore以外でも動作する
- TCAからdelegateで自動で何かを監視して処理が実行される仕組みを使う
- その仕組の中でEffectからSubscriberが取り出せるのでイベントを呼び出せる
一つのActionをきっかけにdelegateを作成して監視させれば、そこからActionを何度も呼び出すことができる。