Skip to content

Instantly share code, notes, and snippets.

@BrunoCerberus
Last active August 2, 2023 04:46
Show Gist options
  • Save BrunoCerberus/6aaf190041434bf258bc76fce0105cf9 to your computer and use it in GitHub Desktop.
Save BrunoCerberus/6aaf190041434bf258bc76fce0105cf9 to your computer and use it in GitHub Desktop.
Unidirectional Data Flow Architecture
import Combine
import Foundation
/*
+----------------+ +------------------------+ +-------------------+
| View | ----> | ViewModel | ----> | Interactor |
| | | | | |
| ViewEvents | <---- | ViewState DomainMap | <---- | DomainState |
+----------------+ +------------------------+ +-------------------+
^ |
| v
| +---------------------------+
+----| ProductsViewStateReducing |
+---------------------------+
*/
// We should only be relying on one published property per domain stack -
// adding @Published and @ObservableObject to things like view states or domain states
// breaks the unidirectional data flow architecture and results in multiple sources of truth for your view.
// ViewModel class for managing the state and interaction of the Products view.
final public class ProductsViewModel: CombineViewModel {
// The view's current state, which is published to update the UI.
@Published public var viewState: ProductsViewState
// Interactor to handle domain actions and states.
private var interactor: AnyCombineInteractorNoError<ProductsDomainAction, ProductsDomainState>
// Reducer to transform domain states into view states.
private let reducer: ProductsViewStateReducing
// Event-action mapping to convert view events into domain actions.
private let domainMap: ProductsEventActionMap
// Subject to capture view events from the UI.
private let viewEvents = PassthroughSubject<ProductsViewEvent, Never>()
// A publisher that maps view events to domain actions and shares the resulting domain state.
private lazy var domainState: AnyPublisher<ProductsDomainState, Never> =
viewEvents.eraseToAnyPublisher()
.map(domainMap.action(from:))
.interact(with: interactor)
.share()
.eraseToAnyPublisher()
// Initializes the ViewModel with initial state, interactor, reducer, and event-action mapping.
public init(viewState: ProductsViewState = .loading,
interactor: AnyCombineInteractorNoError<ProductsDomainAction, ProductsDomainState>,
reducer: ProductsViewStateReducing,
domainMap: ProductsEventActionMap) {
self.viewState = viewState
self.interactor = interactor
self.reducer = reducer
self.domainMap = domainMap
startViewState() // Initiates the view state flow.
}
// Sets up the view state flow by subscribing to domainState and updating viewState accordingly.
private func startViewState() {
domainState
.map(reducer.reduce(domainState:)) // Transforms domain state to view state.
.compactMap { $0 } // Removes any nil view states.
.receive(on: DispatchQueue.main) // Ensures UI updates are on the main thread.
.assign(to: &$viewState) // Assigns the latest view state to $viewState.
}
// Function to send view events to the ViewModel.
public func sendViewEvent(_ event: ProductsViewEvent) {
viewEvents.send(event) // Sends the view event to the viewEvents subject.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment