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

MVVM with Combine in SwiftUI

MVVM with Combine in SwiftUI

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

Akifumi Fukaya

June 13, 2019
Tweet

More Decks by Akifumi Fukaya

Other Decks in Technology

Transcript

  1. About me • Name ◦ Akifumi Fukaya • Company ◦

    Merpay, Inc. (2018/06 ~) • Role ◦ Software Engineer (iOS) • Account ◦ Twitter: @akifumifukaya ◦ Facebook: Akifumi Fukaya ◦ Github: akifumi 2
  2. Combine Features • Generic • Type safe • Composition first

    • Request driven https://developer.apple.com/videos/play/wwdc2019/722/ 4
  3. 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. 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. 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. 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. 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. Create ViewModel final class ContentViewModel : BindableObject { var didChange

    = PassthroughSubject<Void, Never>() var username: String = "" { didSet { didChange.send(()) } } } 12
  9. 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. 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. 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. 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. 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. 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
  15. 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