alexis

software engineer, nyc

Byte of the Week: Accessible Stack Axis in SwiftUI

Since iOS 9 and the introduction of UIStackView, stacks have become a common building block on iOS. In SwiftUI, this functionality is provided by HStack and VStack.

The following layout is very common: it is a row in a list that shows a text on the left and a button on the right. You would typically use an HStack for this.

Screenshot of the list in default iOS font

But when a user increases their font size using Dynamic Type, you would end up with the following layout, which is not very intelligible:

Screenshot of the list in XXL accessibility font

One solution to this problem is accessing the sizeCategory property of your view's environment to determine whether to use an HStack or a VStack. You can do so with the @Environment property wrapper.

@Environment(\.sizeCategory) private var sizeCategory

In MetroBuddy, I created a reusable view component that automatically switches the axis of the stack based on the isAccessibilityCategory property of the size category.

struct FlexibleStack<Content: View>: View {
    let hStackAlignment: VerticalAlignment
    let vStackAlignment: HorizontalAlignment

    let contentBuilder: (Axis) -> Content
    @Environment(\.sizeCategory) private var sizeCategory

    init(hStackAlignment: VerticalAlignment, vStackAlignment: HorizontalAlignment, @ViewBuilder content: @escaping (Axis) -> Content) {
        self.hStackAlignment = hStackAlignment
        self.vStackAlignment = vStackAlignment
        self.contentBuilder = content
    }

    @ViewBuilder var body: some View {
        if sizeCategory.isAccessibilityCategory {
            VStack(alignment: vStackAlignment, spacing: 16) {
                contentBuilder(.vertical)
            }
        } else {
            HStack(alignment: hStackAlignment, spacing: 8) {
                contentBuilder(.horizontal)
            }
        }
    }
}

Example

Using the FlexibleStack view above, we can fix our row layout:

struct Row: View {
    var body: some View {
        FlexibleStack(hStackAlignment: .center, vStackAlignment: .leading) { axis in
            Text("Rain On Me (with Ariana Grande)")
            
            if axis == .horizontal {
                Spacer()            
            }
            
            Button("Play", action: {})
        }
    }
}

This is the result:

Comparison of the updated large vs. updated