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

# はじめに

この発表資料は、
[iOSアプリ開発のためのFunctional Architecture情報共有会3](https://connpass.com/event/201194/)の発表資料です。

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

---

# 導入

iOSDC2020 [iOSアプリ開発のための"The Composable Architecture"がすごく良いので紹介したい](https://fortee.jp/iosdc-japan-2020/proposal/40b59a33-1f0a-45ba-ba94-10ce9cf960f4)で出た質問から

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

---

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

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

---

1. アプリ起動でView表示
1. SwiftUIなら`onAppear { viewStore.send( Action.A ) }`
1. Reducerの処理でDelegateを作成して実行しておく
1. 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/4eb14b7e-b709-eec7-892a-38adbb7271bd.png)

---

**実際のコード**

---

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

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

- 上書きされる前に実行すると未実装としてクラッシュ
    - これはテストコードでも任意の実装に置き換えも可能

```swift
public struct LocationManager {
  ...
  var create: (AnyHashable) -> Effect<Action, Never> = { _ in 
      // ActionはLocationManager.Actionを省略できている
      _unimplemented("create") 
  }
  ...
}
```

---

### Managerをstaticに作成

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

```swift
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で終了時/キャンセル時にクリーンアップ

```swift
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で特定のメソッドが実行される

```swift
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を呼び出す

```swift
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を繋げる

```swift
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を呼び出すようにする

```swift
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を呼び出す


```swift
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を何度も呼び出すことができる。