Predictable UIKit Layout using BaseComponents (Part 1)mmackh
Inspired by the recent post by on Manual UIKit Layout by Steve Troughton-Smith, I wanted to share my approach on doing layout predictable layout using SplitView, ScrollingView and ConditionalLayoutView from my Swift Package: BaseComponents. I've never publicly described how either of these components work in detail, so if you are interested in understanding or using any of these classes, read on.
Before my transition to Swift, PCSplitView served as my go-to manual layout class in all of my projects. The idea was to take any layout, break it down into understandable sections and translate those into code. In addition to doing layout, animations should remain smooth with minimal effort. First iterations of this class weren't elegant, but the basic ideas remained the same over the years. UIStackView and SwiftUI were introduced much later, but I have not yet felt the need to transition.
Almost all UI layouts can be expressed either vertically or horizontally, which results in a known maximums (width and/or height). In SplitView and ScrollView, we express this concept as direction. In addition to direction, the layout of an individual component is expressed in one of these three distinct layout types.
- Fixed - Highest priority in layout. Represents a static unit in points.
- Automatic - Tries to automatically determine the dimensions of a component in fixed units.
- Percentage - Takes the remaining width or height, subtracts any fixed length layout and uses up a given percentage of that space.
When starting with layout in code, we use viewDidLoad as a starting point inside a UIViewController.
The SplitView provides each subview with a width of 33.33% (we could have chosen .equal) of the parent view's height (left) or width (right), which makes creating pixel-perfect layouts easier.
Moving on to a more complex example, a SplitView can have a Child SplitView in order to mix and match vertical and horizontal directions. In addition, the following example illustrate how we respect a view's safeAreaInsets.
Even though the syntax is not as elegant as SwiftUI, a more advanced layout is simple to reason about. We pinned the toolbar to the bottom by filling the available space with padding from the superview's safeAreaInset (Line 10) and 100% of the remainder from 2 UIView components (Line 12, 13). A fine divider was added with the height of a single pixel (Line 15). By adding a child SplitView (Line 17), we are able to add even more components in a horizontal direction.
Let's try some automatic layout, a favourite in SwiftUI tutorials. I've added some color coding on the right.
Internally on each layoutSubviews call, SplitView calculates the required height of the label by using the maximum width as a reference point. In addition to the label's height, the safeAreaInsets top value is subtracted from the width that is available to the .percentage and .equal layoutType. Since two we have two views designated with .equal, the calculation behind the scenes is 100% / 2 = 50%. Meaning 50% of the remaining height is given to each subview.
There are always tradeoffs between performance, usability and complexity when it comes to layout, but I hope to have hit a sweet-spot with these classes. If the layout becomes too complex for SplitView to handle, there's always the ability to embed a different system as subview into into the hierarchy.
Using this approach has allowed me to build InstaPDF for iOS, iPad and Mac with a single codebase as well as improve my speed shipping other apps. I hope to follow up this format to explain more complex layouts soon, but for now, give BaseComponents a try and let me know what you think on Twitter (@mmackh).