Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Composable Architecture
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
ry-itto
June 01, 2020
Programming
820
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
420
CA.swift#14
ryitto
3
5.9k
swift-argument-parserで 簡単 CLI ツール作り
ryitto
1
180
Data Essentials in SwiftUI
ryitto
1
550
CollectionViewの 新しいレイアウトの作り方
ryitto
0
88
Swift5.1 SwiftUI
ryitto
0
160
Other Decks in Programming
See All in Programming
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
250
AI時代のIaC入門 〜なぜインフラをコードで管理したいのか?〜 / Introduction to IaC in the AI Era
akihisaikeda
5
1.2k
Kotlinの15年とServer-Side Kotlin
n_takehata
1
170
エージェントを使い続けるためのメタワーク / Meta-work for keeping using the agent
rkaga
6
2k
Seven New Principles of Software Engineering
yegor256
0
370
[2026-10-09]一枚の布で作らなくてもいい。 信頼性は「パッチワーク」で紡がれる~既存の運用と新しい可能性をつなぎ合わせてきたCREの話~
tosite
0
120
Python Lambdaにおけるテストしやすい設計とAI支援の活用 / Designing Testable Python Lambda Functions with AI
kitano_yuichi
1
240
Apple Intelligenceとオンデバイス時代の到来
yahir0
0
160
AIが生成したコードは読むべきか?抽象化設計とコードの品質保証 / abstraction-design-and-quality
minodriven
11
7.3k
AI Augmentation Techniques with Liberty
ivargrimstad
0
120
TS 7.0 で baseUrl が使えなくなった今、パスエイリアスをどうするか
bicstone
1
140
Web Framework Benchmark Battle 2026, Any%
a_matsuda
3
3k
Featured
See All Featured
Abbi's Birthday
coloredviolet
4
10k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
1
470
Un-Boring Meetings
codingconduct
0
470
Producing Creativity
orderedlist
PRO
348
41k
The Invisible Side of Design
smashingmag
301
52k
Navigating Algorithm Shifts & AI Overviews - #SMXNext
aleyda
1
1.7k
Building the Perfect Custom Keyboard
takai
2
930
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.9k
Believing is Seeing
oripsolob
1
250
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
3.2k
ラッコキーワード サービス紹介資料
rakko
5
5.5M
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 ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷҎ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ •
ӳޠྗ͕ͬͱཉ͍͠
͋Γ͕ͱ͏͍͟͝·ͨ͠
ੜ͓ΊͰͱ͏