Slide 1

Slide 1 text

MVVM with Combine @akifumi

Slide 2

Slide 2 text

About me ● Name ○ Akifumi Fukaya ● Company ○ Merpay, Inc. (2018/06 ~) ● Role ○ Software Engineer (iOS) ● Account ○ Twitter: @akifumifukaya ○ Facebook: Akifumi Fukaya ○ Github: akifumi 2

Slide 3

Slide 3 text

What is Combine? https://developer.apple.com/documentation/combine 3

Slide 4

Slide 4 text

Combine Features ● Generic ● Type safe ● Composition first ● Request driven https://developer.apple.com/videos/play/wwdc2019/722/ 4

Slide 5

Slide 5 text

Key Concepts ● Publishers ● Subscribers ● Operators https://developer.apple.com/videos/play/wwdc2019/722/ 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

How to actualize MVVM with Combine https://github.com/akifumi/mvvm-with-combine-in-swiftui/blob/master/CombineSample/ContentView.swift 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Create ViewModel (~ Xcode 11 beta 4) final class ContentViewModel : BindableObject { var willChange = PassthroughSubject() var username: String = "" { didSet { willChange.send(()) } } } 12

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Create ViewModel (Xcode 11 Beta 5 ~) final class ContentViewModel : ObservableObject, Identifiable { var objectWillChange = PassthroughSubject() @Published var username: String = "" { didSet { objectWillChange.send(()) } } } 16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Create publisher to validate username let publisher = $username .debounce(for: 0.1, scheduler: RunLoop.main) .removeDuplicates() .flatMap { (username) -> AnyPublisher in Future { (promise) in // FIXME: API request if 1...10 ~= username.count { promise(.success(username)) } else { promise(.success(nil)) } } .eraseToAnyPublisher() } // Publishers.FlatMap, Publishers.RemoveDuplicates.Publisher, RunLoop>>> publisher 18

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Demo

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Thank you for your attention.