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
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
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
ChromeでローカルLLM
ryu1013
0
160
Groovy as a human and AI friendly programming language
paulk
0
110
作って学ぶSpring BootのAuto Configuration / JJUG Night Seminar 2026-10-19
rshindo
0
210
Next.js: The final boss of Web Performance
guaca
0
340
當我們開始不讀 Code - AI Agent 時代,Code Review 與 Clean Code 還剩下什麼?
jame2408
1
320
Jetpack Compose Mechanisms
skydoves
3
870
Optunaを作ったときにやりたかったこと、その後 (2026/10/16, Optuna Meetup #3)
iwiwi
4
1.3k
Findy - エンジニア向け会社紹介/Findy Company Deck
findyinc
6
400k
JIS keyboard support in Omarchy
lef237
0
160
Seven New Principles of Software Engineering
yegor256
0
310
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
240
複雑なUIコンポーネントとの ⻑い付き合い⽅:波及するバグからテスト戦略を経て、安全な再設計へ
mixplace
0
460
Featured
See All Featured
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
50
10k
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
620
Building Flexible Design Systems
yeseniaperezcruz
330
41k
Optimizing for Happiness
mojombo
378
71k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
280
KATA
mclloyd
PRO
35
16k
How to Get Subject Matter Experts Bought In and Actively Contributing to SEO & PR Initiatives.
livdayseo
0
230
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
540
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
540
VelocityConf: Rendering Performance Case Studies
addyosmani
331
25k
Mobile First: as difficult as doing things right
swwweet
225
10k
Heart Work Chapter 1 - Part 1
lfama
PRO
10
37k
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 ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷҎ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ •
ӳޠྗ͕ͬͱཉ͍͠
͋Γ͕ͱ͏͍͟͝·ͨ͠
ੜ͓ΊͰͱ͏