bearjaw.dev

Doing swifty things 1-bit at a time 🚀

Autolayout Wrapper

Disclaimer:

Yes, there are a lot of AutoLayout wrappers available. This is mainly me writing down my approach of creating a thin, lightweight, and flexible wrapper around AutoLayout.

Some considerations

There are many AutoLayout wrappers out there. A project that I worked on recently used SnapKit. And while there’s nothing wrong with using third-party dependencies, I prefer to learn and understand Apple’s APIs first and then write a thin wrapper around it myself to make my life easier. It also saves you from adding another third-party-dependency to your app.

Now you could say why am I even bothering with all these old APIs and technologies and not use SwiftUI instead? Well, SwiftUI has only been around since iOS 13 which kind of limits your usage of SwiftUI in production if your app still needs to support older versions of iOS.

In personal projects I did start to experiment a little with SwiftUI but so far UIKit has been a good friend to me. There’s also no point in discussing whether you should only use or learn one or the other. Like many already pointed out it depends on what you want to do and they complement each other quite well.

Time travel

When I started out as an iOS developer AutoLayout wasn’t a thing yet, so I used to calculate my views’ origins and sizes manually like this:

import UIKit

final class MyView: UIView {

    private let label = UILabel()
    
    ...
    override func layoutSubviews() {
        super.layoutSubviews()
        let origin = ...
        let size = ...
        label.frame = CGRect(origin: origin, size: size)
    }
    ...
}

While this method works well and can be quite efficient when it comes to rendering, it is definitely more verbose and time consuming than SwiftUI or AutoLayout (Once you finally get a hang of it). Another thing to keep in mind is that by calculating your frames directly, ideally dynamically, your UI probably wouldn't support right-to-left languages properly.

It took me quite some time to become familiar with AutoLayout. The API seems very cumbersome and heavy to use. To me most of the methods felt counterintuitive and seemed to do the opposite of what I expected. It definitely got better once we got anchors in iOS 9. However debugging AutoLayout is a mess and time consuming. There’s a reason why WTFAutoLayout exists.

Sometimes it’s easy and you only forgot to set view.translatesAutoresizingMaskIntoConstraints = false. Other times the height and vertical position are ambiguous and it seems impossible to grasp why. Also it's important to remember to call constraint.active = true to actually tell the OS that it should use and evaluate that constraint. That’s quite a lot of things you always have to do and remember. It proved therefore quite useful to create a thin wrapper around common AutoLayout functionality.

This can slow your development velocity down quite a bit. At the same time you might not always have the luxury to sit down and develop your own libraries to fix these issues during your day to day job. What you can do though is introduce this process step by step. Every time you find yourself writing a specific UI building block you could extract the code into a reusable function.

Putting it all together

One of the problems of AutoLayout is that when using constraints you need to remember to set translatesAutoresizingMaskIntoConstraints to false. You can make your life easier by leveraging property wrappers. I really liked this option. It makes it clear to anyone reading your code that this view is using constraints and that our favourite property has already been set to false. Property wrappers in general are an amazing tool to have in Swift.

@propertyWrapper
struct AutoLayoutable<T: UIView> {
    var wrappedValue: T {
        didSet {
            wrappedValue.translatesAutoresizingMaskIntoConstraints = false
        }
    }
    
    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
        wrappedValue.translatesAutoresizingMaskIntoConstraints = false
    }
}

However there are some drawbacks with this. You can’t really use lazy var with property wrappers which could be an issue if you’re looking to squeeze out as much efficiency as possible from your views. The most efficient view is the one that isn’t rendered. You also can't use lets. Depending on your coding style and / or your needs it could be better to manually set the value.

Example

final class DetailView: UIView {

    @AutoLayoutable
    private var labelTitle = UILabel()

    @AutoLayoutable
    private var labelSubtitle = UILabel()
    
}

One thing that bothered me with Apple's AutoLayout API is that it just feels heavy and hard to read. Coming from Objective-C where methods were always quite expressive and stated clear intent, my wrapper therefore had to be declaritive and readable as well.

By using pinToTop(of:space:priority) it became much clearer to me what I wanted to achieve without getting too much into the details of how I want to achieve it. Another idea I took into consideration was to actually have my constraints be expressible using enums. That has the advantage of reducing the number of methods you'd need to memorise and use. You could maybe pass an array of anchors that would then translate to the correct constraints.

I still might refactor my current version to a more enum driven one. For now I wanted to keep things simple. The implementation of pinToTop is pretty straightforward:

extension UIView {
    
    @discardableResult
    func pinToTop(of view: UIView,
                  space: CGFloat,
                  priority: UILayoutPriority = .defaultHigh) -> NSLayoutConstraint {
        let constraint = topAnchor.constraint(equalTo: view.topAnchor, constant: space)
        constraint.priority = priority
        constraint.isActive = true
        return constraint
    }
    
}

One thing I noticed is that sometimes you need to adjust the constant or activate or deactivate your constraints. Using @discardableResult and returing the constraint solves that issue quite well. Now you can get a reference to the constraint and animate a UI change for example.

Usually you also want some kind of spacing between two UI elements. The constant for trailling, bottom anchors uses a negative value and for top, leading uses positive values. Again this little quirk in AutoLayout is something that makes sense once you understand that it reflects the coordinate system of the OS. To make my life easier I only use positive values on the call site and my implementation handles the cases correctly.

Conclusion

I didn't include all extensions I created but the rest follow the same pattern so it's quite easy to extend and maintain. My layout code went from a lot of repetitive code to one liners:

private func configureConstraints() {
    titleLabel.pinToTop(of: self, space: spacer4)
    titleLabel.pinToSides(of: self, space: spacer4)
    detailLabel.pinTopToBottom(of: self, space: spacer2)
    detailLabel.pinToSides(of: self, space: spacer4)
    detailLabel.pinToBottom(of: self, space: spacer4)
}

This increased my speed in creating a completely new UIs immensely. Not only that but I gained a lot of readability and clear intend. Of course AutoLayout offers a lot more functionality and not everything is covered by my small library yet. But it also doesn't have to be complete. Since it's just an extension on UIView I can add functionality anytime I need it.

Since I adapted my small wrapper my broken AutoLayout constraints nearly vanished. There are still cases where you do need to scratch your head a little and think about content hugging priorities and compress resistances but that's also what keeps things interesting.

Thanks for reading! 🐶

Tagged with: