Skip to content

Instantly share code, notes, and snippets.

@mflint
Created January 26, 2019 13:02
Show Gist options
  • Save mflint/30d70560722ec151202cc89f824da0ee to your computer and use it in GitHub Desktop.
Save mflint/30d70560722ec151202cc89f824da0ee to your computer and use it in GitHub Desktop.
data-driven screen dispatching for iOS
class DeparturesViewController: UIViewController, ViewController {
private var viewModel: DeparturesViewModel!
func accept(_ viewModel: ViewModel) {
self.viewModel = viewModel as? DeparturesViewModel
}
}
struct DeparturesViewModel: ViewModel {
init(route: RouteModel) {
}
}
extension UIViewController {
static func create(using viewModel: ViewModel) -> UIViewController {
let viewController = self.init(nibName: String(describing: self), bundle: Bundle.main)
if let vc = viewController as? ViewController {
vc.accept(viewModel)
}
return viewController
}
}
struct Dispatcher: Dispatching {
func dispatch(_ dispatchable: Dispatchable) -> ViewModel? {
let dispatch = dispatchable.dispatch
guard let viewControllerType = dispatch.viewControllerType as? UIViewController.Type else {
return nil
}
let viewModel = dispatch.viewModel
let viewController = viewControllerType.create(using: viewModel)
switch dispatch.presentation {
case .push:
guard let currentViewController = Dispatcher.currentViewController() else {
return nil
}
Job.uiJob {
if let navigationController = currentViewController.navigationController {
navigationController.pushViewController(viewController,
animated: true)
}
}
return viewModel
case .modal:
guard let currentViewController = Dispatcher.currentViewController() else {
return nil
}
Job.uiJob {
if dispatch.options.contains(.embedInNavigationController) {
let navigationController = UINavigationController(rootViewController: viewController)
currentViewController.present(navigationController,
animated: true,
completion: nil)
} else {
currentViewController.present(viewController,
animated: true,
completion: nil)
}
}
return viewModel
}
}
// MARK: - private
private static func rootViewController() -> UIViewController? {
return Job.syncUIJob({ () -> UIViewController? in
return UIApplication.shared.keyWindow?.rootViewController
})
}
private static func currentViewController() -> UIViewController? {
guard let rootViewController = Dispatcher.rootViewController() else {
return nil
}
return doFindViewController(from: rootViewController)
}
private static func doFindViewController(from viewController: UIViewController, extraCriteria: (UIViewController) -> Bool = {viewController in return false}) -> UIViewController {
if extraCriteria(viewController) {
return viewController
}
if let tabBarViewController = viewController as? UITabBarController,
let selectedTabViewController = tabBarViewController.selectedViewController {
return doFindViewController(from: selectedTabViewController)
}
if let navigationViewController = viewController as? UINavigationController,
let topViewController = navigationViewController.topViewController {
return doFindViewController(from: topViewController)
}
if let presentedViewController = viewController.presentedViewController {
return doFindViewController(from: presentedViewController)
}
return viewController
}
}
extension RouteModel: Dispatchable {
var dispatch: DispatchChange {
let viewModel = DeparturesViewModel(route: self)
return DispatchChange(presentation: .modal,
options: .embedInNavigationController,
viewModel: viewModel,
viewControllerType: DeparturesViewController.self)
}
}
protocol ViewModel {
}
protocol ViewController {
func accept(_ viewModel: ViewModel)
}
enum PresentationType {
case push
case modal
}
struct PresentationOptions: OptionSet {
let rawValue: Int
static let embedInNavigationController = PresentationOptions(rawValue: 1 << 0)
}
struct DispatchChange {
var presentation: PresentationType
var options: PresentationOptions
var viewModel: ViewModel
var viewControllerType: ViewController.Type
init(presentation: PresentationType,
options: PresentationOptions = [],
viewModel: ViewModel,
viewControllerType: ViewController.Type) {
self.presentation = presentation
self.options = options
self.viewModel = viewModel
self.viewControllerType = viewControllerType
}
}
protocol Dispatchable {
var dispatch: DispatchChange { get }
}
protocol Dispatching {
@discardableResult
func dispatch(_: Dispatchable) -> ViewModel?
}
struct Job {
static func uiJob(_ job: @escaping () -> Void) {
DispatchQueue.main.async(execute: job)
}
static func syncUIJob<T>(_ job: @escaping () -> T) -> T {
if Thread.isMainThread {
return job()
}
var result: T!
DispatchQueue.main.sync {
result = job()
}
return result
}
}
struct RouteModel {
let fromStationCode: String
let toStationCode: String
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment