Slide 1

Slide 1 text

Composable Architecture ҏ౻྇໵ (@ry_itto)ɹ2020/06/01

Slide 2

Slide 2 text

ࣗݾ঺հ • ໊લɿҏ౻྇໵ • ֶߍɿձ௡େֶɾֶ෦4೥ • iOS ΤϯδχΞ • Χϐόϥ͕޷͖ Twitter/GitHub Facebook

Slide 3

Slide 3 text

Composable Architecture ͱ͸ʁ SwiftUI ͷొ৔ʹΑͬͯߟ͑ΒΕͨΞʔΩςΫνϟɻ Point-Free ͱ͍͏ Swift ʹؔ͢ΔಈըΛओʹग़͍ͯ͠ΔαΠ τͰ঺հ͞Ε͍ͯΔɻ ݱࡏ Part1 ͔Β Part4 ·Ͱ঺հಈը͕ग़͍ͯΔɻ Part1, 2 ͰΞʔΩςΫνϟͷઆ໌ Part3,4 Ͱςετؔ࿈

Slide 4

Slide 4 text

ͱͷؔ࿈ The Composable Architecture was built on a foundation of ideas started by other libraries, in particular Elm and Redux.

Slide 5

Slide 5 text

ͱͷؔ࿈

Slide 6

Slide 6 text

ͪΐͬͱৄࡉʹ Composable Architecture ͷॏཁͳ ֓೦ • State management • Composition • Side effects • Testing • Ergonomics

Slide 7

Slide 7 text

ͪΐͬͱৄࡉʹ Redux ͱͷࠩҟ Redux ෭࡞༻Λॲཧ͢Δ࣌ʹͲͷΑ͏ʹ͢Δ΂ ͖͔͕ΞʔΩςΫνϟϨϕϧͰ͸ఆٛ͞ Ε͍ͯͳ͍ Composable Architecture ෭࡞༻Λॲཧ͢Δ࣌ʹ Effect ܕΛ Reducer ͔Βඞͣฦ͢Α͏ʹઃܭ͞ Ε͍ͯΔ Composable Architecture ͷํ͕ ݻ੍͍໿Λ͍࣋ͬͯΔ

Slide 8

Slide 8 text

ͪΐͬͱৄࡉʹ Elm ͱͷࠩҟ Elm Cmd ͰͲΜͳछྨͷ࡞༻Λߦ͑Δ੍͔ ޚ͍ͯ͠Δ Composable Architecture Combine ͷ Publisher protocol ʹ४ ڌ͍ͯ͠ΔͨΊɺ༷ʑͳछྨͷ࡞༻ ΛΤεέʔϓϋονʢ࢖༻ʣͰ͖Δ Composable Architecture ͷํ͕ ؇੍͍໿Λ͍࣋ͬͯΔ

Slide 9

Slide 9 text

ͪΐͬͱৄࡉʹ ଞͷϥΠϒϥϦ (ΞʔΩςΫνϟܥ) ʹͳ͍ಛ௃ Composition • େ͖ΊͳػೳΛΑΓখ͍͞୯Ґ ʹ͢Δ͜ͱ͕Ͱ͖Δ • ΞϓϦ։ൃΛ্͍ͯ͘͠Ͱͷਏ ΈʢίϯύΠϧ͕࣌ؒ௕͍ʣΛ ϞδϡʔϧԽ͢Δ͜ͱͰղܾ͢ Δ͜ͱ͕Ͱ͖Δ

Slide 10

Slide 10 text

ͪΐͬͱৄࡉʹ ࣮૷্བྷΉϥΠϒϥϦ ͷΫϥε/ߏ଄ମ • Store Redux ΍ Flux ͷ Store ͱ΄΅ಉ͡ ΋ͷ • Reducer ૝૾ʹ೉͘͠ͳ͍ Reducer Ͱ͢ɻ ଞͱҧͬͯ Environment ΋ܕύϥ ϝʔλʹࢦఆ͠·͢ ※ Reducer ͷఆٛ
 (inout State, Action, Environment) -> Effect

Slide 11

Slide 11 text

ͪΐͬͱৄࡉʹ ࣮૷͢Δࡍʹ࡞Δ΋ͷ • State ΞϓϦͷঢ়ଶ • Action ΞΫγϣϯ • Environment ͜͜ʹAPI ΫϥΠΞϯτΛ࣋ͬͨ ΓɺϝΠϯͷεϨουΛࢦఆͨ͠ Γ͢Δɻ DI ͱ͔Ͱ͖ͬͱΑ͘࢖͏

Slide 12

Slide 12 text

σϞɾαϯϓϧ༻ʹ࡞ͬͨ΋ͷ • uzimaru-evolution (https://github.com/ry-itto/uzimaru-evolution) ͏͡·ΔΞΠίϯͷਐԽ • ComposableGitHubApp (https://github.com/ry-itto-playground/ComposableGitHubApp) GitHub ΫϥΠΞϯτʢϦϙδτϦݕࡧͷΈʣΛ Composable Architecture Ͱ࣮૷ͨ͠αϯϓϧΞϓϦ

Slide 13

Slide 13 text

uzimaru-evolution ػೳ໘ • ը໘Լ෦ͷ໼ҹΛԡ͢ͱਐԽ/ୀ ԽͰ͖Δ • ཛͷঢ়ଶͷ࣌͸ୟ͘ͱਐԽͨ͠Γ ͢Δ ಺෦ • ΄΅΄΅ Flux/Redux ʹͳͬͯ͠ ·͍ͬͯΔ

Slide 14

Slide 14 text

struct ContentView: View { let store: Store var body: some View { WithViewStore(self.store) { viewStore in ZStack { Color("background") VStack { Text("uzimaru Evolution") Image.init(viewStore.evolution.text) .onTapGesture { viewStore.send(.poke) } HStack(spacing: 30) { Button( action: { viewStore.send(.degenerate) }, label: { Image(systemName: "arrow.left.circle") }) Text(viewStore.evolution.text) Button( action: { viewStore.send(.evolve) }, label: { Image(systemName: "arrow.right.circle") }) } } } } } } uzimaru-evolution View

Slide 15

Slide 15 text

enum EvolutionAction { case evolve case degenerate case poke } struct EvolutionState: Equatable { var pokeCount: Int var evolution: Evolution init(pokeCount: Int = 0, evolution: Evolution) { self.pokeCount = pokeCount self.evolution = evolution } } uzimaru-evolution Action / State

Slide 16

Slide 16 text

struct EvolutionReducerID: Hashable {} typealias EvolutionReducer = Reducer let evolutionReducer = EvolutionReducer { (state, action, _) in switch action { case .evolve: guard let next = state.evolution.next else { return .cancel(id: EvolutionReducerID()) } state.evolution = next case .degenerate: guard let previous = state.evolution.previous else { return .cancel(id: EvolutionReducerID()) } state.evolution = previous case .poke: if case .v1 = state.evolution { state.pokeCount += 1 if state.pokeCount == 4 { state.pokeCount = 0 return .init(value: .evolve) } } else { return .cancel(id: EvolutionReducerID()) } } return .none } uzimaru-evolution Reducer

Slide 17

Slide 17 text

struct EvolutionReducerID: Hashable {} typealias EvolutionReducer = Reducer let evolutionReducer = EvolutionReducer { (state, action, _) in switch action { case .evolve: guard let next = state.evolution.next else { return .cancel(id: EvolutionReducerID()) } state.evolution = next case .degenerate: guard let previous = state.evolution.previous else { return .cancel(id: EvolutionReducerID()) } state.evolution = previous case .poke: if case .v1 = state.evolution { state.pokeCount += 1 if state.pokeCount == 4 { state.pokeCount = 0 return .init(value: .evolve) } } else { return .cancel(id: EvolutionReducerID()) } } return .none } uzimaru-evolution ཛঢ়ଶͷ࣌ʹୟ͘ͱਐԽ͢ Δࡍͷέʔεఆٛ 4ճୟ͍ͨΒผͷΞΫγϣϯ Λୟ͘Α͏ͳܗʹ͍ͯ͠Δ

Slide 18

Slide 18 text

෭࡞༻ͷશ͘ͳ͍αϯϓϧΞϓϦΛ ࡞ͬͯ͠·ͬͨͷͰ

Slide 19

Slide 19 text

ComposableGitHubApp ػೳ໘ • ϦϙδτϦݕࡧͯ͠ɺ݁ՌͷϦϙ δτϦ໊Λཏྻ͢Δ͚ͩ ಺෦ • Composable Architecture Λ͓ͦ Β͍͍͘ײ͡ʹ࢖༻Ͱ͖͍ͯΔ

Slide 20

Slide 20 text

struct ContentView: View { let store: Store var body: some View { WithViewStore(self.store) { viewStore in VStack { TextField( "ݕࡧϫʔυ", text: viewStore.binding( get: { $0.query }, send: { .queryChanged(text: $0) } ) ) .textFieldStyle(RoundedBorderTextFieldStyle()) List { ForEach(viewStore.repositories, id: \.id) { repository in Text(repository.name) } } }.onAppear { viewStore.send(.load) } } } } ComposableGitHubApp View

Slide 21

Slide 21 text

enum AppAction { case queryChanged(text: String) case load case repositoriesResponse(Result<[Repository], Error>) } struct AppEnvironment { let apiClient: GitHubAPIClient let mainQueue: AnySchedulerOf } struct AppState: Equatable { var query: String = "" var repositories: [Repository] = [] static func == (lhs: AppState, rhs: AppState) -> Bool { return lhs.query == rhs.query && lhs.repositories.count == rhs.repositories.count && !(0..

Slide 22

Slide 22 text

enum AppAction { case queryChanged(text: String) case load case repositoriesResponse(Result<[Repository], Error>) } struct AppEnvironment { let apiClient: GitHubAPIClient let mainQueue: AnySchedulerOf } struct AppState: Equatable { var query: String = "" var repositories: [Repository] = [] static func == (lhs: AppState, rhs: AppState) -> Bool { return lhs.query == rhs.query && lhs.repositories.count == rhs.repositories.count && !(0..

Slide 23

Slide 23 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp Reducer

Slide 24

Slide 24 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp Reducer

Slide 25

Slide 25 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp APIClient ͷϦϙδτϦݕࡧ ༻ͷϝιουΛݺͿ ฦΓ஋͸ Effect<[Repository], Error>

Slide 26

Slide 26 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp ϝΠϯεϨουͰऔಘͨ͠ ஋ΛόΠϯυ RxSwift ͩͱ subscribe(on:) ʹ͋ͨΔ

Slide 27

Slide 27 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp ࣦഊ͠ͳ͍ Effect ʹม׵͢ Δ Effect<[Repository], Error> ͔Β Effect, Never> ʹܕม׵͞ΕΔ

Slide 28

Slide 28 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp Action ʹม׵ͯ͠ ࣮ߦதͰ ΋ΩϟϯηϧՄೳͳ΋ͷͱ ͯ͠ొ࿥͢Δ

Slide 29

Slide 29 text

let appReducer = Reducer { state, action, environment in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp load έʔεͷ࣍ʹೖΔέʔ εɻ ੒ޭ࣌͸্ɺࣦഊ࣌͸Լʹ ೖΔɻ

Slide 30

Slide 30 text

·ͱΊ • Composable Architecture ݁ߏྑͦ͞͏ • SwiftUI ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷ೥Ҏ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ • ӳޠྗ͕΋ͬͱཉ͍͠

Slide 31

Slide 31 text

͋Γ͕ͱ͏͍͟͝·ͨ͠

Slide 32

Slide 32 text

஀ੜ೔͓ΊͰͱ͏