Upgrade to Pro — share decks privately, control downloads, hide ads and more …

pixivのリアーキテクチャにおける
The Composable Architecter活用

tatsubee
October 24, 2024
91

pixivのリアーキテクチャにおける
The Composable Architecter活用

現在、pixivのiOSアプリでは、The Composable Architecture (TCA) を用いたリアーキテクチャを進めています。TCAはSwiftUIとの組み合わせで使用されることが多い印象がありますが、UIKitでも問題なく活用できる上、最近ではUIKitに対するサポートも強化されています。

本発表では、UIKitを主に使用しているpixivアプリが、TCAを導入したことによって得られた恩恵や、UIKit×TCA実装の知見を紹介します!

tatsubee

October 24, 2024
Tweet

Transcript

  1. ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ࢖͍ํ final class FeatureViewController: UIViewController { let store: StoreOf<Feature>

    let label = UILabel() … override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } self.label.text = self.store.text } } }
  2. ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • observeͷ෼ׂ final class FeatureViewController: UIViewController { let

    titleLabel = UILabel() let counterLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } self.titleLabel1.text = self.store.text1 self.counterLabel2.text = “\(self.store.count)” } } }
  3. ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • observeͷ෼ׂ final class FeatureViewController: UIViewController { let

    titleLabel = UILabel() let counterLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } self.titleLabel1.text = self.store.text1 } observe { [weak self] in guard let self else { return } self.counterLabel2.text = “\(self.store.count)” } } }
  4. ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • Task final class FeatureViewController: UIViewController { let

    label = UILabel() override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } Task { try? await Task.sleep() self.label.text = self.store.text } } } }
  5. ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • Task final class FeatureViewController: UIViewController { let

    label = UILabel() override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } Task { try? await Task.sleep() self.label.text = self.store.text } } } } มߋΛ௥੻Ͱ͖ͳ͍
  6. ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • Task final class FeatureViewController: UIViewController { let

    label = UILabel() override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } let text = self.store.text Task { try? await Task.sleep() self.label.text = text } } } }
  7. υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ @Reducer struct ListFeature: Reducer { @ObservableState struct State:

    Hashable { … } enum Action { case onAppeared case rowTapped case rowLongTapped } var body: some ReducerOf<Self> { … } }
  8. υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ @Reducer struct ListFeature: Reducer { @ObservableState struct State:

    Hashable { … } enum Action { case onAppeared } var body: some ReducerOf<Self> { … } } @Reducer struct RowFeature: Reducer { @ObservableState struct State: Hashable, Identifiable { … } enum Action { case rowTapped case rowLongTapped } var body: some ReducerOf<Self> { … } }
  9. υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ @Reducer struct ListFeature: Reducer { @ObservableState struct State:

    Hashable { … } enum Action { case onAppeared } var body: some ReducerOf<Self> { … } }
  10. υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ @Reducer struct ListFeature: Reducer { @ObservableState struct State:

    Hashable { … var rows: IdentifiedArrayOf<RowFeature.State> = [] } enum Action { case onAppeared case rows(IdentifiedActionOf<RowFeature>) } var body: some ReducerOf<Self> { Reduce { state, action in … } } }
  11. υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ @Reducer struct ListFeature: Reducer { @ObservableState struct State:

    Hashable { … var rows: IdentifiedArrayOf<RowFeature.State> = [] } enum Action { case onAppeared case rows(IdentifiedActionOf<RowFeature>) } var body: some ReducerOf<Self> { Reduce { state, action in … } .forEach(\.rows, action: \.rows) { RowFeature() } } }
  12. υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ struct ListView: View { let store: StoreOf<ListFeature> =

    .init( initialState: ListFeature.State() ) { ListFeature() } var body: some View { ForEach(store.scope(state: \.rows, action: \.rows)) { RowView(store: $0) } } }
  13. υϝΠϯΛࡉ෼Խ͢Δ UIKitͰ࢖͍ಘΔύλʔϯ • UITableView final class ListViewController: UITableViewController { let

    store: StoreOf<ListFeature> = .init(initialState: ListFeature.State()) { ListFeature() } override func viewDidLoad() { … } // MARK: UITableViewDataSource override func numberOfSections(in tableView: UITableView) override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell }
  14. υϝΠϯΛࡉ෼Խ͢Δ UITableView final class ListViewController: UITableViewController { let store: StoreOf<ListFeature>

    = .init(initialState: ListFeature.State()) { ListFeature() } var observations: [IndexPath: ObserveToken] = [:] override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { observations[indexPath]?.cancel() observations[indexPath] = observe { [weak self] in guard let self else { return } cell.textLabel?.text = "\(store.rows[indexPath.row].id)" } return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let id = store.rows[indexPath.row].id if let store = store.scope(state: \.rows[id: id], action: \.rows[id: id]) { navigationController?.pushViewController(RowDetailViewController(store: store), animated: true) } } }
  15. uikit-navigation ࢖͍ํ: Tree-based final class FeatureViewController: UIViewController { let store:

    StoreOf<Feature> override func viewDidLoad() { super.viewDidLoad() observe { … } } }
  16. uikit-navigation ࢖͍ํ: Tree-based final class FeatureViewController: UIViewController { @UIBindable var

    store: StoreOf<Feature> override func viewDidLoad() { super.viewDidLoad() observe { … } } }
  17. uikit-navigation ࢖͍ํ: Tree-based final class FeatureViewController: UIViewController { @UIBindable var

    store: StoreOf<Feature> override func viewDidLoad() { super.viewDidLoad() observe { … } present(item: $store.scope( state: \.child, action: \.child )) { store in ChildViewController(store: store) } } }
  18. uikit-navigation ࢖͍ํ: Stack-based class AppController: NavigationStackController { private var store:

    StoreOf<AppFeature>! convenience init(store: StoreOf<AppFeature>) { @UIBindable var store = store self.init(path: $store.scope(state: \.path, action: \.path)) { ListViewController() } destination: { store in switch store.case { case let .detail(store): DetailViewController(store: store) case let .editItem(store): EditViewController(store: store) } } } }
  19. TCA × UIKit × pixiv • ⭕ • TCAͷಋೖ •

    ViewControllerͰঢ়ଶΛ؂ࢹ • swift-navigation(uikit-navigation)Λ׆༻ͨ͠state-drivenͳը໘ભҠ • ˚ • υϝΠϯͷࡉ෼Խ • swift-navigationͱTCAͷ౷߹
  20. TCA × UIKit × pixiv • ࡉ෼Խɾswift-navigationͱTCAͷ౷߹͕·ͩͰ͖͍ͯͳ͍ཧ༝ • ࢼߦࡨޡதʂ •

    ը໘ભҠઌʹReducerΛಋೖͰ͖͍ͯͳ͍ • Tree-based͸ಛʹ໰୊ͳͦ͞͏ • swift-navigationͷΈͰstate-drivenͳը໘ભҠ͕࣮ݱͰ͖ΔͨΊ • Stack-basedΛ΍Γ͍ͨ৔߹ͩͱͪΐͬͱ೉͍͔͠΋ʁ • StackΛ؅ཧ͢Δ࢓૊ΈΛࣗ෼Ͱ࡞Βͳ͍ͱ͍͚ͳ͍ʁ