Last active
July 27, 2017 10:23
-
-
Save cojoj/fc7cdaea553cd33778bcadb5c7ac148c to your computer and use it in GitHub Desktop.
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
// 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