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.5k
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
1.9k
waiwai-swiftpm-part2
d_date
3
610
わいわいSwift PM part 1
d_date
2
490
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
770
Integrate your app to modern world
d_date
2
780
Other Decks in Programming
See All in Programming
Intent as Code
shoppingjaws
6
1.1k
世界の中心で、AI(App Intents)をさけぶ ー App Intents中心設計の実践ガイド
touyou
0
700
Are APIs Still Relevant in the AI Era?
soyuka
0
330
難しいけど、読めた。- OSSの入口に立った話。
sts11142
0
130
GitHubハンズオン講座 — 実務レベルのチーム開発のフローを身につけよう
junhat6
0
110
SONY CISC-NEWS NWS-1750 + NWB-225 フレームバッファの NetBSD/news68k ドライバ実装 / OSC2026Hiroshima
tsutsui
0
150
WebRTC映像をAirPlayに対応させる挑戦.pdf
monolithic_adam
0
320
UnityでSystem.Net.WebSocketsなWebSocketサーバが動かないのでUnity Monoのコードを覗いてみた / about implementing websocket server with unity mono
drumath2237
1
450
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
270
TiDB Cloudのカスタムコントローラーによるオートスケール対応
takaidohigasi
0
140
ソニーのクラウド共通基盤の変遷とAI時代の開発スタイルに合わせた進化 / The Journey of Sony’s Common Cloud Platform and Its Evolution for AI-Native Development
kenjiyoneyama
0
160
モデルのリファクタリングが難しいと思ったら、そもそも複雑だったのはビジネス仕様だった ? / is-the-business-domain-the-real-complexity
hatsu38
0
420
Featured
See All Featured
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
290
Chasing Engaging Ingredients in Design
codingconduct
0
340
Building a Scalable Design System with Sketch
lauravandoore
464
34k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.6k
The untapped power of vector embeddings
frankvandijk
2
1.9k
Are puppies a ranking factor?
jonoalderson
2
4k
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
570
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
540
Information Architects: The Missing Link in Design Systems
soysaucechin
1
1.2k
The Curious Case for Waylosing
cassininazir
1
540
Building Applications with DynamoDB
mza
96
7.2k
Test your architecture with Archunit
thirion
2
2.4k
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