Declarative iOS layout with Panda

For the first time in a few years, something I'm super excited about was announced at WWDC. I can't wait to get my hands on SwiftUI and try it out in a real project. I'm concerned about how well it will work on custom, complicated layouts, but I'm still excited to give it a go. And how much more productive will we be with live reloading?! I know this has technically been possible in the past, but I've never managed to get it working, so I'm looking forward to trying it when it's easier to work with.

A quick look at the SwiftUI example code reminded me of a library I've been using recently called Panda. Since SwiftUI is only available for iOS 13+, I thought it might be worth sharing how I'm using Panda to create my layouts in a similar style to the SwiftUI examples.

Here's a super simple example from the Panda docs:

view.pd.add(
    imageView.pd.image(logoImage),
    label.pd.text("Logo").textColor(.red).font(size: 20),
    button.pd.title("Go").action(buttonTapped)
)

Panda wraps UIView's addSubview function and lets you lay out your views in a visual way that I find really handy. Stevia, which is an auto layout DSL, also has a layout function that lets you lay out your views in a visual way, which looks something like this:

sv(
    subview1,
    subview2.sv(
        nestedView1,
        nestedView2
    ),
    subview3
)

I prefer using Panda, which lets me include styling for each element as I add it to the hierarchy, but I can understand why others might prefer to separate the styling. It's also worth mentioning that the Stevia approach also sets translatesAutoresizingMaskIntoConstraints to false for each view, whereas Panda doesn't. I usually follow my Panda layout with auto layout constraints using a DSL that sets translatesAutoresizingMaskIntoConstraints to false for me, but in case you don't, that might be worth noting.

So here's a look at a layout I created with Panda, which includes inline styling:

let dayLabel = UILabel()

self // UIView subclass
    .pd.backgroundColor(completed ? completeBg : incompleteBg)
    .add(
        stack
            .pd.axis(.vertical)
            .distribution(.equalSpacing)
            .alignment(.center)
            .spacing(8)
            .add(
                dayLabel
                    .pd.text(day)
                    .textColor(.red)
        )
    )

// Add constraints here

Panda wraps all the properties on each view and lets you chain the calls together within your visual layout hierarchy. Using .pd to start the Panda chain, you can adjust properties like fonts and colours, and add more subviews. You can also use Panda with stack views, as you can see in my example above.

There's also built-in composition to make it easier to adjust some properties. For example, you can set borderWidth and borderColor in one Panda call, rather than using separate ones. Or, to set a shadow, you can use Panda to set shadowOpacity, shadowRadius, shadowOffset, shadowColor, and shadowPath all in one call.

It didn't take me long at all to get used to this approach, and I find the visual layout of the code helps a lot when I want to find and adjust something later. I've purposely indented my code so that each view's name stands out more, to make the hierarchy easier to scan, but that's just personal preference.

Here's another, more complicated example:

 self.view
    .pd.border(width: 1.0, color: Styles.Colours.Grey.light)
    .cornerRadius(8.0)
    .add(
        vStack
            .pd.axis(.vertical)
            .distribution(.equalSpacing)
            .alignment(.leading)
            .spacing(8)
            .add(
                mainStack
                    .pd.axis(.horizontal)
                    .distribution(.equalSpacing)
                    .alignment(.top)
                    .spacing(8)
                    .add(
                        img
                            .pd.backgroundColor(Styles.Colours.Orange.light)
                            .cornerRadius(8.0)
                            .contentMode(.scaleAspectFit)
                            .contentHuggingPriority(.required, for: .horizontal)
                            .tintColor(Styles.Colours.Orange.normal),
                        stack
                            .pd.axis(.vertical)
                            .distribution(.equalSpacing)
                            .alignment(.leading)
                            .spacing(8)
                            .contentHuggingPriority(.defaultLow, for: .horizontal)
                            .add(
                                name
                                    .pd.text(goal.attribute.capitalized)
                                    .font(.boldSystemFont(ofSize: 20.0))
                                    .contentHuggingPriority(.defaultLow, for: .horizontal),
                                ruleLabel
                                    .pd.text(subtitle(for: goal.rule, goalValue: goal.value))
                                    .font(.systemFont(ofSize: 15.0))
                                    .textColor(.lightGray)
                                    .contentHuggingPriority(.defaultLow, for: .horizontal)
                        ),
                        hStack
                            .pd.axis(.horizontal)
                            .distribution(.equalSpacing)
                            .alignment(.center)
                            .spacing(12)
                            .contentHuggingPriority(.defaultHigh, for: .horizontal)
                            .add(
                                totalView,
                                avgView
                        )
                ),
                graph
        )
)

As a side note, Panda has a sister library called Bamboo, which is an auto layout DSL that has some great features and really concise syntax for adding constraints to your views once your hierarchy is set up. But you can use each of them without the other just as well.

If you like the look of SwiftUI but can't use it just yet because you're supporting iOS versions prior to 13, you might want to give Panda a try.