preface


This blog focuses on a Swift practice trick, chained UI code. Chained code has a natural advantage over Objective-C in Swift. And through the powerful characteristics of Swift language itself, it takes very little code to make their Swift project have the ability to write chained UI code.

The reason


So to answer the question, why do you write a UI in chained code? The answer is to improve code readability. The importance of code readability in software engineering goes without saying. Code readability can be improved in many ways, such as proper indentation, proper naming, clear logical structure, and so on. Chained UI code is one way to improve code readability from the point of clear logical structure.

In iOS development, we deal with UI code every day. In terms of UI implementation, Storyboad versus code is a never-ending debate in iOS development. Both have their advantages, and discussing them is beyond the scope of this blog. No matter how you write your UI, code, or Storyboard, if the overall UI layout doesn’t have a clear hierarchy and structure, it can be difficult to maintain. The focus of this article is on a code style that uses chained code to write UI layouts, not how to make UI layouts structurally clear.

precondition


There are some preconditions that need to be explained. For the rest of the presentation, UI layout is done in code, and AutoLayout is used instead of absolute layout. No one would write an entire UI using the native Autolayout API, usually using a wrapper library. SnapKit, the most common Autolayout wrapped library, is used as an example. If your project uses another library, modify the code accordingly.

Core code implementation


Let’s take a look at the structure of the UI layout code. Take a look at some of the UI layout code done in code and you’ll see some of the specific structures.

The first step is definitely to initialize a view, followed by a series of steps to complete the requirements: adding to the parent View, adding view constraints, and configuring properties. In the next three steps, add to the superview step before adding the view constraint step. As for the step of configuring the View properties, there is no problem before or after these two steps.

In addition to initializing the view, extract the next three steps into three generic chained layout functions, which are extended to UIView using the extension mechanism. The implementation code is as follows:

import UIKit
import SnapKit

protocol ViewChainable {}
extension ViewChainable where Self: UIView {
    @discardableResult
    func config(_ config: (Self) -> Void) -> Self {
        config(self)
        return self
    }
}
extension UIView: ViewChainable {
    func adhere(toSuperView: UIView) -> Self {
        toSuperView.addSubview(self)
        return self
    }
    @discardableResult
    func layout(snapKitMaker: (ConstraintMaker) -> Void) -> Self {
        self.snp.makeConstraints { (make) in
            snapKitMaker(make)
        }
        return self
    }
}
Copy the code

That’s all the code to complete the chained UI layout. Isn’t it great to accomplish the entire core functionality in just over 20 lines of code? Without reference to any third party libraries, just copy this code into the Swift project and your project will immediately have the ability to write chained UI code.

As an example, we want to add a label to a view that is horizontally centered and whose top is 80 away from the top of the parent view. The code is as follows:

let label = UILabel()
    .adhere(toSuperView: view)
    .layout { (make) in
        make.top.equalToSuperview().offset(80)
        make.centerX.equalToSuperview()
    }
    .config { (label) in
        label.backgroundColor = UIColor.clear
        label.font = UIFont.systemFont(ofSize: 20)
        label.textColor = UIColor.darkGray
        label.text = "Label"
    }
Copy the code

So what’s the benefit of this sample code? First, the steps for the UI layout are obvious from this code, and the logic for adding constraints and configuring views is wrapped up in different closures. The structure of the code is a little bit cleaner than it was before, where all the code was indented the same way. After locating the UI problem, you can directly analyze the corresponding code logic according to whether the problem is a layout error or a configuration problem.

The additional benefit of such code is increased reusability of the UI configuration-related code. Make the following changes to the config part of the code above:

let labelConfiger = { (label: UILabel) in
    label.backgroundColor = UIColor.clear
    label.font = UIFont.systemFont(ofSize: 20)
    label.textColor = UIColor.darkGray
    label.text = "Label"
}
label.config(labelConfiger)
Copy the code

Complete the same code as before, but define a Closure of type (UILabel) -> Void and pass it directly to the chain function config as an argument. If you create a new UILabel called Label2 and configure it the same way as label, its config function will take the Closure of labelConfiger as its argument. You can ask what happens if the text value of the two labels is different. In this case, just change the code as follows:

label.config(labelConfiger)
    .config { (label) in
        label.text = "Label1"
    }

label2.config(labelConfiger)
    .config { (label) in
        label.text = "Label2"
    }
Copy the code

That’s right. As chained code, the config function can be called to itself multiple times. Note, however, that subsequent config calls override the state set in the previous config function for the same property of the view.

In Swift, a Closure can be treated as a variable, and even a function is actually a Closure. So, it’s free to play ~~

Analysis of the implementation


To explain the implementation of chain core code. The key to chained functions is that they return values of the same type as themselves.

Since it’s UI specific, the first thing we do is extend UIView, the view parent class. The adhere function has nothing to say except that it adds itself to the superview passed in as a parameter. Next is layout this layout function implementation, due to the use of SnapKit, and the specific layout logic of different views are not the same. So the key logic code for using SnapKit layout is passed in as a Closure Closure argument. One of the trickier things to understand here is the implementation of the config function. You can see that it defines a protocol called ViewChainable, and then extends it to UIView to implement it. The reason for this is that in order for the closure arguments of the Config function to be used in practice, the closure’s first argument type can be specified on different subclasses of UIView. In the example above, when an instance of UILabel calls config, the closure argument type required is (UILabel) -> Void instead of (UIView) -> Void.

To optimize the


It’s already a long article, but I hope you read on. The next step is to optimize the chained core code above.

Since the above implementation extends methods directly to UIView, this runs into a problem in real engineering: naming conflicts. For example, if the next VERSION of the SDK is updated and UIKit adds a Adhere function to UIView with the same parameters and return value types as defined above, you will have to modify your previous code.

A common practice in the Objective-C era is to prefix system library extension methods with at least three characters, so that the function name becomes this: xxx_Adhere. Of course, you can follow this form and add a prefix to the names of all the previous chained UI functions, and there’s no problem doing that. In the Swift world, however, there is a Swifty implementation, which I described in detail in my previous blog post on the Swift Namespace form extension implementation. The HandOfTheKing project mentioned in that article already includes the implementation of chained UI code in the form of namespaces. It should be noted that in the latest update, I changed the namespace prefix of this project from hk to hand, which always feels related to Hong Kong. The implementation code is posted below (no namespace implementation is included).

import UIKit
import SnapKit

extension UIView: NamespaceWrappable { }
extension NamespaceWrapper where T: UIView {
    public func adhere(toSuperView: UIView) -> T {
        toSuperView.addSubview(wrappedValue)
        return wrappedValue
    }

    @discardableResult
    public func layout(snapKitMaker: (ConstraintMaker) -> Void) -> T {
        wrappedValue.snp.makeConstraints { (make) in
            snapKitMaker(make)
        }
        return wrappedValue
    }

    @discardableResult
    public func config(_ config: (T) -> Void) -> T {
        config(wrappedValue)
        return wrappedValue
    }
}
Copy the code

After changing to this, the previous label code is changed to the following style:

let label = UILabel()
    .hand.adhere(toSuperView: view)
    .hand.layout { (make) in
        make.top.equalToSuperview().offset(80)
        make.centerX.equalToSuperview()
    }
    .hand.config(labelConfiger)
Copy the code

The advanced


If you’re interested in responsive programming, you’ll find that the RxSwift framework is a perfect match for the chained layout code above. The RxSwift library itself also supports many chained calls. This is another big hole for RxSwift, which I won’t cover in this article.

At the end


I saw a post on V2EX yesterday asking if iOS development in 2017 will be objective-C or Swift. For me, accustomed to Swift, I don’t want to go back to Objective-C. Of course, at the end of the day, there is still a bit of fun to be had with Xcode, the poor IDE. To end this blog with a sigh, alas…