MVVM with Combine in SwiftUI

MVVM with Combine in SwiftUI

How to actualize MVVM with Combine in SwiftUI (or UIViewController)

Ee4f2266abb0268305431dbbcade59d2?s=128

Akifumi Fukaya

June 13, 2019
Tweet

Transcript

  1. 2.

    About me • Name ◦ Akifumi Fukaya • Company ◦

    Merpay, Inc. (2018/06 ~) • Role ◦ Software Engineer (iOS) • Account ◦ Twitter: @akifumifukaya ◦ Facebook: Akifumi Fukaya ◦ Github: akifumi 2
  2. 4.

    Combine Features • Generic • Type safe • Composition first

    • Request driven https://developer.apple.com/videos/play/wwdc2019/722/ 4
  3. 6.

    Publishers • Declares that a type can transmit a sequence

    of values over time. • Defines how values and errors are produced • Value type -> `Struct` • Allows registration of a `Subscriber` https://developer.apple.com/videos/play/wwdc2019/722/ 6
  4. 7.

    Subscribers • A protocol that declares a type that can

    receive input from publisher. • Receives values and a completion • Reference type -> `Class` https://developer.apple.com/videos/play/wwdc2019/722/ 7
  5. 8.

    Operator • Adopts `Publisher` • Describes a behavior for changing

    values • Subscribes to a `Publisher` (“upstream”) • Sends result to a `Subscriber` (“downstream”) • Value type -> `Struct` https://developer.apple.com/videos/play/wwdc2019/722/ 8
  6. 10.

    What are requirements? • Input text into TextField • Validate

    input text • Separate view and model as ViewModel • Bind view and ViewModel • Content of TextField is two way binding 10
  7. 11.

    Add TextField to ContentView struct ContentView : View { @State

    private var username: String = "" var body: some View { VStack { TextField($username, placeholder: Text("Placeholder"), onEditingChanged: { (changed) in print("onEditingChanged: \(changed)") }, onCommit: { print("onCommit") }) } .padding(.horizontal) } } 11
  8. 12.

    Create ViewModel final class ContentViewModel : BindableObject { var didChange

    = PassthroughSubject<Void, Never>() var username: String = "" { didSet { didChange.send(()) } } } 12
  9. 13.

    Create publisher to validate username final class ContentViewModel : BindableObject

    { ︙ private let usernameSubject = PassthroughSubject<String, Never>() private var validatedUsername: AnyPublisher<String?, Never> { return usernameSubject .debounce(for: 0.5, scheduler: RunLoop.main) // Currently doesn't work .removeDuplicates() .flatMap { (username) -> AnyPublisher<String?, Never> in Publishers.Future<String?, Never> { (promise) in // FIXME: API request if 1...10 ~= username.count { promise(.success(username)) } else { promise(.success(nil)) } } .eraseToAnyPublisher() } .eraseToAnyPublisher() } } 13
  10. 14.

    Send value when username is changed final class ContentViewModel :

    BindableObject { var didChange = PassthroughSubject<Void, Never>() var username: String = "" { didSet { guard oldValue != username else { return } usernameSubject.send(username) didChange.send(()) } } ︙ } 14
  11. 15.

    Create ContentViewModel.StatusText final class ContentViewModel : BindableObject { ︙ struct

    StatusText { let content: String let color: Color } var status: StatusText = StatusText(content: "NG", color: .red) { didSet { didChange.send(()) } } } 15
  12. 16.

    Create onApprear() interface to ViewModel final class ContentViewModel : BindableObject

    { ︙ lazy var onAppear: () -> Void = { [weak self] in _ = self?.validatedUsername .sink(receiveValue: { (value) in if let value = value { self?.username = value } else { print("validatedUsername.receiveValue: Invalid username") } }) // Update StatusText _ = self?.validatedUsername .map { (value) -> StatusText in (value != nil) ? StatusText(content: "OK", color: .green) : StatusText(content: "NG", color: .red) } .sink(receiveValue: { [weak self] (value) in self?.status = value }) } } 16
  13. 17.

    Bind ContentViewModel to ContentView struct ContentView : View { @ObjectBinding

    var viewModel: ContentViewModel var body: some View { VStack { HStack { Text($viewModel.status.value.content) .color($viewModel.status.value.color) Spacer() } TextField($viewModel.username, placeholder: Text("Placeholder"), onEditingChanged: { (changed) in print("onEditingChanged: \(changed)") }, onCommit: { print("onCommit") }) } .padding(.horizontal) .onAppear(perform: viewModel.onAppear) } } 17
  14. 18.
  15. 19.

    Wrap up • Introduction of Combine • Created sample codes

    using SwiftUI, Combine & MVVM as one of the architecture ideas • Currently, some features does not work ◦ NotificationCenter ◦ Schedule ◦ @Published ◦ Not implemented some features will be released in next version • Let’s discuss about best practice of SwiftUI architecture • Samples ◦ SwiftUI: https://github.com/akifumi/mvvm-with-combine-in-swiftui ◦ UIViewController: https://github.com/akifumi/mvvm-with-combine-in-uiviewcontroller 19
  16. 20.

    Related sessions • Introducing Combine ◦ https://developer.apple.com/videos/play/wwdc2019/722/ • Combine in

    Practice ◦ https://developer.apple.com/videos/play/wwdc2019/721/ • Data Flow Through SwiftUI ◦ https://developer.apple.com/videos/play/wwdc2019/226/ 20