How to share layout code between UIView and UITableViewCell subclasses
By: Oleg Dreyman
Tags: Articles, Swift, Programming, Uikit, iOS

How to share layout code between UIView and UITableViewCell subclasses

Short story of “composition over inheritance” (including generics, yay!)

Disclaimer: this technique is relevant only if you create your layouts programmatically (without Interface Builder and Storyboards). If you want to take a deep look at why you should abandon Interface Builder, make sure to read this amazing piece by Zeplin crew. If, on the other hand, you want to strengthen your love for Interface Builder, I highly recommend this inspiring article from Lyft’s Scott Berrevoets.

What happened?

You may know this frustration: you have just made a perfect subclass of UITableViewCell , it looks and works just as you wanted it to, and everything is great… until you happen to need that exact same layout somewhere else in your app, and, most importantly — outside of table view.

Full with frustration of how UITableViewCell and UIView are similar but actually different (technically, you can use UITableViewCell as UIView , but this practice is reasonably questionable), you decide to just copy and paste the code into newly created UIView subclass. And here things start to go fundamentally wrong — because the moment you need to make a change, you have to maintain that change at several places in your codebase.

So what can I do about this?

Turns out the solution is incredibly easy, with almost no drawbacks down the road. First, let’s make a very simple protocol

protocol ViewLayout {



func layout(on view: UIView)



init()



}

Then we create a layout by adopting this protocol

final class TwoLabelsLayout : ViewLayout {



let leftLabel = UILabel()

let rightLabel = UILabel()



init() { }



func layout(on view: UIView) {

let stackView = UIStackView(arrangedSubviews: [leftLabel, rightLabel])

stackView.axis = .horizontal



stackView.translatesAutoresizingMaskIntoConstraints = false // don't forget this one to enable Auto Layout in code



view.addSubview(stackView)

stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true

stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true

stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true

stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 10).isActive = true

}



}

See how you’re not dealing with any details of view life cycle and — you’re only writing a layout-specific code.

The next step would be to create helper generic base classes:

class View<Layout : ViewLayout> : UIView {



public let layout: Layout



public override init(frame: CGRect) {

self.layout = Layout()

super.init(frame: frame)

layout.layout(on: self)

}



required init?(coder aDecoder: NSCoder) {

fatalError("init(coder:) has not been implemented")

}



} class TableViewCell<Layout : ViewLayout> : UITableViewCell {



public let layout: Layout



override init(style: UITableViewCellStyle, reuseIdentifier: String?) {

self.layout = Layout()

super.init(style: style, reuseIdentifier: reuseIdentifier)

layout.layout(on: contentView) // important!

}



required init?(coder aDecoder: NSCoder) {

fatalError("init(coder:) has not been implemented")

}



}

And now we can painlessly use our layouts to create views

let twoLabels = View<TwoLabelsLayout>(frame: .zero)

or register table view cell classes

tableView.register(TableViewCell<TwoLabelsLayout>.self, forCellReuseIdentifier: "two-labels")

That was quite easy, huh?

Of course, in order to access and modify view’s content, you have to use .layout syntax

twoLabels.layout.leftLabel.text = "AnySuggestion"

Okay, but what should I do with my existing codebase?

Adopting this approach into already existing views and table view cells is very straightforward (although a bit time-consuming).

First, of course, you should extract all your layout code to those ViewLayout objects. Then, if, for example, you have a TextFieldAndImageView , you just change it’s superclass and wrap those .layout calls to make your already existing code compiling:

class TextFieldAndImageView : View<TextFieldAndImageLayout> {



var textField: UITextField {

return layout.textField

}



var imageView: UIImageView {

return layout.imageView

}



}

And that’s it: you’ve just made your view code much more reusable without breaking a single line of user code. Well done!

I tried this, but it doesn’t work. What’s wrong?

While adapting the “view layout” principle, there are some places where you should be careful:

In your layout(on:) method of ViewLayout , you need to remember to call view.addSubview to get your stuff actually showing up on the screen. When creating your base TableViewCell<Layout : ViewLayout> class, it’s very important to write layout.layout(on: contentView) instead of layout.layout(on: self) . translatesAutoresizingMaskIntoConstraints = false . Always relevant.

Sounds great, but what if I’m using Interface Builder?

Interface Builder is certainly a great tool, but it’s a huge, huge enemy of code reusability. Extensive usage of Interface Builder and Storyboards requires a lot of thought and effort to do things “right”, so abandoning Interface Builder altogether can actually improve the architecture of your app. Check out the links in the disclaimer or down below to gain a deeper understanding of the problem.