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
Thinking about Architecture for SwiftUI
Search
d_date
January 30, 2020
Programming
2.6k
8
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Thinking about Architecture for SwiftUI
2020/01/30 CA.swift
d_date
January 30, 2020
More Decks by d_date
See All by d_date
TCA Practice in 5 min
d_date
2
2k
waiwai-swiftpm-part2
d_date
3
610
わいわいSwift PM part 1
d_date
2
500
What's new in Firebase 2021
d_date
2
1.6k
CI/CDをミニマルに構築する
d_date
1
640
Swift Package centered project - Build and Practice
d_date
20
17k
How to write Great Proposal
d_date
4
2.4k
Integrate your app to modern world in Niigata
d_date
0
780
Integrate your app to modern world
d_date
2
780
Other Decks in Programming
See All in Programming
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
3.7k
Workers Cache を知る
syumai
0
370
ActionController meets Protocol Buffers
sorah
0
560
コーディング自動化の次へ。人の介入をゼロにするファクトリーとサポートAIによる実践
timakin
5
4k
パスワード再設定メールのURLについて知っておこう
uyuki234
0
160
[2026-09-26]空論ジェネリックプロセス~テスト資産とAIで紡ぐ、再現可能なパフォーマンスチューニングの話~
tosite
0
270
Optunaを作ったときにやりたかったこと、その後 (2026/10/16, Optuna Meetup #3)
iwiwi
2
810
気持ちぃ〜角丸 Squircle
ryu1013
0
320
Next.js: The final boss of Web Performance
guaca
0
290
Long Living Tokens, Die, Die, Die! (or: The One Ring is Banned in Modern)
sylph01
0
330
SalesForceを内製化!? ~ HR事業を支える顧客管理基盤のインフラを大公開 ~
oku053
0
190
ChromeでローカルLLM
ryu1013
0
140
Featured
See All Featured
New Earth Scene 8
popppiees
4
2.7k
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1.1k
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
63
46k
Tell your own story through comics
letsgokoyo
1
1.1k
Everyday Curiosity
cassininazir
0
360
State of Search Keynote: SEO is Dead Long Live SEO
ryanjones
0
320
Prompt Engineering for Job Search
mfonobong
0
520
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.5k
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
2
6.3k
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
540
Future Trends and Review - Lecture 12 - Web Technologies (1019888BNR)
signer
PRO
0
3.8k
Building Flexible Design Systems
yeseniaperezcruz
330
41k
Transcript
Thinking about Architecture for SwiftUI CA.swift Daiki Matsudate @d_date iOS
Developer
Daiki Matsudate • Tokyo • iOS Developer from iOS 4
• Google Developers Expert for Firebase • Book: ʮiOSΞϓϦઃܭύλʔϯೖʯ
None
None
March, 18 - 20th, 2020 https://www.tryswift.co/
None
https://twitter.com/ios_memes/status/1174273871983370240?s=21
The Age of Declarative UI
The Age of Declarative UI • iOS: SwiftUI • Android:
Jetpack Compose • ReactNative / Flutter
SwiftUI • UI Framework with declarative Syntax • Live Preview
in Xcode • Available for iOS, iPadOS, macOS, watchOS and tvOS • Using newest Swift features • Property wrapper • Function Builder • Opaque Result Type • Goodbye Storyboard / Xib
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif public protocol View { associatedtype Body : View var body: Self.Body { get } }
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif Opaque Result Type
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif Function Builders
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } }
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } }
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } } Property Wrapper wrappedValue: Value projectedValue: Wrapped<Value> Without $ With $
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } } Property Wrapper Value Binding<Value> Without $ With $ cf. RxSwift.BehaviorRelay
Dataflow
Combine
Combine • Declarative Swift API • ඇಉظͳΠϕϯτΛܕͱͯ͠දݱ • ଟछଟ༷ͳԋࢉࢠͰΠϕϯτΛϋϯυϦϯά •
Reactive Framework by Apple
https://twitter.com/diegopetrucci/status/1135655480825655297
User Interaction SwiftUI Action State Mutation View Updates Render !
" ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/
Data Flow with MVVM struct FormView: View { let dependency:
FormViewController.Dependency @ObservedObject var viewModel: FormViewSwiftUIModel init(dependency: FormViewController.Dependency) { self.dependency = dependency self.viewModel = .init(validation: dependency.validation) } var isValid: Bool { viewModel.isValid && !viewModel.isEmpty }
Data Flow with MVVM struct FormView: View { let dependency:
FormViewController.Dependency @ObservedObject var viewModel: FormViewSwiftUIModel init(dependency: FormViewController.Dependency) { self.dependency = dependency self.viewModel = .init(validation: dependency.validation) } var isValid: Bool { viewModel.isValid && !viewModel.isEmpty } ViewModel with ObservedObject
Data Flow with MVVM import Foundation import SwiftUI class FormViewSwiftUIModel:
ObservableObject { let validation: (String) -> ValidationResult var value: String = "" { willSet { if newValue != value { validationResult = self.validation(newValue) } } } var validationResult: ValidationResult = .empty { willSet { objectWillChange.send() } }
https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()
https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send() Unidirectional Dataflow
Unidirectional Dataflow
Unidirectional Dataflow • Flux • Redux (ReSwift etc.) • Composable
Architecture
None
None
Redux • Unidirectional ( View -> Action -> Store ->
Reducer -> State -> View) • Single Store • n-State / n-Action • Mutate state in reducer
None
Example: Composable Architecture
None
https://www.pointfree.co
Composable Architecture • Redux + Elm Architecture • Optimize for
SwiftUI / Combine • Functional • View / State / Action / Store / Reducer • Side Effect has treated as Effect type • Composable
Send Action to Store public var body: some View {
VStack { HStack { Button("-") { self.store.send(.counter(.decrTapped)) } Text("\(self.store.value.count)") Button("+") { self.store.send(.counter(.incrTapped)) } }
Handle action in Reducer public func counterReducer(state: inout CounterState, action:
CounterAction) -> [Effect<CounterAction>] { switch action { case .decrTapped: state.count -= 1 return [] case .incrTapped: state.count += 1 return []
Effect public struct Effect<A> { public let run: (@escaping (A)
-> Void) -> Void public init(run: @escaping (@escaping (A) -> Void) -> Void) { self.run = run } public func map<B>(_ f: @escaping (A) -> B) -> Effect<B> { return Effect<B> { callback in self.run { a in callback(f(a)) } } } }
Send action public func send(_ action: Action) { let effects
= self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }
Update View with State public var body: some View {
VStack { HStack { Button("-") { self.store.send(.counter(.decrTapped)) } Text("\(self.store.value.count)") Button("+") { self.store.send(.counter(.incrTapped)) } }
Are you using Combine / SwiftUI now?
Can we do same in RxSwift / UIKit now?
Send Action to Store sendButton.rx.tap .subscribe(onNext: { [store] _ in
if let phone = store.value.phoneNumber { store.send(.verify(phone, self)) } }) .disposed(by: disposeBag)
Handle action in Reducer func signUpReducer(state: inout SignUpState, action: SignUpAction)
-> [Effect<SignUpAction>] { switch action { case .verify(let phone, let delegate): state.loading = .loading(showLoadingView: true) return [ PhoneAuthProvider.provider() .verifyPhoneNumber(phoneNumber: phone, uiDelegate: delegate) .map(SignUpAction.verifyResponse) ] case .verifyResponse(let result): switch result { case .success(let code): state.loading = .completed state.verificationID = code return [] case .failure(let error): state.loading = .error(SignUpError(error: error)) return [] }
Effect public struct Effect<A> { public let run: (@escaping (A)
-> Void) -> Void public init(run: @escaping (@escaping (A) -> Void) -> Void) { self.run = run } public func map<B>(_ f: @escaping (A) -> B) -> Effect<B> { return Effect<B> { callback in self.run { a in callback(f(a)) } } } }
Send action public func send(_ action: Action) { let effects
= self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }
Update View with State store[\.loading] .compactMap { $0 } .distinctUntilChanged()
.subscribe(onNext: { [weak self, store] state in guard let self = self else { return } self.modifyLoadState(state: state) switch state { case .loading: self.phoneNumberTextField.resignFirstResponder() case .completed: if let verificationID = store.value.verificationID, store.value.validationResult != nil { self.transit(verificationID: verificationID) store.send(.reset) } case .error(let error): self.errorLabel.text = error.localizedDescription } }) .disposed(by: disposeBag)
Update View with State store[\.loading] .compactMap { $0 } .distinctUntilChanged()
.subscribe(onNext: { [weak self, store] state in guard let self = self else { return } self.modifyLoadState(state: state) switch state { case .loading: self.phoneNumberTextField.resignFirstResponder() case .completed: if let verificationID = store.value.verificationID, store.value.validationResult != nil { self.transit(verificationID: verificationID) store.send(.reset) } case .error(let error): self.errorLabel.text = error.localizedDescription } }) .disposed(by: disposeBag)
Why Single Store?
Why Single Store? • Broadcast ALL States • Easily to
REUSE existing state
Resources / free episodes • https://www.pointfree.co/episodes/ep65-swiftui-and-state-management-part-1 • https://www.pointfree.co/episodes/ep66-swiftui-and-state-management-part-2 • https://www.pointfree.co/episodes/ep67-swiftui-and-state-management-part-3
• https://www.pointfree.co/episodes/ep80-the-combine-framework-and-effects-part-1 • https://www.pointfree.co/episodes/ep81-the-combine-framework-and-effects-part-2 • https://www.pointfree.co/episodes/ep85-testable-state-management-the-point • https://www.pointfree.co/episodes/ep86-swiftui-snapshot-testing
Resources / for subscribers • https://www.pointfree.co/episodes/ep68-composable-state-management-reducers • https://www.pointfree.co/episodes/ep69-composable-state-management-state-pullbacks • https://www.pointfree.co/episodes/ep70-composable-state-management-action-pullbacks
• https://www.pointfree.co/episodes/ep71-composable-state-management-higher-order-reducers • https://www.pointfree.co/episodes/ep72-modular-state-management-reducers • https://www.pointfree.co/episodes/ep73-modular-state-management-view-state • https://www.pointfree.co/episodes/ep74-modular-state-management-view-actions • https://www.pointfree.co/episodes/ep75-modular-state-management-the-point • https://www.pointfree.co/episodes/ep76-effectful-state-management-synchronous-effects • https://www.pointfree.co/episodes/ep77-effectful-state-management-unidirectional-effects • https://www.pointfree.co/episodes/ep78-effectful-state-management-asynchronous-effects • https://www.pointfree.co/episodes/ep79-effectful-state-management-the-point • https://www.pointfree.co/episodes/ep82-testable-state-management-reducers • https://www.pointfree.co/episodes/ep83-testable-state-management-effects • https://www.pointfree.co/episodes/ep84-testable-state-management-ergonomics • And more…
None