Simple μFramework for loading Storyboard and Xib files for iOS.
Let's imagine:
You have Main.storyboard file with initial view controller and two other controllers with identifiers: "PageViewController", "PageDetailsViewController". You have to instantiate them programatically and all you need to do is to declare enum like that:
enum Main: String, Storyboard {
case initialViewController, pageViewController, pageDetailsViewController
}
... and you can load view controllers:
let pageViewController = Main.pageViewController.load() // type will be UIViewController
... or with type:
let pageViewController: PageViewController = Main.pageViewController.load()
... and also you would like to write Unit Test which will check all the controllers are loading properly. So you write:
extension Main: CaseIterable { }
class AppTests: XCTestCase {
func testMainStoryboard() {
_ = Main.allCases.map { $0.load() } // [UIViewController]
}
}
Lucky you ;) You can do that with the Loaders.
When you use single storyboard per view controller you can declare it:
enum Details: Storyboard, HasInitialController { }
Then you can instantiate controller like that:
_ = Details.instantiateInitialViewController() // UIViewController
When you need typed initial view controller you have to specify typealias like that:
enum Details: Storyboard, HasInitialController {
typealias InitialControllerType = DetailsViewController
}
You can also declare strong type view controllers based on identifiers:
enum Main: Storyboard, HasInitialController {
typealias InitialControllerType = MainViewController
static var pageViewController: PageViewController { return load() }
static var pageDetailsViewController: PageDetailsViewController { return load() }
}
then load:
_ = Main.instantiateInitialViewController() // MainViewController
_ = Main.pageViewController // PageViewController
_ = Main.pageDetailsViewController // PageDetailsViewController
If you don't like computed property to work as a fabric you can use methods instead:
enum Main: Storyboard, HasInitialController {
typealias InitialControllerType = MainViewController
static func pageViewController() -> PageViewController { return load() }
static func pageDetailsViewController() -> PageDetailsViewController { return load() }
}
then load:
_ = Main.instantiateInitialViewController() // MainViewController
_ = Main.pageViewController() // PageViewController
_ = Main.pageDetailsViewController() // PageDetailsViewController
Sometimes there is a need to pass the factory that creates UIViewControllers and create UIViewController later instead of passing UIViewController instance. For that you can use Loader
struct or just ConrollerLoader
enum Main: Storyboard, HasInitialController {
static var pageViewController: Loader<PageViewController> { return loader() }
static var pageDetailsViewController: ControllerLoader { return loader() }
}
_ = Main.initialViewController.load()
_ = Main.pageViewController.load()
_ = Main.pageDetailsViewController.load()
or
enum Main: String, Storyboard {
case mainViewController
}
let loader = Main.mainViewController.loader()
loader.load()
or
enum Main: String, Storyboard, ControllerLoader {
case mainViewController
}
let loader = Main.mainViewController
loader.load()
Loaders also provides a way to register and deque reusable views loaded from xib files. It works for UITableViewCell, UICollectionViewCell, UICollectionReusableView. The rule you has to follow is that the class name, xib file name and identifier has to be the same.
enum FormCells: Nibs {
static var firstTableViewCell: Reusable<FirstTableViewCell> { return load() }
static var secondTableViewCell: Reusable<SecondTableViewCell> { return load() }
}
then you can register them:
FormCells.firstTableViewCell.register(on: tableView)
and later dequeue:
let cell = FormCells.firstTableViewCell.dequeue(on: tableView, for: indexPath) // FirstTableViewCell
Note: It works exaclty the same for UICollectionView.
If you have storyboards or reusables in different module of your app you can simply enclose declaration in enum with the same name as module. You can still enclose your declaration by any enum for grouping purposes but please remember to not conflict it with any module in your app.
enum Storyboards { // there is no module 'Storyboards' in the app so it will use 'current' module for Main storyboard
enum Main: String, Storyboard {
case initialViewController, pageViewController, pageDetailsViewController
}
enum User { // there is module User in the app it will load storyboards 'Main' and 'Profile' from there
enum Main: String, Storyboard {
case initialViewController, userViewController, userDetailsViewController
}
enum Profile: Storyboard, HasInitialController { }
}
}
Creating custom view using xib files instead writing them by hand in code is great idea. It is simple but need some boilerplate and usually it is not obvious for beginers how to do it in proper way. Each developer who is using your custom view should have possibility to instantiate that view in storyboards but he also should be able to do it from the code. He also should see the custom view in storyboard properly instead of "white rectangles". Loaders provide simple mechanism for loading xib files to custom views corectly.
To make custom designable view you need to create Xib file with the same name as your custom class and set the 'File Owner' with that class to have all IBOutlets initialized properly (remember - do not set 'Custom Class' for the main view - set 'File Owner' only).
In your custom view you have to add two constructors and inside them you have to add "Xib file" by single line of code Nib.add(to: self`)
. Simple implementation may look like that.
@IBDesignable
class DesignableView: UIView {
@IBOutlet private var label: UILabel!
@IBInspectable var title: String = "title" {
didSet {
label.text = title
}
}
override init(frame: CGRect) {
super.init(frame: frame)
Nib.add(to: self)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
Nib.add(to: self)
}
}
Loaders is a simple way to define all your storyboard's UIViewControllers and NIB reusables (UITableViewCell, UICollectionViewCell, UICollectionReusableView) in clean, declarative way. It brings autocomplete and compile time checking for storyboards and xib files.