Created
January 26, 2019 13:02
-
-
Save mflint/30d70560722ec151202cc89f824da0ee to your computer and use it in GitHub Desktop.
data-driven screen dispatching for iOS
This file contains hidden or 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
class DeparturesViewController: UIViewController, ViewController { | |
private var viewModel: DeparturesViewModel! | |
func accept(_ viewModel: ViewModel) { | |
self.viewModel = viewModel as? DeparturesViewModel | |
} | |
} |
This file contains hidden or 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
struct DeparturesViewModel: ViewModel { | |
init(route: RouteModel) { | |
} | |
} |
This file contains hidden or 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
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 | |
} | |
} |
This file contains hidden or 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
extension RouteModel: Dispatchable { | |
var dispatch: DispatchChange { | |
let viewModel = DeparturesViewModel(route: self) | |
return DispatchChange(presentation: .modal, | |
options: .embedInNavigationController, | |
viewModel: viewModel, | |
viewControllerType: DeparturesViewController.self) | |
} | |
} |
This file contains hidden or 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
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? | |
} |
This file contains hidden or 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
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 | |
} | |
} |
This file contains hidden or 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
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