Slide 1

Slide 1 text

Thinking about Architecture for SwiftUI CA.swift Daiki Matsudate @d_date iOS Developer

Slide 2

Slide 2 text

Daiki Matsudate • Tokyo • iOS Developer from iOS 4 • Google Developers Expert for Firebase • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

March, 18 - 20th, 2020 https://www.tryswift.co/

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

https://twitter.com/ios_memes/status/1174273871983370240?s=21

Slide 8

Slide 8 text

The Age of Declarative UI

Slide 9

Slide 9 text

The Age of Declarative UI • iOS: SwiftUI • Android: Jetpack Compose • ReactNative / Flutter

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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 } }

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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) } } } } }

Slide 18

Slide 18 text

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) } } } } }

Slide 19

Slide 19 text

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 Without $ With $

Slide 20

Slide 20 text

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 Without $ With $ cf. RxSwift.BehaviorRelay

Slide 21

Slide 21 text

Dataflow

Slide 22

Slide 22 text

Combine

Slide 23

Slide 23 text

Combine • Declarative Swift API • ඇಉظͳΠϕϯτΛܕͱͯ͠දݱ • ଟछଟ༷ͳԋࢉࢠͰΠϕϯτΛϋϯυϦϯά • Reactive Framework by Apple

Slide 24

Slide 24 text

https://twitter.com/diegopetrucci/status/1135655480825655297

Slide 25

Slide 25 text

User Interaction SwiftUI Action State Mutation View Updates Render ! " ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/

Slide 26

Slide 26 text

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 }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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() } }

Slide 29

Slide 29 text

https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()

Slide 30

Slide 30 text

https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send() Unidirectional Dataflow

Slide 31

Slide 31 text

Unidirectional Dataflow

Slide 32

Slide 32 text

Unidirectional Dataflow • Flux • Redux (ReSwift etc.) • Composable Architecture

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

Redux • Unidirectional ( View -> Action -> Store -> Reducer -> State -> View) • Single Store • n-State / n-Action • Mutate state in reducer

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

Example: Composable Architecture

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

https://www.pointfree.co

Slide 40

Slide 40 text

Composable Architecture • Redux + Elm Architecture • Optimize for SwiftUI / Combine • Functional • View / State / Action / Store / Reducer • Side Effect has treated as Effect type • Composable

Slide 41

Slide 41 text

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)) } }

Slide 42

Slide 42 text

Handle action in Reducer public func counterReducer(state: inout CounterState, action: CounterAction) -> [Effect] { switch action { case .decrTapped: state.count -= 1 return [] case .incrTapped: state.count += 1 return []

Slide 44

Slide 44 text

Send action public func send(_ action: Action) { let effects = self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }

Slide 45

Slide 45 text

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)) } }

Slide 46

Slide 46 text

Are you using Combine / SwiftUI now?

Slide 47

Slide 47 text

Can we do same in RxSwift / UIKit now?

Slide 48

Slide 48 text

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)

Slide 49

Slide 49 text

Handle action in Reducer func signUpReducer(state: inout SignUpState, action: SignUpAction) -> [Effect] { 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 [] }

Slide 51

Slide 51 text

Send action public func send(_ action: Action) { let effects = self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }

Slide 52

Slide 52 text

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)

Slide 53

Slide 53 text

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)

Slide 54

Slide 54 text

Why Single Store?

Slide 55

Slide 55 text

Why Single Store? • Broadcast ALL States • Easily to REUSE existing state

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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…

Slide 58

Slide 58 text

No content