Last active
February 28, 2020 12:30
-
-
Save bitomule/3be8aadf6b1a116f0cba5dd1c74769fe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 1. We need to define the screen "global" state | |
struct ParentState: Equatable { | |
var view1State: [String] | |
var view2State: Int | |
} | |
enum ParentActions { | |
case view1(View1Actions) | |
case view2(View2Actions) | |
} | |
// This should be generated using sourcery. Wee need it to have enum keypaths | |
extension ParentActions { | |
var view1: View1Actions? { | |
get { | |
guard case let .view1(value) = self else { return nil } | |
return value | |
} | |
set { | |
guard case .view1 = self, let newValue = newValue else { return } | |
self = .view1(newValue) | |
} | |
} | |
var view2: View2Actions? { | |
get { | |
guard case let .view2(value) = self else { return nil } | |
return value | |
} | |
set { | |
guard case .view2 = self, let newValue = newValue else { return } | |
self = .view2(newValue) | |
} | |
} | |
} | |
// 2. Then we define local view state and local actions | |
enum View1Actions { | |
case addName | |
} | |
enum View2Actions { | |
case increment | |
} | |
// 3. We create reducers. We can have as many as we can and compose from local to global | |
func test1Reducer(state: inout [String], action: View1Actions) { | |
switch action { | |
case .addName: | |
state.append("Kurwa") | |
} | |
} | |
func test2Reducer(state: inout Int, action: View2Actions) { | |
switch action { | |
case .increment: | |
state += 1 | |
} | |
} | |
// 4. We create one single reducer for the screen. In this case I'm merging 2 reducers but I could be merging 2 reducers of combined reducers | |
let parentReducer: (inout ParentState, ParentActions) -> Void = combine( | |
pullback(test1Reducer, value: \.view1State, action: \.view1), | |
pullback(test2Reducer, value: \.view2State, action: \.view2) | |
) | |
// 5. We create the parent VC. WE KILL THE PRESENTER. | |
// Now the ViewController will be in charge of the store | |
// There's a lot of missing pieces here for cases like search and navigation | |
// Can´t create real UIKit because macOS playground but imagine this is a UIViewController | |
class ParentViewController { | |
// This may be injected | |
var store = Store<ParentState,ParentActions>(initialValue: ParentState(view1State: [], view2State: 0), reducer: parentReducer) | |
// Reference only for debugging this test | |
var childVC: Child1ViewController? | |
init() { | |
// Setup all the components layout. This parent controller is just a container of components | |
addTopView() | |
} | |
func addTopView() { | |
// 6. When we setup the view we build all the related vc and add them as childs seting up the view layout | |
childVC = Child1ViewController(store: store.view(value: { $0.view1State }, action: { .view1($0) })) | |
// We add the childVC into the view hierarchy | |
} | |
} | |
// 7. Each child VC has an store | |
class Child1ViewController { | |
var store: Store<[String],View1Actions> | |
private var disposeBag = DisposeBag() | |
lazy var view: View1 = { | |
return View1(delegate: self) | |
}() | |
init(store: Store<[String],View1Actions>) { | |
self.store = store | |
store.observable.bindToView(configure: view.configure, with: disposeBag) | |
} | |
} | |
// 8. Implements a view delegate so the UIKit view just sends inputs to the VC | |
extension Child1ViewController: View1Delegate { | |
func didTap() { | |
store.send(.addName) | |
} | |
} | |
protocol View1Delegate { | |
func didTap() | |
} | |
// Fake UIView | |
class View {} | |
// 9. We bind the view to the store observer calling the single view model entry point | |
extension ObservableType { | |
func bindToView(configure: @escaping (Element) -> Void, with disposeBag: DisposeBag) { | |
subscribe ({ e in | |
switch e { | |
case let .next(value): | |
configure(value) | |
case .error, .completed: | |
break | |
} | |
}).disposed(by: disposeBag) | |
} | |
} | |
// The view is 100% independent of any of the new features. It's a UIView with single viewmodel entry point and a delegate for input | |
class View1 { | |
typealias ViewModel = [String] | |
let delegate: View1Delegate | |
init(delegate: View1Delegate) { | |
self.delegate = delegate | |
} | |
func configure(viewModel: [String]) { | |
print(viewModel) | |
} | |
func uiDidTap() { | |
delegate.didTap() | |
} | |
} | |
// TEST TEST TEST | |
let parent = ParentViewController() | |
// Fake user behaviour tapping on view elements | |
parent.childVC?.view.uiDidTap() | |
parent.childVC?.view.uiDidTap() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment