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

7d1521d950d9bc5697868830cb04ff2f?s=47 yimajo
September 20, 2020

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

7d1521d950d9bc5697868830cb04ff2f?s=128

yimajo

September 20, 2020
Tweet

Transcript

  1. iOSΞϓϦ։ൃͷͨΊͷ The Composable Architecture ͕ ͘͢͝ྑ͍ͷͰ঺հ͍ͨ͠ iOSDC Japan 2020 09/21

    11:30 #iosdc #d ࠓ৓ળۣ / @yimajo
  2. iOSDC2020.9.21 11:30ͷൃදʹ ೖΕΒΕ͍ͯͳ͔ͬͨ಺༰ʹ͍ͭͯ ิ଍ͷͨΊʹ ௥هͨ͠ࢿྉʹͳ͍ͬͯ·͢ ௥ه

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

  4. About The Composable Architecture

  5. About The Composable Architecture • A library for building applications

    • SwiftUI, UIKit • iOS, MacOS, tvOS, and WatchOS
  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
  7. Point-Free ௥ه https://www.pointfree.co/

  8. What is The Composable Architecture

  9. State management

  10. Composition

  11. Side Effect

  12. Testable

  13. Ergonomics

  14. Basic Usage

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

    • Store • Environment
  16. State Action Reducer Effect View Store

  17. State Action Reducer Effect View Store

  18. State Action Reducer Effect View Store

  19. State Action Reducer Effect View Store

  20. State Action Reducer Effect View Store

  21. State Action Reducer Effect View Store

  22. State Action Reducer Effect View Store

  23. State Action Reducer Effect View Store DI Environment

  24. αϯϓϧղઆ

  25. None
  26. TCA

  27. Samples TCA

  28. Samples Swift Package Dependencies TCA

  29. ΍ΕΔ͜ͱ • - ͱ + ΛλοϓͰ͖Δ • ਺ࣈͷද͕ࣔ - ͱ

    + ʹΑΓมߋ͞ΕΔ ಛ௃ • Effect ͕ͳ͍ɻEnvironment͸Χϥ CaseStudies/CounterDemo
  30. CaseStudies/CounterDemo State Action Reducer View Store

  31. CaseStudies/CounterDemo State Action Reducer View Store // Action enum CounterAction:

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

    Equatable { case decrementButtonTapped case incrementButtonTapped } // State struct CounterState: Equatable { var count = 0 }
  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 }
  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<CounterState, CounterAction> var body: some View { WithViewStore(self.store) { viewStore in HStack { Button("−") { viewStore.send(.decrementButtonTapped) } Text(“\(viewStore.count)”) Button("+") { viewStore.send(.incrementButtonTapped) } } }
  35. ΍ΕΔ͜ͱ • 2ߦͷ- ͱ + ΛλοϓͰ͖ΔView͕2ͭ ಛ௃ • Effect ͕ͳ͍ɻEnvironment͸Χϥ

    • Χ΢ϯλʔ༻ͷStoreͱViewΛͻͱͭͷ ෦඼ͱͯ͠2ߦར༻͍ͯ͠Δ CaseStudies/TwoCounterDemo
  36. CaseStudies/TwoCounterDemo State Action Reducer View Store

  37. CaseStudies/TwoCounterDemo State Action Reducer View Store // Action enum TwoCountersAction

    { case counter1(CounterAction) case counter2(CounterAction) }
  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() }
  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() }
  40. struct TwoCountersView: View { let store: Store<TwoCountersState, TwoCountersAction> 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") } }
  41. ΍ΕΔ͜ͱ • - ͱ + ΛλοϓͰ͖Δ • - λοϓ͢Δͱ1ඵޙʹ+͞ΕΔ •

    Number factϘλϯΛλοϓͰWeb APIݺͼग़͠ ಛ௃ • Effect ͕2ͭ͋Γɺ- Ϙλϯλοϓͱ NumberFact ϘλϯʹΑΓಈ࡞͢Δɻ CaseStudies/EffectBasics
  42. CaseStudies/EffectBasics State Action Reducer View Store Environment Effect

  43. CaseStudies/EffectBasics State Action Reducer View Store Environment // Action enum

    EffectsBasicsAction: Equatable { case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse ( Result<String, NumbersApiError> ) } Effect
  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<String, NumbersApiError> ) } Effect
  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<String, NumbersApiError> ) } 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 } }
  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<String, NumbersApiError> ) } 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 } }
  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<String, NumbersApiError> ) } 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 } }
  48. ͜͜·ͰΛ੔ཧ

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

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

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

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

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

  54. WithViewStore & ViewStore 㚎 㚎

  55. public struct WithViewStore<State, Action, Content>: View where Content: View {

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

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

    private let content: (ViewStore<State, Action>) -> Content private var prefix: String? @ObservedObject private var viewStore: ViewStore<State, Action> public init( _ store: Store<State, Action>, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content ) { self.content = content self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) } public var body: some View { … লུ return self.content(self.viewStore) }
  58. @dynamicMemberLookup public final class ViewStore<State, Action>: ObservableObject { … লུ

    /// The current state. public private(set) var state: State { willSet { self.objectWillChange.send() } } … লུ public func send(_ action: Action) { self._send(action) }
  59. Effect

  60. public struct Effect<Output, Failure: Error>: Publisher

  61. • Combine.Publisherʹ४ڌ public struct Effect<Output, Failure: Error>: Publisher

  62. • Combine.Publisherʹ४ڌ • PublisherͳͷͰTCA಺෦ͰSubscriber؂ࢹͰ͖Δ public struct Effect<Output, Failure: Error>: Publisher

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

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

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

  66. Effect<Int, Never>.future { callback in DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

    callback(.success(42)) } } ௥ه
  67. Effect<User, Error>.result { let fileUrl = URL( fileURLWithPath: NSSearchPathForDirectoriesInDomains( .documentDirectory,

    .userDomainMask, true )[0] ) .appendingPathComponent("user.json") let result = Result<User, Error> { let data = try Data(contentsOf: fileUrl) return try JSONDecoder().decode(User.self, from: $0) } return result } ௥ه
  68. EffectܕΛ؂ࢹͯ࣍͠ͷActionΛݺͼग़͢࢓૊Έ

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

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

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

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

  73. ςετίʔυ

  74. State Action Reducer Environment Effect TestStore Scheduler

  75. State Action Reducer Environment Effect TestStore Scheduler SUT

  76. State Action Reducer Environment Effect TestStore Scheduler SUT

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

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

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

    Double SUT
  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" } ) }
  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" } ) }
  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" } ) }
  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 ͱൺֱ
  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 ͱൺֱ
  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 ͱൺֱ
  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͕ड͚औΔظ଴஋
  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
  88. TCAͷςετίʔυʹ͍ͭͯͷ੔ཧ

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

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

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

  92. TCAʹ͍ͭͯ ·ͱΊ

  93. ྑ͍ͱ͜Ζ

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

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

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

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

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

    • ςετεέδϡʔϥͱςετํ๏Λఏڙͯ͘͠Ε͍ͯΔ
  99. ೉͍͠ͱ͜Ζ

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

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

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

  103. ͞Βʹৄ͍͠৘ใ͸

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

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

  106. ࣗݾ঺հ

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

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

  109. • ࠓ৓ળۣʢ͍·͡ΐ͏ Α͠ͷΓʣ • ΞΧ΢ϯτ͸େ఍ y.imajo ͱ͔ yimajo ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT

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

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

    ΢ΣΞΛҰਓͰ΍͍ͬͯ·͢ • ͓͢͢Ίͷα΢φ͸஑ାͷʮ͔Δ ·Δʯ ը૾Ҿ༻ݩ: ͓Ͱ͔͚ମݧܕϝσΟΞSPOT ʰ஑ାʹര஀ͨ͠α΢φͷָԂʮ͔Δ·Δʯ͕ྑ͗ͯ͢ҰੜՈʹؼΕͳ͍͔΋͠Εͳ͍ ʱ https://travel.spot-app.jp/karumaru_yoppy/
  112. pixiv / BOOTHͰిࢠॻ੶Λൢച͍ͯ͠·͢ https://swift.booth.pm/ • async/await ݚڀಡຊ (iOSDC 2018) •

    RxSwift ݚڀಡຊ1~4 (iOSDC 2019) • VIPER ݚڀಡຊ • SwiftUI ΨΠυϒοΫ • Combine ΨΠυϒοΫ (iOSDC 2020)
  113. ݱঢ়ܧଓதͷ࢓ࣄ • IoTͷʮ·͝νϟϯωϧʯͱ͍͏ ੡඼ͷiOSΞϓϦ։ൃΛख఻ͬͯ ͍·͢ • SwiftUI൛΋࡞ͬͯͨΓ͠·͢

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

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

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

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

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