Memoji of Jacob giving a thumbs up

Responsive View Components

I used to write my iOS views to be pixel perfect, until I discovered UIKit's Preferred Font method and how it worked. After that, I struggled to implement views that were useful at scale, or even beautiful on smaller screens like that of the iPhone 8. Eventually I designed a UIKit component that accounted for accessible font sizes. In this article we'll remake that component in SwiftUI.


The Problem

A debit card transaction represented in an HStack





We have a layout that works perfectly in a SwiftUI HStack. This view contains one Image and two Text views. Looks great right?

Well...

Let's look at it after bumping our preferred font size.


A debit card transaction represented in an HStack with very large font

Nope!


That looks like garbage. We need to respond to the font size increase, and change the layout of this component.

Good news is, the SwiftUI team has given us easy access to the user's preferred font size.


Responsive Stack View

For this tutorial, we'll focus on a simple view that responds to accessible font sizes. To do that we'll use the DynamicTypeSize Environment variable in SwiftUI.

On it, there is a computed property, isAccessibilitySize, that returns what we're looking for. If the user has selected to use large fonts and has scaled their font up into the larger fonts category, isAccessibilitySize returns true.

We can add use it like this:

public struct SomeView : View {
    @Environment(\.dynamicTypeSize) public var dynamicTypeSize: DynamicTypeSize

    public var body: some View {
        if dynamicTypeSize.isAccessibilitySize {
            Text("isAccessibilitySize == true")
        } else {
            Text("isAccessibilitySize == false")
        }
    }
}



That's great! But how do I pass in the content I want?



To make our own stack view we'll need to be able to pass in a bunch of subviews. We can do this with a ViewBuilder.

A ViewBuilder is a property wrapper that allows us to pass one or more views into our stack view as a function.

Alright! Let's put the Responsive Stack View together using the DynamicTypeSize and a ViewBuilder!

struct RStack<Content: View> : View {
    @Environment(\.dynamicTypeSize) var dynamicTypeSize: DynamicTypeSize
    
    let content: () -> Content
    
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    
    var body: some View {
        if dynamicTypeSize.isAccessibilitySize {
            VStack { content() }
        } else {
            HStack { content() }
        }
    }
}


Let's see what it looks like!


A debit card transaction represented in an RStack



Notice how the RStack has the same syntax of the HStack and VStack? That's the ViewBuilder doing it's magic.


let debitCardImageName = "creditcard.fill" // #DebtIsDumb

struct ContentView: View {
    var body: some View {
        RStack {
            Image(systemName: debitCardImageName)
                .foregroundColor(.red)
                .padding(.trailing)
            
            Text("Joe's Coffee Shop")
            Spacer()
            Text("$2.34")
        }.padding()
    }
}

A debit card transaction represented in an RStack with a spacer and very large font



Much Better!


We're getting closer! Although, that spacer is a bit of a problem...

Let's make another component to handle that little detail. After all, practice makes permanent.

For this component, we want to make sure we add a spacer for a horizontal layout, but not a vertical layout. We won't need to use a ViewBuilder because we are only going to be working with 2 Text views.

Let's do it!

Practice makes permanent


struct RDualText: View {
    @Environment(\.dynamicTypeSize) var dynamicTypeSize: DynamicTypeSize

    var firstText: Text
    var secondText: Text
    var spacer = Spacer()
    
    init(firstText: Text, secondText: Text) {
        self.firstText = firstText
        self.secondText = secondText
    }
    
    var body: some View {
        RStack {
            self.firstText
            if !dynamicTypeSize.isAccessibilitySize {
                spacer
            }
            self.secondText
        }
    }
}


Putting it all together


Alright, let's go back to our content view, and use our new component.

A debit card transaction represented in an RStack




struct ContentView: View {
    var body: some View {
        RStack {
            Image(systemName: debitCardImageName)
                .foregroundColor(.red)
            RDualText(
                firstText: Text("Joe's Coffee Shop"),
                secondText: Text("$2.34")
            )
        }.padding()
    }
}

Accessible apps don't have to be ugly.

Good work! You've learned how to create a ViewBuilder, and respond to a user's preferred font size! Making a component that accounts for a user's accessibility settings was far more difficult in the world of UIKit. But we've only scratched the surface, there is a lot more to handling accessibility well than what we covered here. There are settings for color blindness, preferring bold fonts, reducing motions and more. And that is just the visual side of accessibility!


Homework


If you'd like to learn more, I have a challenge for you!

Take the RStack component and modify it to take a threshold. If the selected font size is greater than the threshold then the RStack flips from horizontal to vertical. When you instantiate your RStack, it should look like this:

RStack(threshold: .accessibilityMedium) {
    Text("Top")
    Spacer()
    Text("Bottom")
}
Tags: