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 full-size slide

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

    View full-size slide

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

    View full-size slide

  4. About The Composable Architecture

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

  8. What is
    The Composable
    Architecture

    View full-size slide

  9. State management

    View full-size slide

  10. Side Effect

    View full-size slide

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

    View full-size slide

  12. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  13. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  14. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  15. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  16. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  17. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  18. State
    Action
    Reducer
    Effect
    View
    Store

    View full-size slide

  19. State
    Action
    Reducer
    Effect
    View
    Store
    DI
    Environment

    View full-size slide

  20. αϯϓϧղઆ

    View full-size slide

  21. Samples
    Swift Package Dependencies
    TCA

    View full-size slide

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

    View full-size slide

  23. CaseStudies/CounterDemo
    State
    Action
    Reducer
    View
    Store

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  26. 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 full-size slide

  27. 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 full-size slide

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

    View full-size slide

  29. CaseStudies/TwoCounterDemo
    State
    Action
    Reducer
    View
    Store

    View full-size slide

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

    View full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

  41. ͜͜·ͰΛ੔ཧ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. WithViewStore
    &
    ViewStore


    View full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. @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 full-size slide

  52. public struct Effect: Publisher

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  59. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  65. ςετίʔυ

    View full-size slide

  66. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler

    View full-size slide

  67. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler
    SUT

    View full-size slide

  68. State
    Action
    Reducer
    Environment
    Effect
    TestStore
    Scheduler
    SUT

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

  75. 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 full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  84. TCAʹ͍ͭͯ
    ·ͱΊ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  90. ೉͍͠ͱ͜Ζ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  94. ͞Βʹৄ͍͠৘ใ͸

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide