Skip to content

Instantly share code, notes, and snippets.

@cojoj
Last active July 27, 2017 10:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cojoj/fc7cdaea553cd33778bcadb5c7ac148c to your computer and use it in GitHub Desktop.
Save cojoj/fc7cdaea553cd33778bcadb5c7ac148c to your computer and use it in GitHub Desktop.
// This gist is to show you how I was able to _finally_ marry MVVM-C with Storyboards.
// There are quite some tutorials on this architecture, but majority of devs choose Xibs instaed of Storyboards.
// I get their point, but... Xibs to me feel like 1984 😜
//
// I started working on this new app, and I've decided to go with coordinators + MVVM as it's seems like a fair
// compromise between modularity, extendability and simplicity, so I started laying out everything in code + Storyboards.
// I've done it before, so I knew that Storyboards in this case will serve as a **containers** for app modules like:
// + Main
// + Auth
// + Camera
//
// No segues (or maybe just an _empty_ ones to show the flow), just Xibs in Sotoryboards package.
// It's way simpler for me to reason about app modules this way...
//
// I started simply by writing `enum` for my Storyboards, but I came across something way more flexible.
// https://medium.com/swift-programming/uistoryboard-safer-with-enums-protocol-extensions-and-generics-7aad3883b44d
// 👆 Amazing stuff!!! So simple, yet so convenient.
// So, as suggested by the auther, I've created an extension on `UIStroryboard` like this:
extension UIStoryboard {
/// Enum holding Storyboard names
enum Storyboard: String {
case main
case launch
case auth
/// Filename of the Storyboard file
/// Capitalized first letter
var filename: String {
return rawValue.capitalized
}
}
/// Convenience initializer for `UIStoryboard` using `Storyboard` enum.
///
/// - Parameters:
/// - storyboard: Storyboard which you'd like to use for initialization.
/// - bundle: Bundle in which we should look for specified `Storyboard`
convenience init(storyboard: Storyboard, bundle: Bundle? = nil) {
self.init(name: storyboard.filename, bundle: bundle)
}
/// Class method for initializing `UIStoryboard` using `Storyboard` enum.
///
/// - Parameters:
/// - storyboard: Storyboard which you'd like to use for initialization.
/// - bundle: Bundle in which we should look for specified `Storyboard`.
class func storyboard(storyboard: Storyboard, bundle: Bundle? = nil) -> UIStoryboard {
return UIStoryboard(name: storyboard.filename, bundle: bundle)
}
/// Instantiates View Controller from Generic Constraint.
///
/// - Returns: View Controller instance.
func instantiateViewController<T: UIViewController>() -> T {
guard let viewController = self.instantiateViewController(withIdentifier: T.storyboardIdentifier) as? T else {
fatalError("Couldn't instantiate view controller with identifier \(T.storyboardIdentifier)")
}
return viewController
}
}
// It already feels super nice to use, but... Since I'm in MVVM-C I wanted all of my View Controller to follow additional thing,
// which is, having **View Models**.
// I've created this simple `protocol`
protocol ViewModelBindable {
associatedtype ViewModelType
var viewModel: ViewModelType! { get set }
}
// I know, I know... This name... But I can't find anything better for now.
//
// With this in my toolbelt I was able to intantiate View Controller in convenient way,
// but still I was forced to assign this View Model everywhere...
// I was kinda jealous of the super simple `init` methods in Xib-guys code...
// After taking a second look... Come one it's exactly the same now!
// So I put together this small `extension`:
extension ViewModelBindable where Self: UIViewController {
/// Initilaizer which allows you to skip overhead of instantiating from Storyboards and binding View Model.
///
/// - Parameters:
/// - storyboard: Storyboard from which View Controller should be initialized (By default it's `.main`).
/// - viewModel: View Model which should be binded to the initialized View Controller.
init(from storyboard: UIStoryboard.Storyboard = .main, viewModel: ViewModelType) {
self = UIStoryboard.storyboard(storyboard: storyboard).instantiateViewController()
self.viewModel = viewModel
_ = self.view
}
}
// Well, it works... You can instantiate View Controllers which are in Storyboards and bind View Models to them.
// This _magical_ line with assinging `view` to nothing forces initialization of `IBOutlets`, so you can reference them immediately and not get crash because of `nil` unwrapping.
// Having this as an extension allows us to have all `UIViewController` instances (and children ones as well) use it without additional code.
//
// Let's take at code.
// We have a simple View Controller conforming to `ViewModelBindalble` protocol.
class AuthViewController: UIViewController, ViewModelBindable {
var viewModel: AuthViewModel!
override func viewDidLoad() {
super.viewDidLoad()
}
}
// Now in our Coordinator for Auth Module we may have a method like:
private func showLogin() {
let controller = AuthViewController(from: .auth, viewModel: AuthViewModel())
router.setRootModule(controller, animated: true, hideBar: true)
}
// Dead simple, right?
// Of course it has some drawbacks or may not be as convinient as you may wish, but you can extend it further!
// If you have one Storyboard file it's even simpler, cause you may completely skip this forst parameter (I have it with default value).
// Just remeber, that to have it work, you need specify `Storyboard ID` in your IB Inspector, so the Storyboard extension can actually find it.
//
// **What I really like about this solution is that I don't have to force cast anything!**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment