Thinking about Architecture for SwiftUI

2594ac7ce91fd7d9a3ce71ca7cc2d0c0?s=47 d_date
January 30, 2020

Thinking about Architecture for SwiftUI

2020/01/30 CA.swift

2594ac7ce91fd7d9a3ce71ca7cc2d0c0?s=128

d_date

January 30, 2020
Tweet

Transcript

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

    Developer
  2. Daiki Matsudate • Tokyo • iOS Developer from iOS 4

    • Google Developers Expert for Firebase • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ
  3. None
  4. None
  5. March, 18 - 20th, 2020 https://www.tryswift.co/

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

  8. The Age of Declarative UI

  9. The Age of Declarative UI • iOS: SwiftUI • Android:

    Jetpack Compose • ReactNative / Flutter
  10. 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
  11. 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
  12. 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
  13. 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 } }
  14. 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
  15. 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
  16. 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
  17. 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) } } } } }
  18. 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) } } } } }
  19. 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 $
  20. 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
  21. Dataflow

  22. Combine

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

    Reactive Framework by Apple
  24. https://twitter.com/diegopetrucci/status/1135655480825655297

  25. User Interaction SwiftUI Action State Mutation View Updates Render !

    " ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/
  26. 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 }
  27. 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
  28. 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() } }
  29. https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()

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

  31. Unidirectional Dataflow

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

    Architecture
  33. None
  34. None
  35. Redux • Unidirectional ( View -> Action -> Store ->

    Reducer -> State -> View) • Single Store • n-State / n-Action • Mutate state in reducer
  36. None
  37. Example: Composable Architecture

  38. None
  39. https://www.pointfree.co

  40. Composable Architecture • Redux + Elm Architecture • Optimize for

    SwiftUI / Combine • Functional • View / State / Action / Store / Reducer • Side Effect has treated as Effect type • Composable
  41. 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)) } }
  42. 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 []
  43. 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)) } } } }
  44. Send action public func send(_ action: Action) { let effects

    = self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }
  45. 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)) } }
  46. Are you using Combine / SwiftUI now?

  47. Can we do same in RxSwift / UIKit now?

  48. 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)
  49. 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 [] }
  50. 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)) } } } }
  51. Send action public func send(_ action: Action) { let effects

    = self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }
  52. 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)
  53. 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)
  54. Why Single Store?

  55. Why Single Store? • Broadcast ALL States • Easily to

    REUSE existing state
  56. 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
  57. 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…
  58. None