Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

About The Composable Architecture

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

What is The Composable Architecture

Slide 9

Slide 9 text

State management

Slide 10

Slide 10 text

Composition

Slide 11

Slide 11 text

Side Effect

Slide 12

Slide 12 text

Testable

Slide 13

Slide 13 text

Ergonomics

Slide 14

Slide 14 text

Basic Usage

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

State Action Reducer Effect View Store

Slide 17

Slide 17 text

State Action Reducer Effect View Store

Slide 18

Slide 18 text

State Action Reducer Effect View Store

Slide 19

Slide 19 text

State Action Reducer Effect View Store

Slide 20

Slide 20 text

State Action Reducer Effect View Store

Slide 21

Slide 21 text

State Action Reducer Effect View Store

Slide 22

Slide 22 text

State Action Reducer Effect View Store

Slide 23

Slide 23 text

State Action Reducer Effect View Store DI Environment

Slide 24

Slide 24 text

αϯϓϧղઆ

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

TCA

Slide 27

Slide 27 text

Samples TCA

Slide 28

Slide 28 text

Samples Swift Package Dependencies TCA

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

CaseStudies/CounterDemo State Action Reducer View Store

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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 }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

CaseStudies/TwoCounterDemo State Action Reducer View Store

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

CaseStudies/EffectBasics State Action Reducer View Store Environment Effect

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

͜͜·ͰΛ੔ཧ

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

αϯϓϧίʔυ ͷิ଍

Slide 54

Slide 54 text

WithViewStore & ViewStore 㚎 㚎

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Effect

Slide 60

Slide 60 text

public struct Effect: Publisher

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

ςετίʔυ

Slide 74

Slide 74 text

State Action Reducer Environment Effect TestStore Scheduler

Slide 75

Slide 75 text

State Action Reducer Environment Effect TestStore Scheduler SUT

Slide 76

Slide 76 text

State Action Reducer Environment Effect TestStore Scheduler SUT

Slide 77

Slide 77 text

State Action Reducer Environment Effect TestStore Scheduler Test Double SUT

Slide 78

Slide 78 text

State Action Reducer Environment Effect TestStore Scheduler Test Double SUT

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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 ͱൺֱ

Slide 84

Slide 84 text

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 ͱൺֱ

Slide 85

Slide 85 text

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 ͱൺֱ

Slide 86

Slide 86 text

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͕ड͚औΔظ଴஋

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

TCAʹ͍ͭͯ ·ͱΊ

Slide 93

Slide 93 text

ྑ͍ͱ͜Ζ

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

೉͍͠ͱ͜Ζ

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

͞Βʹৄ͍͠৘ใ͸

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

ࣗݾ঺հ

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

Ҋ݅Λืू͍ͯ͠·͢

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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