Save 37% off PRO during our Black Friday Sale! »

MVVM with Combine

MVVM with Combine

MVVM with Combine @ Combineゴリゴリキャッチアップ会 20190805

Ee4f2266abb0268305431dbbcade59d2?s=128

Akifumi Fukaya

August 05, 2019
Tweet

Transcript

  1. MVVM with Combine @akifumi

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

    Merpay, Inc. (2018/06 ~) • Role ◦ Software Engineer (iOS) • Account ◦ Twitter: @akifumifukaya ◦ Facebook: Akifumi Fukaya ◦ Github: akifumi 2
  3. What is Combine? https://developer.apple.com/documentation/combine 3

  4. Combine Features • Generic • Type safe • Composition first

    • Request driven https://developer.apple.com/videos/play/wwdc2019/722/ 4
  5. Key Concepts • Publishers • Subscribers • Operators https://developer.apple.com/videos/play/wwdc2019/722/ 5

  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
  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
  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
  9. How to actualize MVVM with Combine https://github.com/akifumi/mvvm-with-combine-in-swiftui/blob/master/CombineSample/ContentView.swift 9

  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
  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
  12. Create ViewModel (~ Xcode 11 beta 4) final class ContentViewModel

    : BindableObject { var willChange = PassthroughSubject<Void, Never>() var username: String = "" { didSet { willChange.send(()) } } } 12
  13. BinableObject (deprecated) /// A type of object that serves notifies

    the framework when changed. @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @available(*, deprecated, message: "Conform to Combine.ObservableObject and Swift.Identifiable") public protocol BindableObject : ObservableObject, Identifiable { /// A type that publishes an event when the object has changed. associatedtype PublisherType : Publisher where Self.PublisherType.Failure == Never /// An instance that publishes an event immediately before the /// object changes. /// /// A `View`'s subhierarchy is forcibly invalidated whenever /// the `willChange` of its `model` publishes an event. var willChange: Self.PublisherType { get } } 13
  14. Combine.ObservableObject @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)

    public protocol ObservableObject : AnyObject { /// The type of publisher that emits before the object has changed. associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never /// A publisher that emits before the object has changed. var objectWillChange: Self.ObjectWillChangePublisher { get } } 14
  15. Swift.Identifiable /// A class of types whose instances hold the

    value of an entity with stable identity. @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol Identifiable { /// A type representing the stable identity of the entity associated with `self`. associatedtype ID : Hashable /// The stable identity of the entity associated with `self`. var id: Self.ID { get } } @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) extension Identifiable where Self : AnyObject { /// The stable identity of the entity associated with `self`. public var id: ObjectIdentifier { get } } 15
  16. Create ViewModel (Xcode 11 Beta 5 ~) final class ContentViewModel

    : ObservableObject, Identifiable { var objectWillChange = PassthroughSubject<Void, Never>() @Published var username: String = "" { didSet { objectWillChange.send(()) } } } 16
  17. Create publisher to validate username final class ContentViewModel : ObservableObject,

    Identifiable { ︙ @Published var username: String = "" { … } private var validatedUsername: AnyPublisher<String?, Never> { return $username .debounce(for: 0.1, scheduler: RunLoop.main) .removeDuplicates() .flatMap { (username) -> AnyPublisher<String?, Never> in Future<String?, Never> { (promise) in // FIXME: API request if 1...10 ~= username.count { promise(.success(username)) } else { promise(.success(nil)) } } .eraseToAnyPublisher() } .eraseToAnyPublisher() } } 17
  18. Create publisher to validate username let publisher = $username .debounce(for:

    0.1, scheduler: RunLoop.main) .removeDuplicates() .flatMap { (username) -> AnyPublisher<String?, Never> in Future<String?, Never> { (promise) in // FIXME: API request if 1...10 ~= username.count { promise(.success(username)) } else { promise(.success(nil)) } } .eraseToAnyPublisher() } // Publishers.FlatMap<AnyPublisher<String?, Never>, Publishers.RemoveDuplicates<Publishers.Debounce<Published<String>.Publisher, RunLoop>>> publisher 18
  19. Create publisher to validate username final class ContentViewModel : ObservableObject,

    Identifiable { ︙ @Published var username: String = "" { … } private var validatedUsername: AnyPublisher<String?, Never> { return $username .debounce(for: 0.1, scheduler: RunLoop.main) .removeDuplicates() .flatMap { (username) -> AnyPublisher<String?, Never> in Future<String?, Never> { (promise) in // FIXME: API request if 1...10 ~= username.count { promise(.success(username)) } else { promise(.success(nil)) } } .eraseToAnyPublisher() } .eraseToAnyPublisher() } } 19
  20. Create ContentViewModel.StatusText final class ContentViewModel : ObservableObject, Identifiable { ︙

    struct StatusText { let content: String let color: Color } @Published var status: StatusText = StatusText(content: "NG", color: .red) { didSet { objectWillChange.send(()) } } } 20
  21. Create onApprear() interface to ViewModel final class ContentViewModel : ObservableObject,

    Identifiable { ︙ private var cancellables: [AnyCancellable] = [] lazy var onAppear: () -> Void = { [weak self] in guard let self = self else { return } let usernameCancellable = self.validatedUsername .sink(receiveValue: { [weak self] (value) in if let value = value { self?.username = value } else { print("validatedUsername.receiveValue: Invalid username") } }) let statusCancellable = self.validatedUsername .map { (value) -> StatusText in return (value != nil) ? StatusText(content: "OK", color: .green) : StatusText(content: "NG", color: .red) } .sink(receiveValue: { [weak self] (value) in self?.status = value }) self.cancellables = [usernameCancellable, statusCancellable] } } 21
  22. Create onDisapprear() interface to ViewModel final class ContentViewModel : ObservableObject,

    Identifiable { ︙ private var cancellables: [AnyCancellable] = [] ︙ lazy var onDisappear: () -> Void = { [weak self] in guard let self = self else { return } self.cancellables.forEach { $0.cancel() } self.cancellables = [] } } 22
  23. Bind ContentViewModel to ContentView struct ContentView : View { @ObservedObject

    var viewModel: ContentViewModel var body: some View { VStack { HStack { Text($viewModel.status.value.content) .foregroundColor($viewModel.status.value.color) Spacer() } TextField("Placeholder", text: $viewModel.username, onEditingChanged: { (changed) in print("onEditingChanged: \(changed)") }, onCommit: { print("onCommit") }) } .padding(.horizontal) .onAppear(perform: viewModel.onAppear) .onDisappear(perform: viewModel.onDisappear) } } 23
  24. Demo

  25. Wrap up • Introduction of Combine • Created sample codes

    using SwiftUI, Combine & MVVM as one of the architecture ideas • APIs of Combine still has (breaking) changes ◦ BindableObject and @ObjectBinding are deprecated • Let’s discuss about best practice of SwiftUI architecture • Documents ◦ Qiita: https://qiita.com/akifumi1118/items/aa5734b1f14d57072456 ◦ SwiftUI: https://github.com/akifumi/mvvm-with-combine-in-swiftui ◦ UIViewController: https://github.com/akifumi/mvvm-with-combine-in-uiviewcontroller 25
  26. 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/ 26
  27. Thank you for your attention.