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. Thinking about Architecture for
    SwiftUI
    CA.swift
    Daiki Matsudate
    @d_date
    iOS Developer

    View Slide

  2. Daiki Matsudate
    • Tokyo

    • iOS Developer from iOS 4

    • Google Developers Expert for Firebase

    • Book: ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ

    View Slide

  3. View Slide

  4. View Slide

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

    View Slide

  6. View Slide

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

    View Slide

  8. The Age of Declarative UI

    View Slide

  9. The Age of Declarative UI
    • iOS: SwiftUI

    • Android: Jetpack Compose

    • ReactNative / Flutter

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. Dataflow

    View Slide

  22. Combine

    View Slide

  23. Combine
    • Declarative Swift API

    • ඇಉظͳΠϕϯτΛܕͱͯ͠දݱ

    • ଟछଟ༷ͳԋࢉࢠͰΠϕϯτΛϋϯυϦϯά

    • Reactive Framework by Apple

    View Slide

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

    View Slide

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

    Publisher
    https://developer.apple.com/videos/play/wwdc2019/226/

    View Slide

  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
    }

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. Unidirectional Dataflow

    View Slide

  32. Unidirectional Dataflow
    • Flux

    • Redux (ReSwift etc.)

    • Composable Architecture

    View Slide

  33. View Slide

  34. View Slide

  35. Redux
    • Unidirectional ( View -> Action -> Store -> Reducer -> State -> View)

    • Single Store

    • n-State / n-Action

    • Mutate state in reducer

    View Slide

  36. View Slide

  37. Example:
    Composable Architecture

    View Slide

  38. View Slide

  39. https://www.pointfree.co

    View Slide

  40. Composable Architecture
    • Redux + Elm Architecture

    • Optimize for SwiftUI / Combine

    • Functional

    • View / State / Action / Store / Reducer

    • Side Effect has treated as Effect type

    • Composable

    View Slide

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

    View Slide

  42. 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 []

    View Slide

  43. Effect
    public struct Effect {
    public let run: (@escaping (A) -> Void) -> Void
    public init(run: @escaping (@escaping (A) -> Void) -> Void) {
    self.run = run
    }
    public func map(_ f: @escaping (A) -> B) -> Effect {
    return Effect { callback in self.run { a in callback(f(a)) } }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  46. Are you using
    Combine / SwiftUI now?

    View Slide

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

    View Slide

  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)

    View Slide

  49. 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 []
    }

    View Slide

  50. Effect
    public struct Effect {
    public let run: (@escaping (A) -> Void) -> Void
    public init(run: @escaping (@escaping (A) -> Void) -> Void) {
    self.run = run
    }
    public func map(_ f: @escaping (A) -> B) -> Effect {
    return Effect { callback in self.run { a in callback(f(a)) } }
    }
    }

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  54. Why Single Store?

    View Slide

  55. Why Single Store?
    • Broadcast ALL States

    • Easily to REUSE existing state

    View Slide

  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

    View Slide

  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…

    View Slide

  58. View Slide