Categories
SwiftUI

Font-sized Images

WWDC 2020: Text now provides an initializer that accepts a single Image, this blog post will be updated or removed to reflect that. For now here’s the correct code.

struct ContentView : View { @ObservedObject var character: Character var body: some View { HStack { Text(Image(character.imageName) .resizable()) Text(character.name) } .font(.title) } }

Often you need to include images inline in text, while being mindful of supporting all of the various user-customizable sizes of text, not to mention the accessibility sizes:

An image matched to each possible title font size

One way to do this is by creating a custom SF Symbol, but that involves individually drawing or hinting each possible size. This obviously gives the optimal result, but it’s not necessarily something we have the resource to do. In the case of user-supplied imagery such as a character portrait, it’s not even an option available to us.

There’s a nice trick to achieving this with any resizable image, and it builds on what we learned in views choose their own sizes, and secondary views.

Recall that a secondary view receives as its proposed size the chosen size of the view it’s attached to.

So what we need to do is attach the resizable Image as a secondary view to a Text view that will inherit the font-size from the environment and choose its size accordingly.

We can then remove the Text itself from the output using the .hidden modifier, while leaving the Image overlaid on top visible:

struct ContentView : View { @ObservedObject var character: Character var body: some View { HStack { Text("M") .hidden() .overlay( Image(character.imageName) .resizable() .aspectRatio(contentMode: .fit)) Text(character.name) } .font(.title) } }

Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.

Categories
SwiftUI

View Builders

In the last post we looked at view modifiers as a way of building custom UI components that use other views as a form of content. In this one we’re going to look at a different approach using ViewBuilder.

We’re actually going to build the exact same card structure we built before:

character title in a card with shadow

When we used a ViewModifier this became a .card method we applied to the HStack like .frame or .padding, this time we’re going to make a block construct like HStack itself.

Our goal is that the code to make character card will look like:

struct ContentView : View { var body: some View { Card { HStack { Image("brawler") Text("Sir Bunnington") .font(.title) } } } }

To achieve this we’ll need Card to be a View again rather than a modifier, and we’ll use generics to handle all of the possible types of view that the content might be:

struct Card<Content> : View where Content : View { var content: Content var body: some View { content .padding() .background(Color.white) .cornerRadius(8) .shadow(radius: 4) } }

This approach in general is useful for views where there is a single view that needs to be passed in to the constructor. Good examples of this being used in SwiftUI include NavigationLink for the destination parameter.

But when we need a more complex child layout we don’t always want to have to abstract it into a custom View structure, and instead want to be able to use a block in at that point.

This is where view builders come in. @ViewBuilder is an attribute that we can declare on the parameters of methods we define, and most usefully, that includes the constructor.

So we can extend our above example to set the value of content from a view builder:

struct Card<Content> : View where Content : View { var content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { content .padding() .background(Color.white) .cornerRadius(8) .shadow(radius: 4) } }

The builder is a method that takes no arguments and returns a view, which we allow type inference to define as the Content our type is generic over. We set the property of that type by calling the method, which invokes the block passed.

And now we can use the Card view exactly as we intended in our goal code.

Patterns

The decision about whether to use a view, view modifier or a view builder is ultimately going to come down to what makes the most sense for your code.

Some like Button make sense as view builders.

But there are good examples in SwiftUI of view modifiers that instinct might suggest be view builders. .frame is a modifier on a view, there is no Frame builder, even though it’s placing the view inside it.

Only once you start combining multiple frames does it makes sense why it’s a modifier, since it’s easier to combine modifiers than it is to combine builders.

A good rule of thumb for me has to be to use a modifier first, and only use a builder when the code patterns really pulled strongly for that syntax.


Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.

Categories
SwiftUI

View Modifiers

We’ve spent some time looking at views without really diving into what a view is, and considering what other options are available to us. Let’s look at that now.

View is a defined as a protocol:

protocol View { associatedtype Body : View var body: Self.Body { get } }

This means that to conform to View we must provide a single property called body whose value is any type that also conforms to View.

SwiftUI views are defined in terms of other views, composed together into complex layouts.

At the foundation of these layouts are fundamental views such as Image and Text that are defined only in terms of themselves. They conform to View, but have Never as their associated Body type.

When we define our own views we rarely, if ever, need to worry about the actual types involved, which is fortunate because they can be very complex. We can use some View and allow the compiler to infer our true Body type.

While we can use some View as the type of a computed property, we cannot use it as the type of a stored property, which means creating a re-usable view containing another is difficult with View alone.

For example, we might have a common “card” look that we want to use for several views:

character title in a card with shadow

We know the code to create the character title:

struct ContentView : View { var body: some View { HStack { Image("brawler") Text("Sir Bunnington") .font(.title) } } }

And we know in principle the code to turn that into a card:

struct CardView : View { // 🛑 This is not legal syntax. var content: some View var body: some View { content .padding() .background(Color.white) .cornerRadius(8) .shadow(radius: 4) } }

But as noted in the comment, for reasons we discussed above, this will not compile.

We could figure out the true type of the content, but that would mean our CardView only worked with that exact type—an HStack containing an Image and a Text with a modified font. It wouldn’t be flexible for any view, and we need that for our project.

If you’re thinking about reaching for generics, you’re on the right lines, and we’ll look at an approach using those in view builders. But SwiftUI already also provides exactly what we need right now, and we’ve been using them all along without really looking at what we were doing.

ViewModifier is another protocol provided alongside View:

protocol ViewModifier { typealias Content associatedtype Body : View func body(content: Self.Content) -> Self.Body }

The primary difference between a ViewModifier and a View is that to conform we don’t just provide a property of view type, but a function that receives a view of one type (Content) and returns a new view of another type (Body).

With a small adjustment, our preview attempt at a CardView can be turned instead into a Card view modifier:

struct Card : ViewModifier { func body(content: Content) -> some View { content .padding() .background(Color.white) .cornerRadius(8) .shadow(radius: 4) } }

We could use this directly, but just as we did with custom alignments it’s worth spending the extra few lines of code to properly integrate it.

To do that we define an extension to View that applies our new Card as a modifier to the view it’s called on:

extension View { func card() -> some View { modifier(Card()) } }

Now to use this, all we need to do is add .card just like we do for any other modifier:

struct ContentView: View { var body: some View { HStack { Image("brawler") Text("Sir Bunnington") .font(.title) } .card() } }

Environment

For the battle tracker, it turns out that having the hit points unaligned gives a poor user experience, and we want a way to use the specified font’s monospaced digit alternative if available.

Because the actual view is low down the hierarchy, but the decision about which font to use is high up, this is the kind of scenario in which environment it useful.

Fortunately view modifiers participate in the lifecycle just as views do, so @Environment and similar work inside a custom ViewModifier just as they do inside a custom View.

One approach we might take would be:

struct MonospacedDigit : ViewModifier { @Environment(\.font) var font: Font? func body(content: Content) -> some View { return content .environment(\.font, font?.monospacedDigit()) } } extension View { func monospacedDigit() -> some View { modifier(MonospacedDigit()) } }

This kind of view modifier is quite common, it takes a property from the environment, modifiers it in some way, and then places the modified result back into the environment for its content.

Note that in this example we use .environment(\.font, …) to update the font, rather than .font directly, because it’s possible that there is no font in the current example and that modifier can’t accept nil. It would have been just as valid to have picked a default font, it depends on the needs of your project.


Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.

Categories
SwiftUI

Size-Limiting Frames

Occasionally we come across a layout where we need to limit the bounds of a view in a particular dimension. For example we might have a view where we show the character portrait along with the title they might be introduced by at court.

This works fine for the brawler:

character portrait with short title

But for the dragoon who has a bit of a khaleesi complex, it gets a little bit unwieldy and we need to limit the number of titles we actually show:

character portrait with long title

One way we can do this is with a .lineLimit on the Text, but when combined with accessibility font sizes, that can still grow taller than we intend. Sometimes we need to constrain a height in terms of pixels.

We might try to use a .frame with maxHeight specified:

struct ContentView : View { @ObservedObject var character: Character var body: some View { // ⚠️ This is an example that does not work. HStack { Image(character.imageName) Text(character.title) .frame(maxHeight: 200) } } }

This works great for the dragoon’s overly long title, allowing it to flow on as many lines as can fit in the space, and then truncating the rest:

character portrait with truncated title

But when we try it on the brawler, there’s a new problem:

character portrait with short title and too-large frame

We specified a maximum height for the frame intending to limit the height of the text, which it does, but the frame itself has chosen the height we specified as the maximum, rather than the height of the text within it.

If we review flexible frames we can understand why.

When we omit a constraint to .frame, the frame is layout-neutral for that constraint and chooses the size chosen by its child. But when we provide a value the frame is no longer layout-neutral for that constraint, and no longer considers the size chosen by the child.

The frame received a proposed size from its parent, and applied the maximum height constraint, proposing the maximum height to the Text child, which in the dragoon’s case resulted in the truncation of their overly long title.

In both cases the Text chose a size equal or smaller to that height. Since the frame was layout-neutral in terms of minimum height, this set the minimum height of the frame to the size of the child, but the maximum height was supplied by us. The size proposed by the frame’s parent was outside the range of these two heights, and was constrained by the frame: to its maximum height.

This wasn’t what we wanted, we wanted the frame to constrain the size of the child, but still be layout-neutral.

Fortunately there’s a solution for this, and it involves the third set of .frame parameters we didn’t consider yet, the ideal size. Since we didn’t specify any value for .idealHeight then the frame’s ideal height is layout-neutral, that is, the ideal height of the frame is the height of the child.

SwiftUI gives us modifiers that fix the size of a view to its ideal size:

/// Fixes this view at its ideal size. func fixedSize() -> some View /// Fixes the view at its ideal size in the specified dimensions. func fixedSize(horizontal: Bool, vertical: Bool) -> View

As we’re dealing with multi-line text we don’t want the first variant since text always ideally wants to be rendered on just one line, but the second variant is perfect since it will allow us to fix just the height of the frame.

We want to fix the size of the .frame so the modifier goes after it, rather then before—which would fix the size of the Text:

struct ContentView : View { @ObservedObject var character: Character var body: some View { HStack { Image(character.imageName) Text(character.title) .frame(maxHeight: 200) .fixedSize(horizontal: false, vertical: true) } } }

The dragoon’s long title renders exactly as before, constrained by the maximum height of the frame:

character portrait with truncated title and correct-sized frame

But now the frame around the brawler’s title is fixed in height to the frame’s ideal size, that of the brawler’s title, and does not expand the stack unnecessarily:

character portrait with short title and correct-sized frame

Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.