Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Composable Architecture
Search
ry-itto
June 01, 2020
Programming
810
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Composable Architecture
uzimaru 生誕LT
ry-itto
June 01, 2020
More Decks by ry-itto
See All by ry-itto
決定版!?OSSアプリプロジェクトでのBeta版アプリ配布の方法「Xcode Cloud + TestFlight」
ryitto
0
400
CA.swift#14
ryitto
3
5.9k
swift-argument-parserで 簡単 CLI ツール作り
ryitto
1
170
Data Essentials in SwiftUI
ryitto
1
540
CollectionViewの 新しいレイアウトの作り方
ryitto
0
79
Swift5.1 SwiftUI
ryitto
0
140
Other Decks in Programming
See All in Programming
Javaの型とAI時代に型が大事な理由 / java types and type in AI era
kishida
2
160
AI駆動開発を妨げる技術的負債の解消アプローチ / ai-refactoring-approach
minodriven
15
8k
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
Contextとはなにか
chiroruxx
1
380
AI 輔助遺留系統現代化的經驗分享
jame2408
1
1.1k
TAKTでAI駆動開発の品質を設計する
j5ik2o
7
1.6k
Agentic UI
manfredsteyer
PRO
0
210
AIを活用したE2Eテスト実装効率化のあゆみ / ebisu-mobile-14-kotetu
kotetuco
0
140
Hatena Engineer Seminar #37「言語モデルの活用に関する研究」
slashnephy
0
390
技術記事、 専門家としてのプログラマ、 言語化
mizchi
13
6.6k
IBM Bobを活用したレガシーアプリの最新化
oniak3ibm
PRO
1
220
トークンをケチるな、設計しろ:GitHub Copilotを賢く使うコンテキスト戦略
ochtum
0
250
Featured
See All Featured
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
210
Rails Girls Zürich Keynote
gr2m
96
14k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
118
120k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
We Have a Design System, Now What?
morganepeng
55
8.2k
Making Projects Easy
brettharned
120
6.7k
The Curse of the Amulet
leimatthew05
2
13k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.7k
Done Done
chrislema
186
16k
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
160
The Cult of Friendly URLs
andyhume
79
6.9k
Reality Check: Gamification 10 Years Later
codingconduct
0
2.2k
Transcript
Composable Architecture ҏ౻྇ (@ry_itto)ɹ2020/06/01
ࣗݾհ • ໊લɿҏ౻྇ • ֶߍɿձେֶɾֶ෦4 • iOS ΤϯδχΞ • Χϐόϥ͕͖
Twitter/GitHub Facebook
Composable Architecture ͱʁ SwiftUI ͷొʹΑͬͯߟ͑ΒΕͨΞʔΩςΫνϟɻ Point-Free ͱ͍͏ Swift ʹؔ͢ΔಈըΛओʹग़͍ͯ͠ΔαΠ τͰհ͞Ε͍ͯΔɻ
ݱࡏ Part1 ͔Β Part4 ·Ͱհಈը͕ग़͍ͯΔɻ Part1, 2 ͰΞʔΩςΫνϟͷઆ໌ Part3,4 Ͱςετؔ࿈
ͱͷؔ࿈ The Composable Architecture was built on a foundation of
ideas started by other libraries, in particular Elm and Redux.
ͱͷؔ࿈
ͪΐͬͱৄࡉʹ Composable Architecture ͷॏཁͳ ֓೦ • State management • Composition
• Side effects • Testing • Ergonomics
ͪΐͬͱৄࡉʹ Redux ͱͷࠩҟ Redux ෭࡞༻Λॲཧ͢Δ࣌ʹͲͷΑ͏ʹ͢Δ ͖͔͕ΞʔΩςΫνϟϨϕϧͰఆٛ͞ Ε͍ͯͳ͍ Composable Architecture ෭࡞༻Λॲཧ͢Δ࣌ʹ
Effect ܕΛ Reducer ͔Βඞͣฦ͢Α͏ʹઃܭ͞ Ε͍ͯΔ Composable Architecture ͷํ͕ ݻ੍͍Λ͍࣋ͬͯΔ
ͪΐͬͱৄࡉʹ Elm ͱͷࠩҟ Elm Cmd ͰͲΜͳछྨͷ࡞༻Λߦ͑Δ੍͔ ޚ͍ͯ͠Δ Composable Architecture Combine
ͷ Publisher protocol ʹ४ ڌ͍ͯ͠ΔͨΊɺ༷ʑͳछྨͷ࡞༻ ΛΤεέʔϓϋονʢ༻ʣͰ͖Δ Composable Architecture ͷํ͕ ؇੍͍Λ͍࣋ͬͯΔ
ͪΐͬͱৄࡉʹ ଞͷϥΠϒϥϦ (ΞʔΩςΫνϟܥ) ʹͳ͍ಛ Composition • େ͖ΊͳػೳΛΑΓখ͍͞୯Ґ ʹ͢Δ͜ͱ͕Ͱ͖Δ • ΞϓϦ։ൃΛ্͍ͯ͘͠Ͱͷਏ
ΈʢίϯύΠϧ͕͍࣌ؒʣΛ ϞδϡʔϧԽ͢Δ͜ͱͰղܾ͢ Δ͜ͱ͕Ͱ͖Δ
ͪΐͬͱৄࡉʹ ্࣮བྷΉϥΠϒϥϦ ͷΫϥε/ߏମ • Store<State, Action> Redux Flux ͷ
Store ͱ΄΅ಉ͡ ͷ • Reducer<State, Action, Environment> ૾ʹ͘͠ͳ͍ Reducer Ͱ͢ɻ ଞͱҧͬͯ Environment ܕύϥ ϝʔλʹࢦఆ͠·͢ ※ Reducer ͷఆٛ (inout State, Action, Environment) -> Effect<Action, Never>
ͪΐͬͱৄࡉʹ ࣮͢Δࡍʹ࡞Δͷ • State ΞϓϦͷঢ়ଶ • Action ΞΫγϣϯ • Environment
͜͜ʹAPI ΫϥΠΞϯτΛ࣋ͬͨ ΓɺϝΠϯͷεϨουΛࢦఆͨ͠ Γ͢Δɻ DI ͱ͔Ͱ͖ͬͱΑ͘͏
σϞɾαϯϓϧ༻ʹ࡞ͬͨͷ • uzimaru-evolution (https://github.com/ry-itto/uzimaru-evolution) ͏͡·ΔΞΠίϯͷਐԽ • ComposableGitHubApp (https://github.com/ry-itto-playground/ComposableGitHubApp) GitHub ΫϥΠΞϯτʢϦϙδτϦݕࡧͷΈʣΛ
Composable Architecture Ͱ࣮ͨ͠αϯϓϧΞϓϦ
uzimaru-evolution ػೳ໘ • ը໘Լ෦ͷҹΛԡ͢ͱਐԽ/ୀ ԽͰ͖Δ • ཛͷঢ়ଶͷ࣌ୟ͘ͱਐԽͨ͠Γ ͢Δ ෦ •
΄΅΄΅ Flux/Redux ʹͳͬͯ͠ ·͍ͬͯΔ
struct ContentView: View { let store: Store<EvolutionState, EvolutionAction> 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
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
struct EvolutionReducerID: Hashable {} typealias EvolutionReducer = Reducer<EvolutionState, EvolutionAction, EvolutionEnvironment>
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
struct EvolutionReducerID: Hashable {} typealias EvolutionReducer = Reducer<EvolutionState, EvolutionAction, EvolutionEnvironment>
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ճୟ͍ͨΒผͷΞΫγϣϯ Λୟ͘Α͏ͳܗʹ͍ͯ͠Δ
෭࡞༻ͷશ͘ͳ͍αϯϓϧΞϓϦΛ ࡞ͬͯ͠·ͬͨͷͰ
ComposableGitHubApp ػೳ໘ • ϦϙδτϦݕࡧͯ͠ɺ݁ՌͷϦϙ δτϦ໊Λཏྻ͢Δ͚ͩ ෦ • Composable Architecture Λ͓ͦ
Β͍͍͘ײ͡ʹ༻Ͱ͖͍ͯΔ
struct ContentView: View { let store: Store<AppState, AppAction> 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
enum AppAction { case queryChanged(text: String) case load case repositoriesResponse(Result<[Repository],
Error>) } struct AppEnvironment { let apiClient: GitHubAPIClient let mainQueue: AnySchedulerOf<DispatchQueue> } 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..<lhs.repositories.count) .contains { lhs.repositories[$0].id != rhs.repositories[$0].id } } } ComposableGitHubApp Action / State / Environment
enum AppAction { case queryChanged(text: String) case load case repositoriesResponse(Result<[Repository],
Error>) } struct AppEnvironment { let apiClient: GitHubAPIClient let mainQueue: AnySchedulerOf<DispatchQueue> } 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..<lhs.repositories.count) .contains { lhs.repositories[$0].id != rhs.repositories[$0].id } } } ComposableGitHubApp Action / State / Environment લճͷঢ়ଶͱൺֱͯ͠ಉ ͩ͡ͱఆͨ͠߹ʹ ը໘ߋ৽Λ͠ͳ͍Α͏ʹ ͳ͍ͬͯΔ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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>
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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:) ʹ͋ͨΔ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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<Result<[Repository], Error>, Never> ʹܕม͞ΕΔ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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 ʹมͯ͠ ࣮ߦதͰ ΩϟϯηϧՄೳͳͷͱ ͯ͠ొ͢Δ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { 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 έʔεͷ࣍ʹೖΔέʔ εɻ ޭ্࣌ɺࣦഊ࣌Լʹ ೖΔɻ
·ͱΊ • Composable Architecture ݁ߏྑͦ͞͏ • SwiftUI ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷҎ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ •
ӳޠྗ͕ͬͱཉ͍͠
͋Γ͕ͱ͏͍͟͝·ͨ͠
ੜ͓ΊͰͱ͏