TCAにおけるTabViewのタップ挙動を検討する

## はじめに

iOSアプリにおいてタブのUIを実装するためのAPIとして、UIKitでは[UITabBarController](https://developer.apple.com/documentation/uikit/uitabbarcontroller)、SwiftUIでは[TabView](https://developer.apple.com/documentation/swiftui/tabview)がそれぞれ提供されています。

タブUIにおいて、Apple標準アプリをはじめ多くのアプリではアクティブなタブをタップした際に次のような挙動がよく見られます。

- 画面遷移している場合はルートまで画面スタックを戻す
- 画面がスクロールされている場合は一番上までスクロールを戻す

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819/367ead2a-87eb-8ef5-63c2-1dd56f02bd03.gif)


UIKitのUITabBarControllerを利用すると一部の挙動は勝手に対応してくれるのですが、SwifUIのTabViewでは何もしてくれないため、現状では開発者が頑張る必要あります。

また一方、SwiftUIアプリのアーキテクチャとして[pointfreeco/swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture)によるTCA(The Composable Architecture)に人気が集まっているようです。個人的には最近触り始めた段階になります。

そこで今回は、TCAを採用したアプリで前述のTabViewでアクティブタプをタップした際の挙動の実装方法を検討しました。

## 実装

こちらにデモアプリを用意しました。

https://github.com/gibachan/TCATabNavigationDemo

前提としている環境は次の通りです。

- Xcode 15.3
- iOS 17
- SwiftUI x TCAアーキテクチャ
    - [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture) 1.9.2

アプリではTabViewで２つのタブ（TabAとTabB）を実装しています。
どちらも前述の挙動を実現していますが、次のように若干異なる実装方法としています。

- TabA: 単純に一覧画面から詳細画面へドリルダウンして行く画面遷移を想定し、[NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink)を利用
- TabB: 何かしらのプレゼンテーションロジックを伴う画面遷移を想定して、Reducerを経由して画面遷移

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819/acc866b2-337d-8fab-f785-f658a446746f.gif)

## ポイント

- 画面遷移をNavigationStackで管理する
    - [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack)はiOS16から利用可能であり、画面遷移をある程度自由に制御できます
    - 主に`pointfreeco/swift-composable-architecture` のCaseStudiesにある[サンプルコード](https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/CaseStudies/SwiftUICaseStudies/03-NavigationStack.swift)を参考にしました
- TabViewにてアクティブなタブをタップされたイベントを検知するため、Bindingをインターセプトする
    - コードは[この辺り](https://github.com/gibachan/TCATabNavigationDemo/blob/f5812f954e521ee618886a4b9b68d8c0ad4d7128/TCATabNavigationDemo/MainTab/MainTabView.swift#L8-L23)
    - [[SwiftUI] タブがタップされた時に上までスクロールする。あるいはそれを共通化する話。](https://qiita.com/YusukeHosonuma/items/dc986c408e14106caf29)の記事が大変参考になりました 
- タブタップのイベントをNotificationCenterから通知し、それを`AsynStream<Void>`へ変換して監視する
    - `AsynStream<Void>`へ変換するコードは[この辺り](https://github.com/gibachan/TCATabNavigationDemo/blob/f5812f954e521ee618886a4b9b68d8c0ad4d7128/TCATabNavigationDemo/Extensions/DependencyValues%2BExtension.swift#L12-L15)
    - タブの`onAppear`で監視を開始し、`onDisappear`で停止する
        - Viewで監視するコードは[この辺り](https://github.com/gibachan/TCATabNavigationDemo/blob/f5812f954e521ee618886a4b9b68d8c0ad4d7128/TCATabNavigationDemo/MainTab/TabAView.swift#L39-L50)
- タブを切り替えても状態を保持するため、Storeを@Stateで保持する
    - コードは[この辺り](https://github.com/gibachan/TCATabNavigationDemo/blob/f5812f954e521ee618886a4b9b68d8c0ad4d7128/TCATabNavigationDemo/MainTab/MainTabView.swift#L27-L29) 
    - `pointfreeco/swift-composable-architecture` の[サンプルコード](https://github.com/pointfreeco/swift-composable-architecture/blob/3c1b72ff7cb11a51c2bda9272889e3b24df767f8/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift#L175-L177)を参考にしました

## まとめ

とりあえずは目的の挙動を実現できているとは思いますが、割と複雑な実装になってしまってる感があり不安もあります。
他の方法など何かありましたらご意見いただけると嬉しいです。