Last active
March 28, 2019 09:43
-
-
Save marty-suzuki/a462cf54d4b07f3c22732ffec58d26a8 to your computer and use it in GitHub Desktop.
Logic of Unidirectional Input / Output ViewModel Sample with Swift KeyPath (Sample logic of https://github.com/cats-oss/Unio)
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
// This sample works on Swift4.2 and Swift5! | |
// Let's try to execute this sample with Playground! | |
import Foundation | |
// - MARK: Artificially RxSwift classes | |
public enum Rx { | |
public final class Observable<E> {} | |
public final class PublishRelay<E> { | |
public func accept(_ element: E) { print("accept called with \"\(element)\"") } | |
public func asObservable() -> Observable<E> { return Observable() } | |
} | |
public final class BehaviorRelay<E> { | |
public let value: E | |
public init(value: E) { self.value = value } | |
public func accept(_ element: E) { print("accept called with \"\(element)\"") } | |
public func asObservable() -> Observable<E> { return Observable() } | |
} | |
} | |
// - MARK: Represents a Framework | |
/// Represents Input | |
public protocol InputType {} | |
/// Represents Output | |
public protocol OutputType {} | |
public enum Framework { | |
/// Wraps Input or Input to add access limitation | |
public final class Relay<T> { | |
private let dependency: T | |
private init(dependency: T) { | |
self.dependency = dependency | |
} | |
} | |
/// Dependencies to generate Output | |
/// | |
/// - note: It is initializable only in Framework | |
public final class OutputDependency<T: InputType> { | |
private let input: T | |
internal init(_ input: T) { | |
self.input = input | |
} | |
public func observable<U: PublishRelayType>(for keyPath: KeyPath<T, U>) -> Rx.Observable<U.E> { | |
return input[keyPath: keyPath].asObservable() | |
} | |
} | |
/// Base ViewModel | |
open class ViewModel<Input: InputType, Output: OutputType> { | |
public let input: Relay<Input> | |
public let output: Relay<Output> | |
public init(input: Input, output: (OutputDependency<Input>) -> Output) { | |
self.input = Relay(input) | |
let dependency = OutputDependency(input) | |
self.output = Relay(output(dependency)) | |
} | |
} | |
} | |
extension Framework.Relay where T: InputType { | |
/// Be able to initialize if T is InputType | |
public convenience init(_ dependency: T) { | |
self.init(dependency: dependency) | |
} | |
/// Be able to accept an element via KeyPath | |
public func accept<U: PublishRelayType>(_ element: U.E, for keyPath: KeyPath<T, U>) { | |
return dependency[keyPath: keyPath].accept(element) | |
} | |
} | |
extension Framework.Relay where T: OutputType { | |
/// Be able to initialize if T is OutputType | |
public convenience init(_ dependency: T) { | |
self.init(dependency: dependency) | |
} | |
/// Be able to access a value via KeyPath | |
public func value<U: BehaviorRelayType>(for keyPath: KeyPath<T, U>) -> U.E { | |
return dependency[keyPath: keyPath].value | |
} | |
/// Be able to access a observable via KeyPath | |
public func observable<U: PublishRelayType>(for keyPath: KeyPath<T, U>) -> Rx.Observable<U.E> { | |
return dependency[keyPath: keyPath].asObservable() | |
} | |
} | |
// - MARK: Relay Types are implemented in Framework | |
public protocol PublishRelayType { | |
associatedtype E | |
func accept(_ element: E) | |
func asObservable() -> Rx.Observable<E> | |
} | |
extension Rx.PublishRelay: PublishRelayType {} | |
public protocol BehaviorRelayType: PublishRelayType { | |
var value: E { get } | |
} | |
extension Rx.BehaviorRelay: BehaviorRelayType {} | |
// - MARK: Represents Application | |
enum Application { | |
/// Extends Framework.ViewModel | |
/// | |
/// - note:Generates Relay<Input> and Relay<Output> automatically at super class, and retains them as properties. | |
final class SubViewModel: Framework.ViewModel<SubViewModel.Input, SubViewModel.Output> { | |
init() { | |
super.init(input: Input(), output: { dependecy in | |
// Resolves dependencies of Output here. | |
print(dependecy.observable(for: \.buttonTap)) | |
return Output(isButtonEnabled: Rx.BehaviorRelay(value: true), | |
buttonText: Rx.BehaviorRelay(value: "text")) | |
}) | |
} | |
} | |
} | |
extension Application.SubViewModel { | |
/// - note: Properties are internal access level, but ViewModel doesn't have a property of Input. | |
/// ViewModel has wrapped Input `Relay<Input>`, so Input doesn' t be accessible directly from outside of ViewModel. | |
struct Input: InputType { | |
let buttonTap = Rx.PublishRelay<Void>() | |
let searchText = Rx.PublishRelay<String?>() | |
} | |
/// - note: Properties are internal access level, but ViewModel doesn't have a property of Output. | |
/// ViewModel has wrapped Output as `Relay<Output>`, so Output doesn' t be accessible directly from outside of ViewModel. | |
struct Output: OutputType { | |
let isButtonEnabled: Rx.BehaviorRelay<Bool> | |
let buttonText: Rx.BehaviorRelay<String> | |
} | |
} | |
// - MARK: main | |
func main() { | |
let viewModel = Application.SubViewModel() | |
viewModel.input.accept((), for: \.buttonTap) | |
viewModel.input.accept("input accepts this text", for: \.searchText) | |
print(viewModel.output.observable(for: \.isButtonEnabled)) | |
print(viewModel.output.value(for: \.isButtonEnabled)) | |
} | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment