Well, all primitive views in Swift (such as Text or List) don’t have any child views.
This is possible because their body property has an uninhabited type (i.
Never), which means it can’t be constructed.
How this is achieved is unclear— if you declare a view whose body has an uninhabited type, then inside that body you’ll have to return an uninhabited type, which is impossible.
It’s likely that there is some runtime dynamism at play, similar to how DynamicViewProperty works by using runtime metadata to reflect over views.
You can observe the dynamic behaviour by creating a view whose body only contains an EmptyView and placing a breakpoint on the body.
You’ll notice that the breakpoint is never hit, meaning SwiftUI is not calling the getter, since EmptyView's body has an uninhabited type.
If we were building views using UIKit, we would usually start by first creating a new class that inherits from a UIView, and maybe customise it by adding some new properties, such as one that stores a list of models.
This means, we would be inheriting the properties of a UIView as well as defining our own.
As a result, we end up with a view which takes a lot of storage.
SwiftUI takes a different approach — instead of inheritance, we use composition and chain smaller, single purpose views together.
This also means the storage is shared across the view hierarchy, leading to more efficient memory usage and allowing SwiftUI to optimise our code for faster performance.
SwiftUI also provides some out-of-the-box view types which can be used in different scenarios.
For example:EquatableView: This is a View which compares itself with its previous value and prevents its child views from updating if there were no changes made to the view.
You can easily grab an EquatableView by calling equatable() on any of your views.
TupleView: This is simply a tuple of Views and allows you to easily store and pass around a small collection of views.
For example: let views: TupleView<(Text, Text)> = .
AnyView: This is a type-erased view and is helpful when you want to return different views conditionally.
This has a performance impact as SwiftUI is going to destroy the entire view hierarchy of this view if any of its child views need an update.
An alternative to this would be to use a Group or ConditonalContent.
View LayoutGreat, now that we’ve got views out of the way, let’s take a look at how the views are laid out on the screen.
SwiftUI lays out the views a bit differently than how auto-layout does.
There is a simple three-step process:The parent view provides a size for its childThe child view picks a size for itselfThe child view communicates its size back to its parent and the parent view places the child view in its coordinate spaceFor example:The parent view has the dimensions of the entire safe area of the device, since it is the “root” view (tip: you can get the full size of the device by applying the .
The parent view then passes down this size to its child view, which is the button.
The button then decides its own size (in this case, by setting its frame to 50pt x 30 pt).
An important thing to remember is that there is no way for the parent to force the child to be of a specific size — it has to respect the size of the child.
The child view then passes its size back to its parent view, which then lays the child view in its coordinate space (in the centre by default).
It also rounds the coordinates to the nearest pixel, which means there is no anti-aliasing.
Let’s take a look at another example:Here, we have a background view applied to the button, filled with a colour.
How does the layout process look now?.Well, it’s similar to how it was before.
The parent view passes down its size to the child child, which is the background view (also known as a modifier view, which we’ll learn more about later in this post).
Since the background view is layout-neutral, it passes that size down to its child view, which is the button.
The button decides to keep the same frame as earlier, so it passes the size back to the background view.
The background view passes this size back to the parent view, but before it does, it also passes that size to its secondary child view, which is the colour view.
Colours always obey whatever size given to them, so in this case, its the same size as the background (which is the same size as the button).
The background then passes the size back to the parent.
Finally, the parent view positions the background view in its own coordinate space and centres it as before.
In case you were wondering — yes, there are no layout constraints available to us, like in UIKit.
Everything is done using stacks, frames, paddings, spacers, etc.
The reason why is because SwiftUI is automatically generating the constraints for us behind the scenes.
View BuilderNow that we’ve understood what views are and how its laid out on the screen, let’s take a look at how views are declared.
In SwiftUI, a view heirarchy is declared declaratively, rather than imperatively.
In other words, your code describes the user interface and SwiftUI does the heavy lifting to figure out how to draw that on the screen.
Here’s a simple example:Here, we have a stack of two views — a label and a button, which contains also contains a label, which gives the button its title.
In this example, we’ve declared the hierarchical relationship between the these views, by assembling them into a structure that stores that information and lets SwiftUI render it on the screen.
We created this structure with the help of a container view.
Container views allow for easy composition of views, for example, by letting you place one view inside of another (and so on).
How does a container view work?.Well, to understand it, let’s take a look at one of many container views available in SwiftUI — a vertical stack.
A vertical stack (or a VStack) is a container view that allows us to place multiple child views, all stacked vertically.
The way you add views to the stack is by placing them inside a closure.
However, this is not an ordinary closure — this is a closure marked with a special attribute, called @ViewBuilder.
This attribute is a function builder (see previous post for details), that “collects” all the views inside the closure and “reduces” it into a single view (you can think of it as a way of allowing us to represent a view as a function of its inputs).
It also helps us describe the hierarchal relationship between views by using braces & indentation to separate the container view, child views and their modifiers from each other.
@ViewBuilder closures only support a maximum of 10 child views, due to Swift’s lack of variadic generics.
You can work around it by either moving the extra child views into their own container (and so on) or extending ViewBuilder with extra buildBlock() methods that can accept more than 10 child views.
Writing this in UIKit would’ve taken us a lot longer, having to explicitly describe each step and calling APIs like addSubview().
This is very tedious and a small mistake could easily ruin the entire result.
By creating our views declaratively, we can focus on describing what we want to see and letting SwiftUI figure how to do it and you can be sure that you’re going to get a high quality result.
View ModifiersThe last thing to talk about is view modifiers.
Just like a View, a ViewModifer is also a protocol, with a single requirement.
But, instead of a body property, it requires a body method that takes in a View, and returns another View by “modifying” it in some way.
Here’s a simple example:In this example, we’re using two built-in modifiers — padding and background.
The padding modifier takes the padding amount as an argument and returns a new padded view.
By default, it will apply the same padding on all directions, but you can customise that if you want.
The second modifier is background, which takes a view as a parameter and returns a new view with the view passed as argument as the background.
In this example, we’re using the Color view, which simply provides a view whose background is a solid colour of our choice.
We can chain these modifiers together to create the modified view and the order in which we chain enforces a deterministic order in which the views are modified.
For example, here’s what it would look like if we put the padding after the background:This is a bit strange, isn’t it?.Well actually, the padding is still on the screen, it’s just that we can’t see it.
The reason we can’t see the padding is because the background modifier is only wrapping the text, not the padding.
This means our padding is being applied outside of the background.
This ordering behaviour is important, because otherwise it would be impossible to know in which order the modifiers are applied, unless we spent a lot of time reading the documentation or manually testing each combination of ordering.
As mentioned earlier, we can create our own modifiers by simply conforming to the ViewModifier protocol.
This is handy when we’re applying the same modification to a bunch of views, as we can encapsulate the behaviour into a single place and apply it to any view we like.
For example, if we had to style the look of text in the container the same, then instead of duplicating the code for each text, we could simply create a modifier that applies that look to each text:View modifiers are a really cool way to apply simple or advanced modifications to views and chaining them to create even more complex modifications.
Modifiers can also be used to create animatable modifications, by conforming to the AnimatableModifier protocol (which we will cover in the future).
ConclusionThat’s it folks, hope you enjoyed reading about views in SwiftUI and how it all comes together to help you design a piece of user interface quickly and easily.
In the next post, we will explore data binding and primitive views and how you can combine them together to build stateful user interfaces.