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
82
Swift5.1 SwiftUI
ryitto
0
150
Other Decks in Programming
See All in Programming
すこし踏み込む CancellationToken
htkym
2
1.5k
GitHubハンズオン講座 — 実務レベルのチーム開発のフローを身につけよう
junhat6
0
110
KiroのSpecで「五目並べ」を作ってみる
satoshi256kbyte
1
320
Agents on Rails - Rails at Scale 2026
irinanazarova
0
310
Starting & Sustaining Code-Based E2E Testing for Non-Coding QA Teams( #jasstniigata )
teyamagu
PRO
1
800
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
600
Are APIs Still Relevant in the AI Era?
soyuka
0
380
海上で動くGoサーバー: goroutineとchannelでさばく航行データストリーム
atsuki_seo
0
1k
Unity6 URPによる海底をテーマにしたVJシステムの解説 | Unity Shader 完全に理解した 勉強会
gam0022
0
210
Intent as Code
shoppingjaws
6
1.1k
GemmaをJevのように使ってみる / Use Gemma like Jev
kishida
4
440
動作中のプログラムの中身をリアルタイムに覗く / Realtime Debugger for CSharp with Roslyn
prota
1
1.6k
Featured
See All Featured
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
710
Abbi's Birthday
coloredviolet
4
10k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
123
22k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
250
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
530
The Impact of AI in SEO - AI Overviews June 2024 Edition
aleyda
6
1.2k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.5k
Build The Right Thing And Hit Your Dates
maggiecrowley
39
3.5k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
33
5k
Paper Plane (Part 1)
katiecoart
PRO
2
11k
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 ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷҎ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ •
ӳޠྗ͕ͬͱཉ͍͠
͋Γ͕ͱ͏͍͟͝·ͨ͠
ੜ͓ΊͰͱ͏