Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Thinking about Architecture for SwiftUI

d_date
January 30, 2020

Thinking about Architecture for SwiftUI

2020/01/30 CA.swift

d_date

January 30, 2020
Tweet

More Decks by d_date

Other Decks in Programming

Transcript

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

    • Google Developers Expert for Firebase • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ
  2. The Age of Declarative UI • iOS: SwiftUI • Android:

    Jetpack Compose • ReactNative / Flutter
  3. 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
  4. 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
  5. 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
  6. 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 } }
  7. 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
  8. 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
  9. 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
  10. 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) } } } } }
  11. 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) } } } } }
  12. 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 $
  13. 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
  14. User Interaction SwiftUI Action State Mutation View Updates Render !

    " ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/
  15. 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 }
  16. 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
  17. 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() } }
  18. Redux • Unidirectional ( View -> Action -> Store ->

    Reducer -> State -> View) • Single Store • n-State / n-Action • Mutate state in reducer
  19. Composable Architecture • Redux + Elm Architecture • Optimize for

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

    = self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }
  24. 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)) } }
  25. 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)
  26. 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 [] }
  27. 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)) } } } }
  28. Send action public func send(_ action: Action) { let effects

    = self.reducer(&self.value, action) effects.forEach { effect in effect.run(self.send) } }
  29. 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)
  30. 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)
  31. 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
  32. 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…