Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marty-suzuki/a462cf54d4b07f3c22732ffec58d26a8 to your computer and use it in GitHub Desktop.
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 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