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

MVVM with ReactiveSwift

MVVM with ReactiveSwift

Let's explore how to leverage ReactiveSwift and the MVVM pattern to build a simple iOS application

Avatar for George Kaimakas

George Kaimakas

November 12, 2019
Tweet

Other Decks in Programming

Transcript

  1. MVVM - Intro • Software Architectural Pattern. • Stands for

    Model-View-ViewModel. • Models: raw representation of data from the backend • Views: The actual UI. Views are driven by view models and keep no reference of Models. • ViewModels: Where the magic happens. ViewModels are responsible for state keeping and for driving the views. Other architectural patterns that apply on iOS are: MVC (Model-View-Controller) the default pattern on iOS, sometimes it degrades to Massive View Controller, VIPER
  2. ReactiveSwift - Intro • Streams of values over time. •

    Similar to Rx(Java, Swift, JS, etc) though tailored to Apple’s platforms. • Can be used to uniformly represent common Cocoa and generic programming patterns (delegates, notifications, closures). • Less spaghetti code. • Steep learning curve (though it’s worth it). • Core Concepts: Signal, SignalProducer, Property, Action. (We will be using most of them during this session). Further Reading: 1. https://github.com/ReactiveCocoa/ReactiveSwift 2. https://rxmarbles.com/
  3. ReactiveSwift - Core Concepts • Signal: any series of events

    over time that can be observed. It is generally used to represent streams of values that are already “in progress” like notifications or user input.
  4. ReactiveSwift - Core Concepts • Signal: any series of events

    over time that can be observed. It is generally used to represent streams of values that are already “in progress” like notifications or user input. • SignalProducer: used to represent operations or tasks e.g network requests. When a SignalProducer starts a new task/operation will be created allowing the caller to observe the results.
  5. ReactiveSwift - Core Concepts • Signal: any series of events

    over time that can be observed. It is generally used to represent streams of values that are already “in progress” like notifications or user input. • SignalProducer: used to represent operations or tasks e.g network requests. When a SignalProducer starts a new task/operation will be created allowing the caller to observe the results. • Property: stores a value and notifies observers about future changes to that value. Properties and MutableProperties can act as a bridge between the reactive and the imperative worlds (though most of the time it is better to use signals).
  6. ReactiveSwift - Core Concepts • Signal: any series of events

    over time that can be observed. It is generally used to represent streams of values that are already “in progress” like notifications or user input. • SignalProducer: used to represent operations or tasks e.g network requests. When a SignalProducer starts a new task/operation will be created allowing the caller to observe the results. • Property: stores a value and notifies observers about future changes to that value. Properties and MutableProperties can act as a bridge between the reactive and the imperative worlds (though most of the time it is better to use signals). • Action: will do some work when executed with an input. While executing, zero or more output values and/or a failure may be generated. Actions are useful for performing side-effecting work upon user interaction, like when a button is clicked. Actions can also be automatically disabled based on a property, and this disabled state can be represented in a UI by disabling any controls associated with the action. Further Reading: 1. https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/FrameworkOverview.md
  7. Goals of this presentation 1. Get a grasp of ReactiveSwift.

    2. Learn how to perform requests and parsing responses using ReactiveSwift and generics. 3. Learn how to extend Signals and SignalProducers. 4. Understand ReactiveFeedback and how to create a ViewModel using it. 5. Learn how to create custom bindings. 6. Do this under 45mins.
  8. The App - iOSMeetup GOAL: Create a simple iOS application

    to display a list of Posts. • Posts come from https://jsonplaceholder.typicode.com/ • We want to support pagination
  9. Setting Up - Targets 1. Create project iOSMeetup. 2. Add

    target iOSMeetupCore. 3. Add target iOSMeetupUI. Linking 1. Set iOSMeetupCore as a dependency of iOSMeetupUI. 2. Link iOSMeetupCore to iOSMeetupUI.
  10. Setting up - Pods 1. From you command line run:

    pod init 2. Open the generated Podfile. 3. Add ReactiveCocoa: Useful bindings for UIKit 4. Add ReactiveSwift. 5. Add ReactiveFeedback (more on that later). 6. Run: pod install
  11. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession.
  12. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`.
  13. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift.
  14. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. URLSession.
  15. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. URLSession.shared.
  16. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. URLSession.shared.reactive
  17. iOSMeetup - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. URLSession.shared.reactive.data(with: request)
  18. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. let producer = URLSession.shared.reactive.data(with: request)
  19. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. let producer = URLSession.shared.reactive.data(with: request) This producer though is of type SignalProducer<(Data, URLResponse), Error> and we need posts, not data.
  20. iOSMeetupCore - Requests the Reactive way ReactiveSwift provides out of

    the box affordances for many of the classes we use every day, one such class being URLSession. All this functionality is given to the user under the namespace `reactive`. Let’s see how to fetch some data from an endpoint using URLSession and ReactiveSwift. let producer = URLSession.shared.reactive.data(with: request) This producer though is of type SignalProducer<(Data, URLResponse), Error> and we need posts, not data. What can we do?
  21. MAP

  22. MAP

  23. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift.
  24. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift. Decoding is provider by Swift Standard Library on Decobable objects (Post happens to conform to Codable).
  25. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift. Decoding is provider by Swift Standard Library on Decobable objects (Post happens to conform to Codable). Decoding might also throw errors.
  26. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift. Decoding is provider by Swift Standard Library on Decobable objects (Post happens to conform to Codable). Decoding might also throw errors. map does not handle errors...
  27. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift. Decoding is provider by Swift Standard Library on Decobable objects (Post happens to conform to Codable). Decoding might also throw errors. map does not handle errors… but attemptMap does.
  28. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift. Decoding is provider by Swift Standard Library on Decobable objects (Post happens to conform to Codable). Decoding might also throw errors. map does not handle errors… but attemptMap does. attemptMap takes an input, applies a transformation that might fail and returns an output in the form of Swift.Result<Output, Error>
  29. iOSMeetupCore - Decoding Data with attemptMap map takes an input,

    applies a transformation and returns the resulting output. It also happens to be supported by ReactiveSwift. Decoding is provider by Swift Standard Library on Decobable objects (Post happens to conform to Codable). Decoding might also throw errors. map does not handle errors… but attemptMap does. attemptMap takes an input, applies a transformation that might fail and returns an output in the form of Swift.Result<Output, Error> ReactiveSwift intercepts that result, decomposes it and forwards the output or fails with error.
  30. iOSMeetupCore - Decoding Data with attemptMap Since we understand how

    attemptMap works, let’s see how we can use it to get our posts.
  31. iOSMeetupCore - Decoding Data with attemptMap URLSession .shared .reactive .data(with:

    request) .mapError(CoreError.unknown) // Convert errors to our own error enum. .attemptMap { (data, _) -> Result<[Post], CoreError> in do { let decoded = try JSONDecoder().decode([Post].self, from: data) return .success(decoded) // If successful, return the posts. } catch let err { return .failure(.unknown(err)) // If failed, return the error. } }
  32. iOSMeetupCore - Decoding Data with attemptMap That’s passable, but if

    we were to use the same approach for many different requests, it would mean that we would have to right a lot of boilerplate code.
  33. iOSMeetupCore - Decoding Data with attemptMap That’s passable, but if

    we were to use the same approach for many different requests, it would mean that we would have to right a lot of boilerplate code. Can we do better?
  34. iOSMeetupCore - Decoding Data with attemptMap That’s passable, but if

    we were to use the same approach for many different requests, it would mean that we would have to right a lot of boilerplate code. Can we do better? Maybe if we wrote an extension to SignalProducer.
  35. iOSMeetupCore - Decoding Data with attemptMap That’s passable, but if

    we were to use the same approach for many different requests, it would mean that we would have to right a lot of boilerplate code. Can we do better? Maybe if we wrote an extension to SignalProducer. And maybe if we did it in such a way that it will work for every decodable.
  36. iOSMeetupCore - Generic Decoding extension Signal where Value == (Data,

    URLResponse) { func decode<T: Decodable>(_ type: T.Type) -> Signal<T, CoreError> { mapError(CoreError.unknown) .attemptMap { (data, _) -> Result<T, CoreError> in do { let result = try JSONDecoder().decode(type, from: data) return .success(result) } catch let err { return .failure(CoreError.unknown(err)) } } } }
  37. ...

  38. iOSMeetupCore - Generic Decoding But, that extension… extends… Signal.. Introducing

    lift. lift is a handy function that elevates signal operators to work on signal producers.
  39. iOSMeetupCore - Generic Decoding But, that extension… extends… Signal.. Introducing

    lift. lift is a handy function that elevates signal operators to work on signal producers. Which means...
  40. iOSMeetupCore - Generic Decoding But, that extension… extends… Signal.. Introducing

    lift. lift is a handy function that elevates signal operators to work on signal producers. Which means… If we write this...
  41. iOSMeetupCore - Generic Decoding But, that extension… extends… Signal.. Introducing

    lift. lift is a handy function that elevates signal operators to work on signal producers. Which means… If we write this... extension SignalProducer where Value == (Data, URLResponse) { func decode<T: Decodable>(_ type: T.Type) -> SignalProducer<T, CoreError> { lift { $0.decode(type) } } }
  42. iOSMeetupCore - Generic Decoding URLSession.shared.reactive .data(with: request) .mapError(CoreError.unknown) .attemptMap {

    (data, _) -> Result<[Post], CoreError> in do { let decoded = try JSONDecoder().decode([Post].self, from: data) return .success(decoded) } catch let err { return .failure(.unknown(err)) } }
  43. iOSMeetupCore - Generic Decoding URLSession.shared.reactive .data(with: request) .mapError(CoreError.unknown) .decode([Post].self) Which

    we can reuse and apply to every Decodable we might encounter in the future. For this presentation, we won’t need it any more.
  44. iOSMeetupCore - Generic Decoding URLSession.shared.reactive .data(with: request) .mapError(CoreError.unknown) .decode([Post].self) Which

    we can reuse and apply to every Decodable we might encounter in the future. For this presentation, we won’t need it any more. This code, is part of a class called PostProvider, responsible for fetching posts.
  45. iOSMeetupCore - Decoupling One problem we encounter while building stuff,

    is tight coupling. To alleviate this, Swift strongly suggests we use protocols.
  46. iOSMeetupCore - Decoupling One problem we encounter while building stuff,

    is tight coupling. To alleviate this, Swift strongly suggests we use protocols. PostProvider conforms to a protcol, originally named: PostProviderProtocol.
  47. iOSMeetupCore - Decoupling One problem we encounter while building stuff,

    is tight coupling. To alleviate this, Swift strongly suggests we use protocols. PostProvider conforms to a protcol, originally named: PostProviderProtocol. public protocol PostProviderProtocol: class { func fetchPosts(page: Int, limit: Int) -> SignalProducer<[Post], CoreError> }
  48. iOSMeetupCore - Decoupling One problem we encounter while building stuff,

    is tight coupling. To alleviate this, Swift strongly suggests we use protocols. PostProvider conforms to a protcol, originally named: PostProviderProtocol. public protocol PostProviderProtocol: class { func fetchPosts(page: Int, limit: Int) -> SignalProducer<[Post], CoreError> } Every ViewModel that wants to fetch posts, will interact with PostProviderProtocol and not directly with PostProvider.
  49. iOSMeetupUI - ReactiveFeedback Requirements for iOS apps have become huge.

    Our code has to manage a lot of state e.g. server responses, cached data, UI state, routing etc. Some may say that Reactive Programming can help us a lot but, in the wrong hands, it can do even more harm to your code base.
  50. iOSMeetupUI - ReactiveFeedback Requirements for iOS apps have become huge.

    Our code has to manage a lot of state e.g. server responses, cached data, UI state, routing etc. Some may say that Reactive Programming can help us a lot but, in the wrong hands, it can do even more harm to your code base. The goal of this library is to provide a simple and intuitive approach to designing reactive state machines.
  51. iOSMeetupUI - ReactiveFeedback Requirements for iOS apps have become huge.

    Our code has to manage a lot of state e.g. server responses, cached data, UI state, routing etc. Some may say that Reactive Programming can help us a lot but, in the wrong hands, it can do even more harm to your code base. The goal of this library is to provide a simple and intuitive approach to designing reactive state machines. Core Concepts:
  52. iOSMeetupUI - ReactiveFeedback Requirements for iOS apps have become huge.

    Our code has to manage a lot of state e.g. server responses, cached data, UI state, routing etc. Some may say that Reactive Programming can help us a lot but, in the wrong hands, it can do even more harm to your code base. The goal of this library is to provide a simple and intuitive approach to designing reactive state machines. Core Concepts: • State • Event • Reducer • Feedback
  53. ReactiveFeedback - State 1/3 State is the single source of

    truth. It represents a state of your system and is usually a plain Swift type (which doesn't contain any ReactiveSwift primitives). Your state is immutable. The only way to transition from one State to another is to emit an Event. Our viewModel can transition between the following states:
  54. ReactiveFeedback - State 2/3 There are multiple ways to represent

    state: Some that come to mind are the following: 1. Enums 2. Structs / Classes 3. Structs / Clases + Context (enum)
  55. ReactiveFeedback - State 2/3 There are multiple ways to represent

    state: Some that come to mind are the following: 1. Enums 2. Structs / Classes 3. Structs / Clases + Context (enum) If you go with enums, while you gain in readability (you always know which state you are in) you have to carry over too much baggage in the form of associated objects.
  56. ReactiveFeedback - State 2/3 There are multiple ways to represent

    state: Some that come to mind are the following: 1. Enums 2. Structs / Classes 3. Structs / Clases + Context (enum) If you go with enums, while you gain in readability (you always know which state you are in) you have to carry over too much baggage in the form of associated objects. If you opt for simple structs or classes you can have as much baggage but you have to guess the state you are in.
  57. ReactiveFeedback - State 2/3 There are multiple ways to represent

    state: Some that come to mind are the following: 1. Enums 2. Structs / Classes 3. Structs / Clases + Context (enum) If you go with enums, while you gain in readability (you always know which state you are in) you have to carry over too much baggage in the form of associated objects. If you opt for simple structs or classes you can have as much baggage but you have to guess the state you are in. If you opt for a mixed combination, you can have as much baggage as you want while never having to guess your state since you know it from the context. I, obviously opted for #3.
  58. ReactiveFeedback - State 3/3 struct State { static func initial()

    -> State { .init(context: .initial, posts: [], error: nil) } let context: Context let posts: [Post] let error: CoreError? } enum Context: Equatable { case initial case loading case loaded } Post, CoreError and Context all conform to Equatable it is pretty trivial for State to conform to Equatable allowing it to be encoded to data and with minimal effort to be used for state restoration. State also provides more functionality which is pretty trivial and not needed for this presentation.
  59. ReactiveFeedback - Event Represents all possible events that can happen

    in your system which can cause a transition to a new State.
  60. ReactiveFeedback - Event Represents all possible events that can happen

    in your system which can cause a transition to a new State. Our viewModel (will) use the following events: public enum Event { case fetchPosts case fetchPostsSucceeded([Post]) case fetchPostsFailed(CoreError) }
  61. ReactiveFeedback - Reducer 1/3 A Reducer is a pure function

    with a signature of (State, Event) -> State. While Event represents an action that might result in a State change, it's actually not what causes the change. An Event is just that, a representation of the intention to transition from one state to another. What actually causes the State to change, the embodiment of the corresponding Event, is a Reducer. A Reducer is the only place where a State can be changed. Let’s see what we want to achieve with our reducer:
  62. ReactiveFeedback - Reducer 3/3 public static func reduce(state: State, event:

    Event) -> State { switch (state.context, event) { case (.initial, .fetchPosts): return state.with(context: .loading) case (.loading, let .fetchPostsSucceeded(posts)): return state.with(context: .loaded).with(posts: posts) case (.loading, let .fetchPostsFailed(err)): return state.with(context: .loaded).with(error: err) case (.loaded, .fetchPosts): return state.with(context: .loading) default: return state } }
  63. ReactiveFeedback - Feedback 1/3 While State represents where the system

    is at a given time, Event represents a trigger for state change, and a Reducer is the pure function that changes the state depending on current state and type of event received, there is not as of yet any type to emit events given a particular current state. That's the job of the Feedback. It's essentially a "processing engine", listening to changes in the current State and emitting the corresponding next events to take place. Feedbacks don't directly mutate states. Instead, they only emit events which then cause states to change in reducers.
  64. ReactiveFeedback - Feedback 2/3 While State represents where the system

    is at a given time, Event represents a trigger for state change, and a Reducer is the pure function that changes the state depending on current state and type of event received, there is not as of yet any type to emit events given a particular current state. That's the job of the Feedback. It's essentially a "processing engine", listening to changes in the current State and emitting the corresponding next events to take place. Feedbacks don't directly mutate states. Instead, they only emit events which then cause states to change in reducers. Our viewModel uses the following feedbacks: 1. whenExecutingAction 2. whenFetchingPosts
  65. ReactiveFeedback - Feedback 2/3 static func whenExecutingAction(_ signal: Signal<Action, Never>)

    -> Feedback<State, Event> { .init(predicate: { $0.context != Context.loading }) { _ -> Signal<Event, Never> in signal .map { action -> Event in switch action { case .fetchPosts: return .fetchPosts } } } }
  66. OK

  67. iOSMeetupUI - Bringing it all together While events describe the

    intent to transition to a new state, there is now way to inject events in out FSM. This is where Action comes to play. Action is a simple enum that describes a user action and might be mapped to an event. enum Action { case fetchPosts } For simple apps as this one, it makes no sense to introduce another enum for event injection, but it comes in handy when building complex flows
  68. ReactiveFeedback - Feedback 3/3 static func whenFetchingPosts(_ postProvider: PostProviderProtocol) ->

    Feedback<State, Event> { .init(skippingRepeated: { $0 }) { state -> SignalProducer<Event, Never> in guard case state.context = Context.loading else { return .empty } return postProvider .fetchPosts(page: state.posts.count / 20 + 1, limit: 20) .map(Event.fetchPostsSucceeded) .flatMapError { SignalProducer<Event, Never>(value: .fetchPostsFailed($0)) } } }
  69. iOSMeetupUI - Bringing it all together As we stated earlier,

    State is the single source of truth for our ViewModel.
  70. iOSMeetupUI - Bringing it all together As we stated earlier,

    State is the single source of truth for our ViewModel. Since State will be changing as our ViewModel transitions from state to state, we need some way to observe the changes… both synchronously... and asynchronously.
  71. iOSMeetupUI - Bringing it all together As we stated earlier,

    State is the single source of truth for our ViewModel. Since State will be changing as our ViewModel transitions from state to state, we need some way to observe the changes… both synchronously... and asynchronously. ReactiveSwift’s Property comes to the rescue.
  72. iOSMeetupUI - Bringing it all together As we stated earlier,

    State is the single source of truth for our ViewModel. Since State will be changing as our ViewModel transition from state to state, we need some way to observe the changes… both synchronously... and asynchronously. ReactiveSwift’s Property comes to the rescue. And as it happens, ReactiveFeedback provides an initialiser that can bring everything we discussed together.
  73. iOSMeetupUI - Bringing it all together As we stated earlier,

    State is the single source of truth for our ViewModel. Since State will be changing as our ViewModel transition from state to state, we need some way to observe the changes… both synchronously... and asynchronously. ReactiveSwift’s Property comes to the rescue. And as it happens, ReactiveFeedback provides an initialiser that can bring everything we discussed together. Let’s take a look
  74. iOSMeetupUI - Bringing it all together let state: Property<State> let

    (action, actionObserver) = Signal<Action, Never>.pipe() let posts = Property<[PostRow]> ... public init(postProvider: PostProviderProtocol) { let feedbacks = [ Feedbacks.whenExecutingAction(action), Feedbacks.whenFetchingPosts(postProvider) ] state = .init(initial: .initial(), scheduler: QueueScheduler.main, reduce: Self.reduce, feedbacks: feedbacks) posts = state.map(\.posts).map { $0.map(PostRow.init) } }
  75. iOSMeetupUI - Executing Actions public init(postProvider: PostProviderProtocol) { lifetimeToken =

    .init() lifetime = .init(lifetimeToken) ... } public func execute(action: Action) { actionObserver.send(value: action) }
  76. What if I want to bind a Signal or SignalProducer

    or Property to execute(action:)? Binding is the process of updating the target's value to the latest value sent by the source. Operator: <~
  77. iOSMeetupUI - Executing Actions let lifetineToken: Lifetime.Token let lifetime =

    Lifetime public init(postProvider: PostProviderProtocol) { lifetimeToken = .init() lifetime = .init(lifetimeToken) ... } public func execute(action: Action) { actionObserver.send(value: action) } public func bindExecute() -> BindingTarget<Action> { .init(lifetime: lifetime, action: execute(action:)) }
  78. UITableView + Reactive On iOS13, Apple introduced UITableViewDiffableDataSource, a class

    that provides Diffing for UITableViews. As stated in the start of this presentation, we will be using the reactive namespace to provide our custom functionality. But before that, let’s take a look at the UITableViewDiffableDataSource’s signature:
  79. UITableView + Reactive To extend reactive, we have to constrain

    Reactive (the struct that actually provides the namespace) to something, but since UITableViewDiffableDataSource is generic we can’t do right away. First, we have to conform UITableViewDiffableDataSource to the following protocol: protocol BindableDataSource: NSObject { associatedtype SectionIdentifier: Hashable associatedtype ItemIdentifier: Hashable func apply(_ snaphsot: [(SectionIdentifier, [ItemIdentifier])], animatingDifferences: Bool, completion: (() -> Void)?) }
  80. UITableView + Reactive Then, we have to provide a default

    implementation for apply: func apply(_ snaphsot: [(SectionIdentifier, [ItemIdentifier])], animatingDifferences: Bool = true, completion: (() -> Void)?) { var snap = NSDiffableDataSourceSnapshot<SectionIdentifier, ItemIdentifier>() for entry in snaphsot { snap.appendSections([entry.0]) snap.appendItems(entry.1, toSection: entry.0) } self.apply(snap, animatingDifferences: animatingDifferences, completion: completion) }
  81. UITableView + Reactive And finally, extend Reactive public extension Reactive

    where Base: BindableDataSource { func apply(animatingDifferences: Bool = true) -> BindingTarget<[(Base.SectionIdentifier, [Base.ItemIdentifier])]> { makeBindingTarget { (dataSource, snapshot) in dataSource.apply(snapshot, animatingDifferences: animatingDifferences, completion: nil) } } }
  82. iOSMeetup - Updating TableView Which, in turn, allows us to

    do the following in our view controller: fileprivate func bindDataSource() { dataSource.reactive.apply(animatingDifferences: true) <~ viewModel .posts .map(rowsForDataSource) .producer .observe(on: UIScheduler()) } rowsForDataSource is a helper function that maps our posts into rows for our table view.