Ever since Apple introduced Storyboard in the iOS 5 era, the debate about how to build uIs this way has been going on in the Cocoa developer community. I wrote a post in 2013 about the trade-off between code writing UI, XIB, and SB. Now, four or five years later, with SB evolving a lot and a lot of experience with SB development, let’s go back and look at some of the concerns we had and extract some of the best practices that we have today based on the current state of SB development.

This article originated as a response to the article on the use of SB (and its English version), and I have some different views on some of them. But as the author says in the last paragraph, you should choose what works best for you. So my opinion, or what I call “good practice,” is just a conclusion from my own point of view. This article will start with a look at each of the points made in the original article, and then introduce some of my own experiences and ways of using SB on a daily basis.

After all the years of controversy about storyboards or Interface Builders, I don’t care about another one. -)

The original analysis

Storyboard conflict risk and loading

There is a very radical point in the original, and that is:

You only put one UIViewController in each SB

I can’t agree with this idea. Developers with experience with XIB back in iOS 3 or 4 will know that this is basically a throwback to XIB usage. There are three main reasons for this:

  • Reduce Git conflicts when two developers develop a View Controller at the same time
  • Speed up storyboard loading, because you only need to load one UIViewController
  • You can load the desired View Controller from SB using only the Initial View Controller

With the introduction of SB reference in Xcode 7, “SB conflicts easily” has become a completely false statement. By properly dividing up the functional modules and what each developer is responsible for, we can completely avoid collision of SB’s changes. For the last two or three years we have had no SB conflicts in our actual projects at all.

In addition, even if the SB partition is problematic, the impact is manageable. In a single SB file, each View Controller has its own scope, so even if different developers work on a SB file at the same time, as long as they don’t change the contents of the same View Controller at the same time, There is no conflict on the View Controller. There are some common parts in SB files, such as IB versions, system versions, etc., but they do not affect the actual UI, and conflicts can be avoided by unifying the development member’s environment. Therefore, multiple VCS in one SB and one VC in one SB actually bring about almost the same risk of conflict.

When it comes to loading SB, we can see that the original author probably didn’t understand the whole process of loading UI, and assumed that the more View Controllers in SB files, the longer the loading time would be, which is not the case. For those who are careful (or have a lot of SB files in their project), Xcode has a process of Compiling Storyboard files at compile time:

During compilation, the SB files used in the project are also compiled and saved in the final app package with the storyboardc extension. This file, similar to.bundle or.framework, is actually a folder containing an info.plist file that describes the compiled SB information, as well as a series of.nib files. Each object in the original SB (or, in general, each View Controller) will be compiled into a separate.nib, which contains the corresponding object hierarchy encoded. When loading a SB and reading a single View Controller from it, the system first finds the compiled.storyboardc file and gets the required View Controller type and NIB relationship from info.plist. To initialize the UIStoryboard. Then read a corresponding NIB, and use UINibDecoder to decode, restore the NIB binary to the actual object, and finally call the initWithCoder of the object: to complete the decoding of each attribute. After this is done, awakeFromNib is called to notify the developer that the load from NIB is complete.

If you understand the process, you can see that there is no difference in speed between loading the VC from an SB with a single View Controller and loading it from multiple View Controllers. To be clear, if you use too many SB files, you will need to read more info.plist when initializing UIStoryboard. Instead, performance degrades (instead, we can use the Storyboard property of the View Controller to get the UIStoryboard that the VC currently belongs to, so as not to initialize the same storyboard multiple times, but this performance penalty is not important).

On the third point, the author uses a piece of code to show how to create type-safe objects by something like this:

let feed = FeedViewController.instance()
// `feed` is of type `FeedViewController`
Copy the code

There are several conditions for doing this, first it needs to create SB files based on the ViewController type name, and second it needs to add helper methods for UIViewController to find SB files based on the type name. It’s not an obvious advantage, it definitely introduces dynamic things like NSStringFromClass, and there are a lot of better ways to create a type-safe View Controller. I’ll cover some of that in Part 2.

The use of the Segue

The second main point of the original text is:

Don’t use Segue

Basically, a Segue is a Segue that connects different View controllers, migrates or organizes VC’s. Given the first argument (a SB file contains only one VC), it is a natural corollary not to use segues, since there are no more than one VC relationship in the same SB to organize, and segues are much less useful. But the author uses a bad example to force the downside of using segue and prepare(for:sender:). Here is a sample code from the original article:

class UsersViewController: UIViewController, UITableViewDelegate { private enum SegueIdentifier { static let showUserDetails = "showUserDetails" } var usernames: [String] = ["Marin"] func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { usernameToSend = usernames[indexPath.row] performSegue(withIdentifier: SegueIdentifier.showUserDetails, sender: nil) } private var usernameToSend: String? override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segue.identifier { case SegueIdentifier.showUserDetails? : guard let usernameToSend = usernameToSend else { assertionFailure("No username provided!" ) return } let destination = segue.destination as! UserDetailViewController destination.username = usernameToSend default: break } } }Copy the code

Basically, what this code does is when the user clicks on a cell in the table View, it saves that click to a member variable of the View Controller, usernameToSend, And then I call performSegue withIdentifier:sender:). Next, get the saved member variable in prepare(for: Sender 🙂 and set it to the target View Controller. This is not necessary for the table View. We could have segued the cell directly to the target View Controller, Then use the Table View indexPathForSelectedRow in prepare(for: Sender 🙂 to get the required data and set the target View Controller. Maybe the original author didn’t know that UITableView had such an API, so he used a bad example.

So is there a problem with segues? My answer is yes, but not much. In the prepare(for:sender:) file, you need to pass the data to the target View Controller. Most of the time, however, the data in this case already exists in the current View Controller (for example, the text needs to be passed in the text field, or a property of the current VC model, etc.). Far more challenging than the problem of variables is the management of migrations between View controllers. Now we can either do the transition with code (pushViewController(: Animated) or present(: Animated :completion), or we can use the segue from the click control in SB, You can even call performSegue from within your code, and managing it in different places makes your code complicated and hard to understand, so we might need to think about how to manage it in a centralized way. Objc. IO’s Swift Talk 5 video – Connecting View Controllers (free) explores this issue a bit and gives a way to centrally manage migration between View Controllers. The method of using callback can be used for reference, but I personally have doubts about the application of the whole idea in the actual project, so we may as well refer to understand.

In addition to managing transitions, segues provide a convenient Container View embed relationship, and can be used to provide some code to run at initialization when using multiple VC relationships like UIPageViewController, Or you can conveniently implement the dismiss by unwinding. All of these “add-ons” allow us to write less code, improve development efficiency, and it’s a shame not to try them out.

Love, don’t reject GUI

The author’s final main point is:

All properties are set in the code

People are visual creatures and one of the main goals of using SB is to intuitively understand the interface. Using the SB canvas we can quickly get information about the View Controller to be developed much faster than reading the code. But if all the attributes are set in code, how much of an advantage is there?

The authors suggest leaving all default Settings (even the background color of the View, or label text, etc.) for added views or ViewControllers in SB, and then setting them in code. At this point, the author’s concern is changes to UI element styles. The author wants to store constants like fonts, colors, etc., and assign them to UI elements in code so that design changes can be made in only one place.

The downside of this approach is that you need a lot of IBOutlets and a lot of extra work to set up these properties. My advice is to use SB directly as much as possible for things that don’t change with the state of the program. For example, text on a label. Unless the text really needs to be changed (such as displaying a user name, or the current number of comments, etc.), there’s no need to add @ibOutlet at all. It’s much easier to set text directly. Other attributes, such as the Cancellable Content Touches of UIScrollView, are best set directly in IB if they don’t need to be changed in the app based on the state of the app. The author mentioned in the original article that “it is easier to scan the code to find the properties of the view than to find a tick in the storyboard”, and I don’t think there is any difference between the two. For example, setting the Cancellable Content Touches of UIScrollView to false via IB Cancancelcontentcontent= “NO” will be added to the Scroll view of the corresponding SB file. It is also easy to find this attribute through a global search. You can even modify SB’s source code to do this without opening Xcode or IB at all. Based on the possibility of finding, batch replacement and update is no different from setting up in code. There is no such thing as being easier to find in code.

Note, however, that attributes in SB are filtered in Xcode’s search results and do not appear, so you may need to use other text editors to look globally.

The author has understandable concerns about view styles such as fonts or colors. IB now lacks a good way to make style, which has been criticized for a long time. There’s a style option in the Font selection, which lets us choose from items like Body, Headline, and so on, which looks good:

But this is just to support Dynamic Type, setting these values is the same as calling the preferredFont(forTextStyle:) from UIFont to get a particular font. These font styles cannot be defined or added. The same goes for colors. Xcode does not provide a concept like project color plates or color variables that can be used in IB.

There are probably two common and simplest solutions to the View style.

The first is to use custom subclasses to set properties such as fonts or colors uniformly. For example, your project might have subclasses of UILabel such as HeaderLabel or BodyLable, and then set the font in the corresponding method in that subclass. This approach is straightforward. You can apply the font by changing the label type in IB. The disadvantage is that as the project gets bigger, the label types may become more numerous. In addition, making changes that are not global, such as adjusting only for a particular label, can be a hassle, and you may want to adjust only for individual cases rather than creating new subclasses for such cases, a decision that can often undo all your previous efforts to unify styles.

Another way to do this is to add attributes like style to the type of the target view and then set them using the Runtime attribute. A simple idea would be something like this, for example for fonts:

extension UIFont {
    
    enum Style: String {
        case p = "p"
        case h1 = "h1"
        case defalt = ""
    }
    
    static func font(forStyle string: String?) -> UIFont {
        guard let fontStyle = Style(rawValue: string ?? "") else {
            fatalError("Unrecognized font style.")
        }
        
        switch fontStyle {
        case .p: return .systemFont(ofSize: 14)
        case .h1: return .boldSystemFont(ofSize: 22)
        case .defalt: return .systemFont(ofSize: 17)
        }
    }
}
Copy the code

This code adds a static method to UIFont to get different styles of fonts from the input string.

We then add an extension to set the style for types that require font style support, such as UILabel:

extension UILabel {
    var style: String {
        get { fatalError("Getting the label style is not permitted.") }
        set { font = UIFont.font(forStyle: newValue) }
    }
}
Copy the code

In IB, we can add runtime attributes to the UILabel that we want to apply the style to:

However, either way, the disadvantage is that we cannot see the change of label visually in IB. Of course, you can overcome this disadvantage by implementing @ibDesignable for custom UILabel subclasses, but this also requires additional work. Let’s hope Xcode and IB move forward and natively support a similar style organization. But it’s a bit arbitrary to throw away a straightforward UI approach.

I’ve basically come up with my own ideas for each point of the article, but as the author says at the end, you should choose your own usage style and decide how to use the Storyboard.

It’s not all or nothing.

The author only uses IB and Storyboard as a tool to set view hierarchies and add layout constraints, which is SB’s strong suit, but I think it’s much more powerful than that. Understanding SB’s design philosophy and using SB in a controlled way will help unlock the potential of the tool and further improve development efficiency.

The next section of this article will briefly introduce a few practices for using SB.

Practical experience

Use storyboards in a type-safe way

The author mentioned that storyboards using a single VC can be created in a type-safe manner. It’s not necessary, and we can do it better in other ways. In the Cocoa framework, there really are a lot of string-based apis for flexibility, which leads to a certain level of insecurity. Apple itself is unlikely to make major changes to existing type-insecure apis for versatility and compatibility, but we can still make our apis more secure with the right packaging. I use tools like R. Swift for both my personal and corporate projects. This project creates a way to retrieve resources using types by scanning your various string-named resources (such as image names, View controllers, and segue identifiers). Compared with the type-safe approach of the original author, this is obviously a more mature and complete approach.

For example, we could get the View Controller from SB with code like this:

let myImage = UIImage(names: "myImage")
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "myViewController") as! MyViewController
Copy the code

With the help of R. Swift, we will be able to use the following code:

let myImage = R.image.myImage()
// myImage: UIImage?

let viewController = R.storyboard.main.myViewController()
// viewController: MyViewController?
Copy the code

This ensures type-safety while also confirming the presence of resources at compile time. If you change the identifier of the View Controller in SB, but do not change the corresponding code, you will get a compilation error.

In addition to images and View controllers, R.swift can be used to localize strings, segues, NIB files, or cells that contain string identifiers. By incorporating R.swift into the project for management, we avoided many potential resource usage hazards and bugs during development, and saved countless hours with the help of auto-completion. Things like taking a Storyboard and creating a View Controller out of it are completely trivial.

Reduce code Settings with @ibInspectable

One limitation of setting view properties via IB is that some properties are not exposed in the IB Settings panel, or they may have to be “turned”. The IB panel already contains 90 percent of the most commonly used attributes, but some of them may slip through the cracks. The most common situations we encounter in engineering practice are setting localized strings for a view that displays text and rounded corners for an Image View.

We solved both of these problems using the extension method by adding @ibInspectable to the corresponding view. For example, for localized strings, we would have an extension like this:

extension UILabel { @IBInspectable var localizedKey: String? { set { guard let newValue = newValue else { return } text = NSLocalizedString(newValue, comment: "") } get { return text } } } extension UIButton { @IBInspectable var localizedKey: String? { set { guard let newValue = newValue else { return } setTitle(NSLocalizedString(newValue, comment: ""), for: .normal) } get { return titleLabel? .text } } } extension UITextField { @IBInspectable var localizedKey: String? { set { guard let newValue = newValue else { return } placeholder = NSLocalizedString(newValue, comment: "") } get { return placeholder } } }Copy the code

In this way, we can set Localized strings directly with the appropriate type of item in IB:

Setting rounded corners is similar, introducing such an extension to UIImageView (or even UIView) and setting it directly in IB avoids a lot of template code:

@IBInspectable var cornerRadius: CGFloat {
   get {
       return layer.cornerRadius
   }
   
   set {
       layer.cornerRadius = newValue
       layer.masksToBounds = newValue > 0
   }
}
Copy the code

@ibinspectable is actually the same as the UILabel style method mentioned above, both of which use runtime attributes. Obviously, you can also write the UILabel style as @ibinspectable to set the style directly in IB.

@ IBOutlet didSet

While this tip won’t make a real difference in the use of IB or SB, I think it’s worth mentioning. If for some reason we do need to set some view properties in our code, a lot of developers will do that in viewDidLoad after we connect to @ibOutlet. Actually, I think a better place to do it is in the didSet of that @ibOutlet. @ibOutlet is also modifying a property, and all this keyword does is expose the property to IB, so its various property observation methods (willSet, didSet, etc.) will be called as normal. For example, here is the code from our actual project:

@IBOutlet var myTextField: UITextField! {didSet {/ / Workaround for https://openradar.appspot.com/28751703 myTextField. Layer. The borderWidth = 1.0 myTextField.layer.borderColor = UIColor.lineGreen.cgColor } }Copy the code

Doing this will keep the code that sets up the view relatively focused on the view itself, and it will also make viewDidLoad cleaner.

Inheritance and reuse issues

To say so much about Storyboard is not to say that it doesn’t have its drawbacks. Not only that, but there are many, many improvements to SB, of which inheritance and reuse using SB is the most difficult.

Storyboard does not allow separate views, so if we want to reuse views through IB, we need to fall back to xiB files. Even so, it is not easy to initialize a View loaded through a XIB in SB’s View Controller. Generally, for this requirement, we choose to load the target nib in init(coder:) and add it to the target view as a subview. This process requires developers to have a good understanding of how the NIB loads the View and view Controller, but unfortunately Apple keeps this process a little secret, so most developers don’t care, don’t know much about it, and just don’t think it’s possible.

View inheritance is a little more difficult. Again, since binary NIBs will be decoded back, care needs to be taken when setting the properties of the parent class. In addition, it can be a difficult choice whether the UI of a subclass should be built by creating a new XIB, or whether the UI of the parent class should be added to the subclass by code. Inheriting and reusing views in code is much easier, and the method is much more explicit.

Not only individual views, but also view Controller inheritance and reuse in SB. View Controller reuse is relatively easy, you initialize the corresponding View Controller with a storyboard, or segue. Inheritance is a little bit more complicated, but the good news is that view Controller inheritance isn’t that complicated compared to view inheritance, and the most common inheritance for UIViewController in UIKit is basically UITableViewController, UICollectionViewController, as a final presentation to the user code for the view of management, also have little need to inherit an already highly dedicated, and use the IB build view Controller. If you have a need for inheritance in your project, it is a good idea to consider the necessity of inheritance first. If an existing View Controller can be reused through different configurations, then “inheritance” may be a bogus requirement.

However, it cannot be denied that the way the UI is built is by encoding and decoding XML files, which makes inheritance and reuse difficult, and this is the biggest shortcoming of IB or SB.

conclusion

This article is intended to give you some of my own ideas about storyboards and how I use them in my daily development. It’s not like “you should use it this way” or “best practices should be like this.” You can choose to build the UI in pure code, but Apple also gives us a much faster IB and Storyboard approach. In my years of experience with SB, the design is not that bad, and the current way of development is much more efficient than using code or XIBs in the past. Developers choose tools based on their own needs and understanding, and everyone chooses and uses them in a way that should be respected. As long as you’re willing to embrace change, try new things, and find what’s right for you, it doesn’t really matter what approach you use.

Finally, may your skills keep growing and your life shine.