Slide 1

Slide 1 text

@tatsubee pixivͷϦΞʔΩςΫνϟʹ͓͚Δ The Composable Architecter׆༻

Slide 2

Slide 2 text

tatsubee pixivࣄۀຊ෦ΞϓϦ෦ॴଐ • 23৽ଔ iOSΤϯδχΞ • ෱Ԭੜ·Ε෱Ԭҭͪ౦ژࡏॅ • ڵຯ • iOS • visionOS • Πϥετ • Flutter

Slide 3

Slide 3 text

pixivΞϓϦʹ͍ͭͯ • ΠϥετɾϚϯΨɾখઆͷ౤ߘ΍Ӿཡָ͕͠ΊΔ • ొ࿥Ϣʔβʔ਺1ԯਓҎ্ • iOS / Androidͱ΋ʹ഑৴தʂ

Slide 4

Slide 4 text

pixivΞϓϦʹ͍ͭͯ • 2009/12/09 ver. 1.0 ϦϦʔε🎉 • 2024/10/24 ver. 7.20.22 ഑৴தʂ

Slide 5

Slide 5 text

pixivΞϓϦʹ͍ͭͯ • 2009/12/09 ver. 1.0 ϦϦʔε🎉 • 2024/10/24 ver. 7.20.22 ഑৴தʂ

Slide 6

Slide 6 text

pixivΞϓϦʹ͍ͭͯ • UIKitϕʔε • ΞʔΩςΫνϟ: MVCʁ • 2000ϑΝΠϧҎ্

Slide 7

Slide 7 text

pixivΞϓϦʹ͍ͭͯ • UIKitϕʔε • ΞʔΩςΫνϟ: MVCʁ • 2000ϑΝΠϧҎ্ ϦΞʔΩςΫνϟਐߦதʂ

Slide 8

Slide 8 text

TCAͷಋೖ࢝Ί·ͨ͠

Slide 9

Slide 9 text

Index • ϦΞʔΩςΫνϟઌͱͯ͠ͷTCAͷબఆཧ༝ • TCA x UIKitͷجૅతͳ࢖͍ํ • TCA x UIKit x pixiv

Slide 10

Slide 10 text

TCAͷબఆཧ༝

Slide 11

Slide 11 text

The Composable Architecture(TCA)ͱ͸ʁ ReduxΛجௐͱͨ͠ঢ়ଶ؅ཧϑϨʔϜϫʔΫͰɺಛʹ࣍ͷ5ͭʹॏ఺Λ͓͍ ͯ։ൃ͞Ε͍ͯΔ • State Management • Composition • Side Effects • Testing • Ergonomics

Slide 12

Slide 12 text

TCAͷબఆཧ༝ • pixivͷঢ়گ • ϐΫγϒࣾ಺ͷঢ়گ

Slide 13

Slide 13 text

TCAͷબఆཧ༝ pixivͷঢ়گ • มߋʹऑ͍ • ݱࡏͷઃܭʹݶք͕͍͖ۙͮͯͨ

Slide 14

Slide 14 text

TCAͷબఆཧ༝ pixivͷঢ়گ • มߋʹऑ͍ • UIͱϩδοΫͷ੾Γ෼͚͕͋·Γͳ͞Ε͍ͯͳ͍ͨΊɺӨڹൣғ͕޿ ͕Γ΍͍͢ • Test΋ଟ͘͸ͳ͍ && ॻ͖΍͘͢ͳ͍

Slide 15

Slide 15 text

TCAͷબఆཧ༝ pixivͷঢ়گ • ݱࡏͷઃܭʹݶք͕͍͖ۙͮͯͨ • ϩδοΫ෦෼ʹ͔ͬͪΓͱͨ͠ઃܭ͸ଘࡏ͢Δ • ͕ɺ͜Ε͔Β΍Γ͍ͨ͜ͱʹ߹Θͳ͘ͳ͖ͬͯͨ • Ұൠతͳઃܭํ๏ͷมભ • ৽ػೳͷ࣮૷

Slide 16

Slide 16 text

TCAͷબఆཧ༝ ϐΫγϒࣾ಺ͷঢ়گ • PastelaνʔϜ͕ઌߦͯ͠TCAΛಋೖ͍ͯͨ͠ • tatsubee͕OJTͱͯ͠Pastelaͷ։ൃʹҰ࣌ظؔΘ͍ͬͯͨ • ݱࡏͷpixivΞϓϦνʔϜ಺Ͱ΋TCAΛ࢖ͬͨ͜ͱ͋Δਓ͕૿͑ͨ → ࣾ಺Ͱͷԣͷ࿈ܞ͕΍Γ΍ͦ͢͏ʂ

Slide 17

Slide 17 text

TCAͷબఆཧ༝ • pixivΞϓϦͷ΢ΟʔΫϙΠϯτΛิڧͰ͖Δ • ࣾ಺Ͱԣͷ࿈ܞ͕Մೳ • (tatsubeeͷ޷Έ)

Slide 18

Slide 18 text

TCA × UIKit

Slide 19

Slide 19 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation

Slide 20

Slide 20 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation

Slide 21

Slide 21 text

ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:) • ObservationϑϨʔϜϫʔΫͷ࢓૊ΈΛ׆༻͠ɺobserve಺ͰΞΫηε͞ ΕΔϑΟʔϧυͷมߋΛࣗಈͰ௥੻͢Δ • PerceptionϥΠϒϥϦʹΑͬͯObservation͸όοΫϙʔτ͞Ε͍ͯΔ ͨΊɺiOS 13+Ͱ͋Ε͹࢖༻Մೳ

Slide 22

Slide 22 text

ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ࢖͍ํ final class FeatureViewController: UIViewController { let store: StoreOf let label = UILabel() … override func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } self.label.text = self.store.text } } }

Slide 23

Slide 23 text

ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • observeͷ෼ׂ • Task

Slide 24

Slide 24 text

ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • observeͷ෼ׂ • Task

Slide 25

Slide 25 text

ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • observeͷ෼ׂ • observe಺ʹهड़͞ΕͨϑΟʔϧυͷ஋ʹมߋ͕͋ͬͨ࣌ɺobserve಺ ͷશͯͷॲཧ͕૸ͬͯ͠·͏

Slide 26

Slide 26 text

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)” } } }

Slide 27

Slide 27 text

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)” } } }

Slide 28

Slide 28 text

ViewControllerͰঢ়ଶΛ؂ࢹ͢Δ observe(_:)ͷ஫ҙ఺ • observeͷ෼ׂ • Task

Slide 29

Slide 29 text

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 } } } }

Slide 30

Slide 30 text

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 } } } } มߋΛ௥੻Ͱ͖ͳ͍

Slide 31

Slide 31 text

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 } } } }

Slide 32

Slide 32 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation

Slide 33

Slide 33 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation ͜ͷ࣌఺ͰReducerΛ࡞੒͠ɺ ঢ়ଶϕʔεͰUIΛૢ࡞Ͱ͖ΔΑ͏ʹͳͬͨ

Slide 34

Slide 34 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation }ͲΕ͚ͩTCAͷڧΈΛ࢖͏͔(࢖͑Δͷ͔ʁ)

Slide 35

Slide 35 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation

Slide 36

Slide 36 text

υϝΠϯΛࡉ෼Խ͢Δ The Composable ArchitectureͷʮComposableʯͷ෦෼ͷ࿩ • େ͖͍Reducer͔Βখ͍͞ػೳΛࢠReducerͱͯ͠ʹ෼ׂ͠ɺ ͦΕΒΛCompose͢Δ͜ͱ͕Ͱ͖Δ

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

υϝΠϯΛࡉ෼Խ͢Δ SwiftUIͷྫ struct ListView: View { let store: StoreOf = .init( initialState: ListFeature.State() ) { ListFeature() } var body: some View { ForEach(store.scope(state: \.rows, action: \.rows)) { RowView(store: $0) } } }

Slide 43

Slide 43 text

υϝΠϯΛࡉ෼Խ͢Δ ͜ΕΛUIKitͰ΋࢖͑Δͷ͔ʁ

Slide 44

Slide 44 text

υϝΠϯΛࡉ෼Խ͢Δ UIKitͰ࢖͍ಘΔύλʔϯ • UITableView • UICollectionView

Slide 45

Slide 45 text

υϝΠϯΛࡉ෼Խ͢Δ UIKitͰ࢖͍ಘΔύλʔϯ • UITableView final class ListViewController: UITableViewController { let store: StoreOf = .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 }

Slide 46

Slide 46 text

υϝΠϯΛࡉ෼Խ͢Δ UITableView final class ListViewController: UITableViewController { let store: StoreOf = .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) } } }

Slide 47

Slide 47 text

υϝΠϯΛࡉ෼Խ͢Δ • UIKitͰ΋υϝΠϯͷࡉ෼Խ͸Մೳʂ • SwiftUIͱൺֱ͢Δͱهड़ྔ͕૿͑ɺෳࡶʹͳΔ͔΋ • Reducerͷن໛ʹ΋ΑΔ͕ɺ ݸਓతʹ͸ࡉ෼Խͤͣʹ ViewController:Reducer = 1:1 Ͱ΋े෼ • ͱ͸͍͑TCAͷڧΈ͸͔ͬ͠Γ࢖͍͍ͨͷͰ΋͏ͪΐ͍୳ͬͯΈΔ • ΞυόΠε͋Ε͹͓ئ͍͠·͢ʂ

Slide 48

Slide 48 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation

Slide 49

Slide 49 text

uikit-navigation Point-Freeͷఏএ͢Δɺstate-drivenͳը໘ભҠͷ2ͭΧςΰϦ • Tree-based navigation • Stack-based navigation

Slide 50

Slide 50 text

uikit-navigation Point-Freeͷఏএ͢Δɺstate-drivenͳը໘ભҠͷ2ͭΧςΰϦ • Tree-based navigation • present(item:content:) • present(isPresented:content:) • navigationDestination(item:content:) • Stack-based navigation • NavigationStackController

Slide 51

Slide 51 text

uikit-navigation ࢖͍ํ: Tree-based final class FeatureViewController: UIViewController { let store: StoreOf override func viewDidLoad() { super.viewDidLoad() observe { … } } }

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

uikit-navigation ࢖͍ํ: Tree-based final class FeatureViewController: UIViewController { @UIBindable var store: StoreOf override func viewDidLoad() { super.viewDidLoad() observe { … } present(item: $store.scope( state: \.child, action: \.child )) { store in ChildViewController(store: store) } } }

Slide 54

Slide 54 text

uikit-navigation ࢖͍ํ: Stack-based class AppController: NavigationStackController { private var store: StoreOf! convenience init(store: StoreOf) { @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) } } } }

Slide 55

Slide 55 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation

Slide 56

Slide 56 text

TCA × UIKit • ViewControllerͰঢ়ଶΛ؂ࢹ • υϝΠϯͷࡉ෼Խ • uikit-navigation pixivͰ͸·ͩ࢖͍͖Ε͍ͯ·ͤΜ😢 }

Slide 57

Slide 57 text

TCA × UIKit × pixiv • ⭕ • TCAͷಋೖ • ViewControllerͰঢ়ଶΛ؂ࢹ • swift-navigation(uikit-navigation)Λ׆༻ͨ͠state-drivenͳը໘ભҠ • ˚ • υϝΠϯͷࡉ෼Խ • swift-navigationͱTCAͷ౷߹

Slide 58

Slide 58 text

TCA × UIKit × pixiv • ࡉ෼Խɾswift-navigationͱTCAͷ౷߹͕·ͩͰ͖͍ͯͳ͍ཧ༝ • ࢼߦࡨޡதʂ • ը໘ભҠઌʹReducerΛಋೖͰ͖͍ͯͳ͍ • Tree-based͸ಛʹ໰୊ͳͦ͞͏ • swift-navigationͷΈͰstate-drivenͳը໘ભҠ͕࣮ݱͰ͖ΔͨΊ • Stack-basedΛ΍Γ͍ͨ৔߹ͩͱͪΐͬͱ೉͍͔͠΋ʁ • StackΛ؅ཧ͢Δ࢓૊ΈΛࣗ෼Ͱ࡞Βͳ͍ͱ͍͚ͳ͍ʁ

Slide 59

Slide 59 text

ײ૝

Slide 60

Slide 60 text

ײ૝ • ԣ࣠Ͱ஌ݟͷڞ༗Ͱ͖Δͷ͸ͱͯ΋خ͍͠ʂ • TCAͷڧΈΛશͯ࢖͍͜ͳ͍ͤͯΔΘ͚Ͱ͸ͳ͍͕ɺ͔ͳΓ։ൃମݧ͕ ྑ͘ͳͬͨ • ࢓༷͕Ӆṭ͞Εʹ͘͘ͳͬͨ • ঢ়ଶ == UIͰ͋ΔͨΊɺTest͕͔ͳΓॻ͖΍͘͢ͳͬͨ • (ͨͿΜ) ReducerͷܗΛյͣ͞ʹSwiftUIʹ΋ҠߦͰ͖Δ͸ͣ

Slide 61

Slide 61 text

͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ