4 Magic Tricks to Simplify Property Configuration in Swift

Originally written by Andre Gillfrost

Stars in this article: Extensions, Closures, generics, keypaths, subtags

When we try to improve the way we define objects in Swift, we set several goals and try to achieve them from different angles, only to find that we try too hard and just write code that looks silly, but maybe we learn something along the way.

case

Here is our control group, using the most primitive UIKit layout code, you should be familiar with:

let label = UILabel(a)func setupLabel(a) {
    label.text = "text"
    label.textColor = .blue
}
Copy the code

There’s nothing wrong with that, but without a code specification, it’s likely that this type of code will become scattered or end up in places you don’t want to see. What can be done to fix it?

The first rule

  • The layout configuration code needs to be standardized
  • Complete the configuration uniquely, preferably when you declare it

The second criterion

  • Brevity for brevity is the soul of wit

1: chain syntax

Using the extension method to extend the type and return the Self instance makes the property configuration smooth.

The advantage of this is that each method has the same name as its property. So you don’t introduce anything new, you don’t increase the cost of learning, it’s the same thing.

extension UIView {
    func backgroundColor(_ backgroundColor: UIColor?). -> Self { 
        self.backgroundColor = backgroundColor
        return self}}Copy the code

This allows us to set the background color of a UIView instance as soon as we declare it and still keep a reference to that instance.

let blueView = UIView().backgroundColor(.blue)
Copy the code

Notice that this method returns type Self, so if it’s an extension of UIView, then when you call this method with a UIView subclass instance, like UILabel(), you’re still returning an instance of UIView

To be able to return an instance of a particular type, you can extend the specified subclass.

Of course, the UIView type being extended above is the first link in the extension chain. The backgroundColor method extends UIView, and all of its subclasses can use it, and you can also set the text method to the UILabel extension.

extension UILabel {
    func text(_ text: String?). -> Self {
        self.text = text
        return self}}let label = UILabel().backgroundColor(.blue).text("I'm a label")
Copy the code

One of the problems with this extension method for setting properties is that it’s not very reusable, and you have to give the extension method to every property you need.

In addition, the code to set the properties may become very long when initializing the instance, but I don’t think it should be called a defect, rather a feature

I’m using it in my project right now, and it looks beautiful, doesn’t it?

private let titleLabel = UILabel()
    .lines(0)
    .autoshrink(to: 0.5)
    .fontSize(17)
Copy the code

Of course, we also want to simplify the verbose method names as much as possible, and combine related attributes as much as possible in a single method to improve readability, so that even new projects can get started quickly.

I’m certainly not the first person to stumble upon this idea. You can search for ChainKit and other projects on GitHub, and the work of writing all these methods has already been done. Once installed with Pod, you’ll be happy to use it.

2: BlockConfigurable

Kotlin and Swift have a lot in common, as well as some very useful tips.

Kotlin has a apply method, which provides a block method, inside which you can call properties of objects or assign values to properties, which is really handy. The return value is itself.

This means that you don’t have to specify recipients inside a block, just as you would if you were assigning attributes to yourself. Just like the chained extension method above, recipients are returned from apply, so these attributes are bound to recipients when the block ends. Something like this:

valDude = dude (). Apply {name = "dude" age =48
}
Copy the code

Imagine, can we do that in Swift? Something like this:

let view = UIView().apply {
    backgroundColor = .blue
    isHidden = true
}
Copy the code

No, but, uh, we can’t just give up.

We can try to go in that direction, we can define a block that assigns a value to a property, pass that block in as an argument, and then return self

extension UIView {
    func apply(block: (UIView) - >Void) -> UIView {
        block(self)
        return self}}Copy the code

When called, the block can be written as a trailing closure and represents the UIView() with the anonymous argument $0, thus largely implementing Kotlin’s syntactic sugar.

let view = UIView().apply {
    $0.backgroundColor = .blue
    $0.isHidden = true
}
Copy the code

Let’s make it more generic by defining a protocol, then giving it a default implementation, and limiting the objects that adhere to the protocol to be of type AnyObject.

protocol BlockConfigurable {}
extension BlockConfigurable where Self: AnyObject {
    func apply(block: (Self) throws -> Void) rethrows -> Self {
        try block(self)
        return self}}extension NSObject: BlockConfigurable {}
Copy the code

OK, we even added a try catch to catch exceptions that might occur.

At this point, all classes that inherit from NSObject, including all Views, and most objects in the framework, can now use the Apply method.

You’ve probably already heard that, yes, Then does this, but it’s a little more refined, and it uses Then instead of Apply. You can install and use them using Pod.

You think this is where it ends? NO!

3: KeypathConfigurable

I prefer the first way to extend backgroundColor for both readability and security. I don’t like the idea that all code needs to be typed, whether it’s written by yourself or using a third party library.

What I want is something like generics, or something like that. That’s where ReferenceWritableKeypath

comes in.
,>

In Swift, keypath is a backslash (\) + property + dot (.) Definition:

\UIView.backgroundColor
Copy the code

The specific type of keypath is ReferenceWritableKeypath

, if the type is known, it can be omitted. Let’s take \. BackgroundColor for example
,>

We define a method that can set attributes to any instance via keypath and return instance self itself.

protocol KeypathConfigurable {}
extension KeypathConfigurable where Self: AnyObject {
    func sporting<T> (_ keyPath: ReferenceWritableKeyPath<Self.T>,
                     _ value: T) -> Self {
        self[keyPath: keyPath] = value
        return self}}Copy the code

Now set a UIView property like this

let view = UIView()
    .sporting(\.backgroundColor, .blue)
    .sporting(\.isHidden, true)
Copy the code

Is it better than before? But it may not be enough. Optimize it!

4: SubscriptKeypathConfigurable

Sporting again and again is a departure from our simple intent, backslash (\) + dot (.). It is hard to eliminate unless you override Swift, and if you must, try Kotlin’s Apply method.

How about replacing sporting with subscript syntax?

protocol SubscriptKeypathConfigurable {}
extension SubscriptKeypathConfigurable where Self: AnyObject {
    subscript<T> (_ keyPath: ReferenceWritableKeyPath<Self.T>, 
             _ value: T) -> Self {
        self[keyPath: keyPath] = value
        return self}}extension UIView: SubscriptKeypathConfigurable {}
Copy the code

Ok, let’s try again:

let view = UIView()[\.backgroundColor, .blue][\.isHidden, true]
let label = UILabel()[\.backgroundColor, .red][\.text, "label"][\.isHidden, false]
Copy the code

Conclusion: What have we done

We tried some new ways of setting properties instead of the traditional way. Extenison, closure, key value keypath, Subscript to make setting properties more concise, readable, and maintainable.

For better or worse, I posted a quote from a Wiki about grammar sugar:… things can be expressed more clearly, more concisely, or in an alternative style that some may prefer.