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
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
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
Optunaを作ったときにやりたかったこと、その後 (2026/10/16, Optuna Meetup #3)
iwiwi
1
430
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
180
AWSに止められる覚悟してますか?
morizo_1984
2
530
AWS CDKのカスタムリソースでContinuum(旧Security Agent)を実装した話 | Implementing Continuum (formerly Security Agent) via AWS CDK Custom Resource
akihisaikeda
2
150
AI主導のプログラミング学習アプリを作ってみた
econanringo
0
180
[ハンズオン]AIへの指示だけで「五目並べ」を作ってみよう
satoshi256kbyte
1
380
なぜCTOを降りてFDEを選んだのか?〜なぜプロダクト企業がFDEで顧客の現場に踏み込むのか〜
gonta
1
210
KiroのSpecで「五目並べ」を作ってみる
satoshi256kbyte
1
380
AIで開発が速くなった先で、開発チームは何を目指すか
shihochan
0
880
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
220
Domain-Driven Transformation
hschwentner
2
2.3k
Agents on Rails - Rails at Scale 2026
irinanazarova
0
560
Featured
See All Featured
Embracing the Ebb and Flow
colly
88
5.2k
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
68
58k
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
10
1.4k
How to build a perfect <img>
jonoalderson
1
6.1k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.7k
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1.3k
A Soul's Torment
seathinner
8
3.8k
Kristin Tynski - Automating Marketing Tasks With AI
techseoconnect
PRO
0
550
Git: the NoSQL Database
bkeepers
PRO
433
67k
Done Done
chrislema
187
17k
A Modern Web Designer's Workflow
chriscoyier
699
190k
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