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

iOSアプリ開発のためのThe Composable Architectureがすごく良いので紹介したい

yimajo
September 20, 2020

iOSアプリ開発のためのThe Composable Architectureがすごく良いので紹介したい

yimajo

September 20, 2020
Tweet

More Decks by yimajo

Other Decks in Programming

Transcript

  1. iOSΞϓϦ։ൃͷͨΊͷ
    The Composable Architecture ͕
    ͘͢͝ྑ͍ͷͰ঺հ͍ͨ͠
    iOSDC Japan 2020 09/21 11:30 #iosdc #d
    ࠓ৓ળۣ / @yimajo

    View Slide

  2. iOSDC2020.9.21 11:30ͷൃදʹ
    ೖΕΒΕ͍ͯͳ͔ͬͨ಺༰ʹ͍ͭͯ
    ิ଍ͷͨΊʹ
    ௥هͨ͠ࢿྉʹͳ͍ͬͯ·͢
    ௥ه

    View Slide

  3. ࣗݾ঺հ͸͕࣌ؒ༨ͬͨΒޙ൒ʹ

    View Slide

  4. About The Composable Architecture

    View Slide

  5. About The Composable Architecture
    • A library for building applications
    • SwiftUI, UIKit
    • iOS, MacOS, tvOS, and WatchOS

    View Slide

  6. About The Composable Architecture
    • A library for building applications
    • SwiftUI, UIKit
    • iOS, MacOS, tvOS, and WatchOS
    • Point-Free hosted by Mr. Brandon and Mr. Stephen.
    • https://github.com/pointfreeco/swift-composable-architecture

    View Slide

  7. Point-Free
    ௥ه
    https://www.pointfree.co/

    View Slide

  8. What is
    The Composable
    Architecture

    View Slide

  9. State management

    View Slide

  10. Composition

    View Slide

  11. Side Effect

    View Slide

  12. Testable

    View Slide

  13. Ergonomics

    View Slide

  14. Basic Usage

    View Slide

  15. Main Parts
    • Action
    • Reducer
    • Effect
    • State
    • Store
    • Environment

    View Slide

  16. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  17. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  18. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  19. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  20. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  21. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  22. State
    Action
    Reducer
    Effect
    View
    Store

    View Slide

  23. State
    Action
    Reducer
    Effect
    View
    Store
    DI
    Environment

    View Slide

  24. αϯϓϧղઆ

    View Slide

  25. View Slide

  26. TCA

    View Slide

  27. Samples
    TCA

    View Slide

  28. Samples
    Swift Package Dependencies
    TCA

    View Slide

  29. ΍ΕΔ͜ͱ
    • - ͱ + ΛλοϓͰ͖Δ
    • ਺ࣈͷද͕ࣔ - ͱ + ʹΑΓมߋ͞ΕΔ
    ಛ௃
    • Effect ͕ͳ͍ɻEnvironment͸Χϥ
    CaseStudies/CounterDemo

    View Slide

  30. CaseStudies/CounterDemo
    State
    Action
    Reducer
    View
    Store

    View Slide

  31. CaseStudies/CounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum CounterAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    }

    View Slide

  32. CaseStudies/CounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum CounterAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    }
    // State
    struct CounterState: Equatable {
    var count = 0
    }

    View Slide

  33. CaseStudies/CounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum CounterAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    }
    // Reducer
    let counterReducer = Reducer { state, action, _ in
    switch action {
    case .decrementButtonTapped:
    state.count -= 1
    return .none
    case .incrementButtonTapped:
    state.count += 1
    return .none
    }
    }
    // State
    struct CounterState: Equatable {
    var count = 0
    }

    View Slide

  34. CaseStudies/CounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum CounterAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    }
    // Reducer
    let counterReducer = Reducer { state, action, _ in
    switch action {
    case .decrementButtonTapped:
    state.count -= 1
    return .none
    case .incrementButtonTapped:
    state.count += 1
    return .none
    }
    }
    // State
    struct CounterState: Equatable {
    var count = 0
    }
    struct CounterView: View {
    let store: Store
    var body: some View {
    WithViewStore(self.store) { viewStore in
    HStack {
    Button("−") {
    viewStore.send(.decrementButtonTapped)
    }
    Text(“\(viewStore.count)”)
    Button("+") {
    viewStore.send(.incrementButtonTapped)
    }
    }
    }

    View Slide

  35. ΍ΕΔ͜ͱ
    • 2ߦͷ- ͱ + ΛλοϓͰ͖ΔView͕2ͭ
    ಛ௃
    • Effect ͕ͳ͍ɻEnvironment͸Χϥ
    • Χ΢ϯλʔ༻ͷStoreͱViewΛͻͱͭͷ
    ෦඼ͱͯ͠2ߦར༻͍ͯ͠Δ
    CaseStudies/TwoCounterDemo

    View Slide

  36. CaseStudies/TwoCounterDemo
    State
    Action
    Reducer
    View
    Store

    View Slide

  37. CaseStudies/TwoCounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum TwoCountersAction {
    case counter1(CounterAction)
    case counter2(CounterAction)
    }

    View Slide

  38. CaseStudies/TwoCounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum TwoCountersAction {
    case counter1(CounterAction)
    case counter2(CounterAction)
    }
    // State
    struct TwoCountersState: Equatable {
    var counter1 = CounterState()
    var counter2 = CounterState()
    }

    View Slide

  39. CaseStudies/TwoCounterDemo
    State
    Action
    Reducer
    View
    Store
    // Action
    enum TwoCountersAction {
    case counter1(CounterAction)
    case counter2(CounterAction)
    }
    // Reducer
    let twoCountersReducer = Reducer
    .combine(
    counterReducer.pullback(
    state: \TwoCountersState.counter1,
    action: /TwoCountersAction.counter1,
    environment: { _ in CounterEnvironment() }
    ),
    counterReducer.pullback(
    state: \TwoCountersState.counter2,
    action: /TwoCountersAction.counter2,
    environment: { _ in CounterEnvironment() }
    )
    )
    // State
    struct TwoCountersState: Equatable {
    var counter1 = CounterState()
    var counter2 = CounterState()
    }

    View Slide

  40. struct TwoCountersView: View {
    let store: Store
    var body: some View {
    Form {
    Section(header: Text(template: readMe, .caption)) {
    HStack {
    Text("Counter 1")
    CounterView(
    store: self.store.scope(state: { $0.counter1 }, action: TwoCountersAction.counter1)
    )
    .buttonStyle(BorderlessButtonStyle())
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
    }
    HStack {
    Text("Counter 2")
    CounterView(
    store: self.store.scope(state: { $0.counter2 }, action: TwoCountersAction.counter2)
    )
    .buttonStyle(BorderlessButtonStyle())
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
    }
    }
    }
    .navigationBarTitle("Two counter demo")
    }
    }

    View Slide

  41. ΍ΕΔ͜ͱ
    • - ͱ + ΛλοϓͰ͖Δ
    • - λοϓ͢Δͱ1ඵޙʹ+͞ΕΔ
    • Number factϘλϯΛλοϓͰWeb APIݺͼग़͠
    ಛ௃
    • Effect ͕2ͭ͋Γɺ- Ϙλϯλοϓͱ NumberFact
    ϘλϯʹΑΓಈ࡞͢Δɻ
    CaseStudies/EffectBasics

    View Slide

  42. CaseStudies/EffectBasics
    State
    Action
    Reducer
    View
    Store
    Environment
    Effect

    View Slide

  43. CaseStudies/EffectBasics
    State
    Action
    Reducer
    View
    Store
    Environment
    // Action
    enum EffectsBasicsAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse (
    Result
    )
    }
    Effect

    View Slide

  44. CaseStudies/EffectBasics
    State
    Action
    Reducer
    View
    Store
    // State
    struct EffectsBasicsState: Equatable {
    var count = 0
    // ௨৴தΛࣔ͢ϑϥά
    var isNumberFactRequestInFlight = false
    // WebAPIͷϨεϙϯε
    var numberFact: String?
    }
    Environment
    // Action
    enum EffectsBasicsAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse (
    Result
    )
    }
    Effect

    View Slide

  45. CaseStudies/EffectBasics
    State
    Action
    Reducer
    View
    Store
    // State
    struct EffectsBasicsState: Equatable {
    var count = 0
    // ௨৴தΛࣔ͢ϑϥά
    var isNumberFactRequestInFlight = false
    // WebAPIͷϨεϙϯε
    var numberFact: String?
    }
    Environment
    // Action
    enum EffectsBasicsAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse (
    Result
    )
    }
    Effect
    // Reducer
    let effectsBasicsReducer = Reducer { state, action, environment in
    switch action {
    case .decrementButtonTapped:
    ɾɾɾলུ
    return Effect(value: EffectsBasicsAction.incrementButtonTapped)
    .delay(for: 1, scheduler: environment.mainQueue)
    .eraseToEffect()
    case .incrementButtonTapped:
    ɾɾɾলུ
    return .none
    case .numberFactButtonTapped:
    state.isNumberFactRequestInFlight = true
    state.numberFact = nil
    return environment.numberFact(state.count)
    .receive(on: environment.mainQueue)
    .catchToEffect()
    .map(EffectsBasicsAction.numberFactResponse)
    case let .numberFactResponse(.success(response)):
    state.isNumberFactRequestInFlight = false
    state.numberFact = response
    return .none
    case .numberFactResponse(.failure):
    ɾɾɾলུ
    return .none
    }
    }

    View Slide

  46. CaseStudies/EffectBasics
    State
    Action
    Reducer
    View
    Store
    // State
    struct EffectsBasicsState: Equatable {
    var count = 0
    // ௨৴தΛࣔ͢ϑϥά
    var isNumberFactRequestInFlight = false
    // WebAPIͷϨεϙϯε
    var numberFact: String?
    }
    Environment
    // Action
    enum EffectsBasicsAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse (
    Result
    )
    }
    Effect
    // Reducer
    let effectsBasicsReducer = Reducer { state, action, environment in
    switch action {
    case .decrementButtonTapped:
    ɾɾɾলུ
    return Effect(value: EffectsBasicsAction.incrementButtonTapped)
    .delay(for: 1, scheduler: environment.mainQueue)
    .eraseToEffect()
    case .incrementButtonTapped:
    ɾɾɾলུ
    return .none
    case .numberFactButtonTapped:
    state.isNumberFactRequestInFlight = true
    state.numberFact = nil
    return environment.numberFact(state.count)
    .receive(on: environment.mainQueue)
    .catchToEffect()
    .map(EffectsBasicsAction.numberFactResponse)
    case let .numberFactResponse(.success(response)):
    state.isNumberFactRequestInFlight = false
    state.numberFact = response
    return .none
    case .numberFactResponse(.failure):
    ɾɾɾলུ
    return .none
    }
    }

    View Slide

  47. CaseStudies/EffectBasics
    State
    Action
    Reducer
    View
    Store
    // State
    struct EffectsBasicsState: Equatable {
    var count = 0
    // ௨৴தΛࣔ͢ϑϥά
    var isNumberFactRequestInFlight = false
    // WebAPIͷϨεϙϯε
    var numberFact: String?
    }
    Environment
    // Action
    enum EffectsBasicsAction: Equatable {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse (
    Result
    )
    }
    Effect
    // Reducer
    let effectsBasicsReducer = Reducer { state, action, environment in
    switch action {
    case .decrementButtonTapped:
    ɾɾɾলུ
    return Effect(value: EffectsBasicsAction.incrementButtonTapped)
    .delay(for: 1, scheduler: environment.mainQueue)
    .eraseToEffect()
    case .incrementButtonTapped:
    ɾɾɾলུ
    return .none
    case .numberFactButtonTapped:
    state.isNumberFactRequestInFlight = true
    state.numberFact = nil
    return environment.numberFact(state.count)
    .receive(on: environment.mainQueue)
    .catchToEffect()
    .map(EffectsBasicsAction.numberFactResponse)
    case let .numberFactResponse(.success(response)):
    state.isNumberFactRequestInFlight = false
    state.numberFact = response
    return .none
    case .numberFactResponse(.failure):
    ɾɾɾলུ
    return .none
    }
    }

    View Slide

  48. ͜͜·ͰΛ੔ཧ

    View Slide

  49. ͜͜·ͰΛ੔ཧ
    • StateΛมߋͰ͖Δͷ͸ReducerͷΈͱ͍͏੍໿Λ՝͢͜ͱ͕Ͱ͖Δ

    View Slide

  50. ͜͜·ͰΛ੔ཧ
    • StateΛมߋͰ͖Δͷ͸ReducerͷΈͱ͍͏੍໿Λ՝͢͜ͱ͕Ͱ͖Δ
    • ViewͱϩδοΫΛͻͱ·ͱ·Γͷ෦඼ͱͯ͠૊Έ߹ΘͤΒΕΔ

    View Slide

  51. ͜͜·ͰΛ੔ཧ
    • StateΛมߋͰ͖Δͷ͸ReducerͷΈͱ͍͏੍໿Λ՝͢͜ͱ͕Ͱ͖Δ
    • ViewͱϩδοΫΛͻͱ·ͱ·Γͷ෦඼ͱͯ͠૊Έ߹ΘͤΒΕΔ
    • େ͖ͳReducerΛ࡞Βͳͯ͘ྑ͍

    View Slide

  52. ͜͜·ͰΛ੔ཧ
    • StateΛมߋͰ͖Δͷ͸ReducerͷΈͱ͍͏੍໿Λ՝͢͜ͱ͕Ͱ͖Δ
    • ViewͱϩδοΫΛͻͱ·ͱ·Γͷ෦඼ͱͯ͠૊Έ߹ΘͤΒΕΔ
    • େ͖ͳReducerΛ࡞Βͳͯ͘ྑ͍
    • ෭࡞༻͸Effectܕʹ͠ɺ࣍ͷΞΫγϣϯΛݺͼग़ͤΔ

    View Slide

  53. αϯϓϧίʔυ
    ͷิ଍

    View Slide

  54. WithViewStore
    &
    ViewStore


    View Slide

  55. public struct WithViewStore: View where Content: View {
    private let content: (ViewStore) -> Content
    private var prefix: String?
    @ObservedObject private var viewStore: ViewStore
    public init(
    _ store: Store,
    removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
    @ViewBuilder content: @escaping (ViewStore) -> Content
    ) {
    self.content = content
    self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
    }
    public var body: some View {
    … লུ
    return self.content(self.viewStore)
    }

    View Slide

  56. public struct WithViewStore: View where Content: View {
    private let content: (ViewStore) -> Content
    private var prefix: String?
    @ObservedObject private var viewStore: ViewStore
    public init(
    _ store: Store,
    removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
    @ViewBuilder content: @escaping (ViewStore) -> Content
    ) {
    self.content = content
    self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
    }
    public var body: some View {
    … লུ
    return self.content(self.viewStore)
    }

    View Slide

  57. public struct WithViewStore: View where Content: View {
    private let content: (ViewStore) -> Content
    private var prefix: String?
    @ObservedObject private var viewStore: ViewStore
    public init(
    _ store: Store,
    removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
    @ViewBuilder content: @escaping (ViewStore) -> Content
    ) {
    self.content = content
    self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
    }
    public var body: some View {
    … লུ
    return self.content(self.viewStore)
    }

    View Slide

  58. @dynamicMemberLookup
    public final class ViewStore: ObservableObject {
    … লུ
    /// The current state.
    public private(set) var state: State {
    willSet {
    self.objectWillChange.send()
    }
    }
    … লུ
    public func send(_ action: Action) {
    self._send(action)
    }

    View Slide

  59. Effect

    View Slide

  60. public struct Effect: Publisher

    View Slide

  61. • Combine.Publisherʹ४ڌ
    public struct Effect: Publisher

    View Slide

  62. • Combine.Publisherʹ४ڌ
    • PublisherͳͷͰTCA಺෦ͰSubscriber؂ࢹͰ͖Δ
    public struct Effect: Publisher

    View Slide

  63. • Combine.Publisherʹ४ڌ
    • PublisherͳͷͰTCA಺෦ͰSubscriber؂ࢹͰ͖Δ
    • ΦϖϨʔλͰϝιουνΣΠϯͰ͖Δ
    public struct Effect: Publisher

    View Slide

  64. Effect Examples
    https://github.com/pointfreeco/swift-composable-architecture/blob/0.7.0/Sources/
    ComposableArchitecture/Effect.swift
    ௥ه

    View Slide

  65. Effect(
    NotificationCenter.default
    .publisher(for: UIApplication.userDidTakeScreenshotNotification)
    )
    NotificationCenter.default
    .publisher(for: UIApplication.userDidTakeScreenshotNotification)
    .eraseToEffect()
    ௥ه

    View Slide

  66. Effect.future { callback in
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    callback(.success(42))
    }
    }
    ௥ه

    View Slide

  67. Effect.result {
    let fileUrl = URL(
    fileURLWithPath: NSSearchPathForDirectoriesInDomains(
    .documentDirectory,
    .userDomainMask,
    true
    )[0]
    )
    .appendingPathComponent("user.json")
    let result = Result {
    let data = try Data(contentsOf: fileUrl)
    return try JSONDecoder().decode(User.self, from: $0)
    }
    return result
    }
    ௥ه

    View Slide

  68. EffectܕΛ؂ࢹͯ࣍͠ͷActionΛݺͼग़͢࢓૊Έ

    View Slide

  69. • Store͸ViewStore͔ΒActionΛݺͼग़͞ΕΔͱɺݱࡏͷActionͷ
    Ωϡʔʹ௥Ճ
    EffectܕΛ؂ࢹͯ࣍͠ͷActionΛݺͼग़͢࢓૊Έ

    View Slide

  70. • Store͸ViewStore͔ΒActionΛݺͼग़͞ΕΔͱɺݱࡏͷActionͷ
    Ωϡʔʹ௥Ճ
    • ΩϡʔΛwhileϧʔϓͯ͠ActionΛॱ࣮࣍ߦ͠
    EffectܕΛ؂ࢹͯ࣍͠ͷActionΛݺͼग़͢࢓૊Έ

    View Slide

  71. • Store͸ViewStore͔ΒActionΛݺͼग़͞ΕΔͱɺݱࡏͷActionͷ
    Ωϡʔʹ௥Ճ
    • ΩϡʔΛwhileϧʔϓͯ͠ActionΛॱ࣮࣍ߦ͠
    • ͦΕʹඥͮ͘EffectΛߪಡॲཧ͍ͯ͘͠
    EffectܕΛ؂ࢹͯ࣍͠ͷActionΛݺͼग़͢࢓૊Έ

    View Slide

  72. • Store͸ViewStore͔ΒActionΛݺͼग़͞ΕΔͱɺݱࡏͷActionͷ
    Ωϡʔʹ௥Ճ
    • ΩϡʔΛwhileϧʔϓͯ͠ActionΛॱ࣮࣍ߦ͠
    • ͦΕʹඥͮ͘EffectΛߪಡॲཧ͍ͯ͘͠
    • EffectΛߪಡ͢Δͱ݁Ռͱͯ࣍͠ͷActionΛݺͼग़͢
    EffectܕΛ؂ࢹͯ࣍͠ͷActionΛݺͼग़͢࢓૊Έ

    View Slide

  73. ςετίʔυ

    View Slide

  74. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler

    View Slide

  75. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler
    SUT

    View Slide

  76. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler
    SUT

    View Slide

  77. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler Test Double
    SUT

    View Slide

  78. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler Test Double
    SUT

    View Slide

  79. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler Test Double
    Test Double
    SUT

    View Slide

  80. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }

    View Slide

  81. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }

    View Slide

  82. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }

    View Slide

  83. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }
    $0.count Λ 1 count ͸ 1
    -> ࣗ෼͕ظ଴͢ΔState
    ࣮ࡍʹมߋ͞ΕͨState
    ͱൺֱ

    View Slide

  84. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }
    $0.count Λ 1 count ͸ 1
    -> ࣗ෼͕ظ଴͢ΔState
    ࣮ࡍʹมߋ͞ΕͨState
    ͱൺֱ

    View Slide

  85. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }
    $0.count Λ 1 count ͸ 1
    -> ࣗ෼͕ظ଴͢ΔState
    ࣮ࡍʹมߋ͞ΕͨState
    ͱൺֱ

    View Slide

  86. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }
    $0.count Λ 1 count ͸ 1
    -> ࣗ෼͕ظ଴͢ΔState
    ࣮ࡍʹมߋ͞ΕͨState
    ͱൺֱ
    ෭࡞༻ͷ݁Ռɺ"DUJPO͕ड͚औΔظ଴஋

    View Slide

  87. func testNumberFact() {
    let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
    mainQueue: self.scheduler.eraseToAnyScheduler(),
    numberFact: { n in Effect(value: "\(n) is a good number") }
    )
    )
    store.assert(
    .send(.incrementButtonTapped) {
    $0.count = 1
    },
    .send(.numberFactButtonTapped) {
    $0.isNumberFactRequestInFlight = true
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.numberFactResponse(.success("1 is a good number"))) {
    $0.isNumberFactRequestInFlight = false
    $0.numberFact = "1 is a good number"
    }
    )
    }
    $0.count Λ 1 count ͸ 1
    -> ࣗ෼͕ظ଴͢ΔState
    ࣮ࡍʹมߋ͞ΕͨState
    ͱൺֱ
    ෭࡞༻ͷ݁Ռɺ"DUJPO͕ड͚औΔظ଴஋
    -> ࣗ෼͕ظ଴͢ΔState

    View Slide

  88. TCAͷςετίʔυʹ͍ͭͯͷ੔ཧ

    View Slide

  89. TCAͷςετίʔυʹ͍ͭͯͷ੔ཧ
    • TCA͸Reducer༻ͷςετํ๏Λఏڙͯ͘͠ΕͯΔ

    View Slide

  90. TCAͷςετίʔυʹ͍ͭͯͷ੔ཧ
    • TCA͸Reducer༻ͷςετํ๏Λఏڙͯ͘͠ΕͯΔ
    • Reducer༻ͷςετͰ͸Effect͸ςετμϒϧͱͯ͠ஔ͖׵͑ɺผ
    ్୯ମςετॻ͍ͨΒ͍͍

    View Slide

  91. TCAͷςετίʔυʹ͍ͭͯͷิ଍
    expectedState actualState
    ൺֱ

    View Slide

  92. TCAʹ͍ͭͯ
    ·ͱΊ

    View Slide

  93. ྑ͍ͱ͜Ζ

    View Slide

  94. ྑ͍ͱ͜Ζ
    • Reducer͔͠StateΛѻ͑ͳ͍ͱ͍͏੍໿͕͋Δ

    View Slide

  95. ྑ͍ͱ͜Ζ
    • Reducer͔͠StateΛѻ͑ͳ͍ͱ͍͏੍໿͕͋Δ
    • ReducerΛ෼ׂͰ͖ΔͨΊɺେ͖ͳReducerΛ࡞Βͳ͍͍ͯ͘

    View Slide

  96. ྑ͍ͱ͜Ζ
    • Reducer͔͠StateΛѻ͑ͳ͍ͱ͍͏੍໿͕͋Δ
    • ReducerΛ෼ׂͰ͖ΔͨΊɺେ͖ͳReducerΛ࡞Βͳ͍͍ͯ͘
    • SwiftUIͰ΋࢖͍΍͍͢

    View Slide

  97. ྑ͍ͱ͜Ζ
    • Reducer͔͠StateΛѻ͑ͳ͍ͱ͍͏੍໿͕͋Δ
    • ReducerΛ෼ׂͰ͖ΔͨΊɺେ͖ͳReducerΛ࡞Βͳ͍͍ͯ͘
    • SwiftUIͰ΋࢖͍΍͍͢
    • TCAΛѻ͏ࡍʹCombineΛ͋Μ·Γཧղͯ͠ͳͯ͘΋࣮ߦͱ؂ࢹͷԸ
    ܙ͕͋Δ

    View Slide

  98. ྑ͍ͱ͜Ζ
    • Reducer͔͠StateΛѻ͑ͳ͍ͱ͍͏੍໿͕͋Δ
    • ReducerΛ෼ׂͰ͖ΔͨΊɺେ͖ͳReducerΛ࡞Βͳ͍͍ͯ͘
    • SwiftUIͰ΋࢖͍΍͍͢
    • TCAΛѻ͏ࡍʹCombineΛ͋Μ·Γཧղͯ͠ͳͯ͘΋࣮ߦͱ؂ࢹͷԸ
    ܙ͕͋Δ
    • ςετεέδϡʔϥͱςετํ๏Λఏڙͯ͘͠Ε͍ͯΔ

    View Slide

  99. ೉͍͠ͱ͜Ζ

    View Slide

  100. ೉͍͠ͱ͜Ζ
    • ͋Δఔ౓CombineΛཧղ͠ͳ͍ͱTCAΛཧղͮ͠Β͍

    View Slide

  101. ೉͍͠ͱ͜Ζ
    • ͋Δఔ౓CombineΛཧղ͠ͳ͍ͱTCAΛཧղͮ͠Β͍
    • Combine͕OSS͡Όͳ͍ͨΊCombineࣗମͷίʔυ͸ಡΊͳ͍

    View Slide

  102. ೉͍͠ͱ͜Ζ
    • ͋Δఔ౓CombineΛཧղ͠ͳ͍ͱTCAΛཧղͮ͠Β͍
    • Combine͕OSS͡Όͳ͍ͨΊCombineࣗମͷίʔυ͸ಡΊͳ͍
    • جຊతʹ͸ΞϓϦઃܭ͸TCAͰ΍Γ͖Βͳ͍ͱ͍͚ͳ͍ʢؾ͕͢
    Δʣɻ

    View Slide

  103. ͞Βʹৄ͍͠৘ใ͸

    View Slide

  104. https://qiita.com/yimajo/items/77c204ab091223f9cb14
    QiitaʹهࣄΛॻ͍͍ͯ·͢

    View Slide

  105. 2020/09/21 14:40ʙ Track B ϨΪϡϥʔτʔΫʢ40෼ʣ
    TCAҎ֎ͷؔ਺ܕͳϑϨʔϜϫʔΫʹ͍ͭͯ஌Γ͍ͨͳΒ
    ͜ͷޙͷτʔΫʹظ଴͠·͠ΐ͏

    View Slide

  106. ࣗݾ঺հ

    View Slide

  107. ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT
    ʰ஑ାʹര஀ͨ͠α΢φͷָԂʮ͔Δ·Δʯ͕ྑ͗ͯ͢ҰੜՈʹؼΕͳ͍͔΋͠Εͳ͍ ʱ
    https://travel.spot-app.jp/karumaru_yoppy/

    View Slide

  108. • ࠓ৓ળۣʢ͍·͡ΐ͏ Α͠ͷΓʣ
    ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT
    ʰ஑ାʹര஀ͨ͠α΢φͷָԂʮ͔Δ·Δʯ͕ྑ͗ͯ͢ҰੜՈʹؼΕͳ͍͔΋͠Εͳ͍ ʱ
    https://travel.spot-app.jp/karumaru_yoppy/

    View Slide

  109. • ࠓ৓ળۣʢ͍·͡ΐ͏ Α͠ͷΓʣ
    • ΞΧ΢ϯτ͸େ఍ y.imajo ͱ͔
    yimajo
    ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT
    ʰ஑ାʹര஀ͨ͠α΢φͷָԂʮ͔Δ·Δʯ͕ྑ͗ͯ͢ҰੜՈʹؼΕͳ͍͔΋͠Εͳ͍ ʱ
    https://travel.spot-app.jp/karumaru_yoppy/

    View Slide

  110. • ࠓ৓ળۣʢ͍·͡ΐ͏ Α͠ͷΓʣ
    • ΞΧ΢ϯτ͸େ఍ y.imajo ͱ͔
    yimajo
    • גࣜձࣾΩϡϦΦγςΟιϑτ
    ΢ΣΞΛҰਓͰ΍͍ͬͯ·͢
    ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT
    ʰ஑ାʹര஀ͨ͠α΢φͷָԂʮ͔Δ·Δʯ͕ྑ͗ͯ͢ҰੜՈʹؼΕͳ͍͔΋͠Εͳ͍ ʱ
    https://travel.spot-app.jp/karumaru_yoppy/

    View Slide

  111. • ࠓ৓ળۣʢ͍·͡ΐ͏ Α͠ͷΓʣ
    • ΞΧ΢ϯτ͸େ఍ y.imajo ͱ͔
    yimajo
    • גࣜձࣾΩϡϦΦγςΟιϑτ
    ΢ΣΞΛҰਓͰ΍͍ͬͯ·͢
    • ͓͢͢Ίͷα΢φ͸஑ାͷʮ͔Δ
    ·Δʯ
    ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT
    ʰ஑ାʹര஀ͨ͠α΢φͷָԂʮ͔Δ·Δʯ͕ྑ͗ͯ͢ҰੜՈʹؼΕͳ͍͔΋͠Εͳ͍ ʱ
    https://travel.spot-app.jp/karumaru_yoppy/

    View Slide

  112. pixiv / BOOTHͰిࢠॻ੶Λൢച͍ͯ͠·͢
    https://swift.booth.pm/
    • async/await ݚڀಡຊ (iOSDC 2018)
    • RxSwift ݚڀಡຊ1~4 (iOSDC 2019)
    • VIPER ݚڀಡຊ
    • SwiftUI ΨΠυϒοΫ
    • Combine ΨΠυϒοΫ (iOSDC 2020)

    View Slide

  113. ݱঢ়ܧଓதͷ࢓ࣄ
    • IoTͷʮ·͝νϟϯωϧʯͱ͍͏
    ੡඼ͷiOSΞϓϦ։ൃΛख఻ͬͯ
    ͍·͢
    • SwiftUI൛΋࡞ͬͯͨΓ͠·͢

    View Slide

  114. ࠷ۙҰ೥Ҏ಺ͷ
    աڈͷ࢓ࣄ
    • ʮϚωʔϑΥϫʔυΫϥ΢υձ
    ܭʯͱ͍͏iOSΞϓϦ։ൃΛख
    ఻͍ͬͯ·ͨ͠

    View Slide

  115. Ҋ݅Λืू͍ͯ͠·͢

    View Slide

  116. • ΋͠ɺSwiftUIΛ࢖ͬͯΞϓϦ։ൃΛख఻ͬͯ΄͍͠૊৫/๏ਓ͕͍
    Ε͹ख఻͏ͷͰ͝࿈བྷ͍ͩ͘͞
    Ҋ݅Λืू͍ͯ͠·͢

    View Slide

  117. • ΋͠ɺSwiftUIΛ࢖ͬͯΞϓϦ։ൃΛख఻ͬͯ΄͍͠૊৫/๏ਓ͕͍
    Ε͹ख఻͏ͷͰ͝࿈བྷ͍ͩ͘͞
    • ΋͠ɺςετίʔυΛॻ͍͍ͯ͘จԽΛࠜ෇͔͍ͤͨͱ͍͏૊৫/๏
    ਓ͕͍Ε͹ॏཁੑͱָ͠͞Λਁಁͤ͞Δख఻͍΋Ͱ͖Δͱࢥ͏ͷ
    Ͱɺ͝࿈བྷ͍ͩ͘͞
    Ҋ݅Λืू͍ͯ͠·͢

    View Slide

  118. ࠷ޙ·Ͱ͝ཡ͍͖ͨͩ
    ͋Γ͕ͱ͏͍͟͝·ͨ͠
    @yimajo

    View Slide