SwiftUI Design Systems


A diagram showing a design system facilitating communication across design, engineering, accessibility and user experience.
Design Systems Should Encourage Communication

Developing an Accessible SwiftUI Design System

When people of different disciplines talk about users they very frequently miscommunicate. Except for basic accessibility semantics the most common cause of major accessibility issues is miscommunication. Design system thought is well positioned to limit miscommunication and therefore lead to more accessible products.

Having well structured code that is clear and concise is essential. So is consideration for non-engineering actors that you need to discuss that code with. By writing code that is clear, concise, and understandable we are much more likely to get requirements and designs that are in sync with how SwiftUI supports solving problems.

Limiting miscommunication results in more accessible products.

These problems may seem trivial initially, but if not thought through can lead to spaghetti code when you get to supporting Font Scaling, Screen Orientation, Dark Mode, or iPad OS. In my opinion this starts with how you architect your SwiftUI code.

So I decided to write about the process of creating my own Design System. I call it N-ARIA, which stands for Native ARIA. ARIA is the language that accessibility experts use to talk about the web.

I'm going to start by building a NAriaTextField.

Getting Started

As an engineer I want to start with the code. Before I start building my Figma files I want to set some ground rules for my code and how I expect it to behave. I like to set an objective set of rules and hierarchy for architectural decisions. This allows me to know that I am being as equitable as I can be.

Here are the SwiftUI architectural rules I generally follow:

  1. Let SwiftUI do the hard work whenever possible.
    1. Avoid UIViewRepresentable unless absolutely necessary.
    2. Never require component consumers to reference Accessibility APIs.
  2. Our public APIs should be simple and concise.
    1. For components the annotation process should be considered and should not require reference to platform APIs.
    2. When all else is equal encourage healthy conversations about user experiences.

We hope to end up with something that looks approximately like this:

A simple form with Name as the title and fields for First, Middle, and Last. First and Middle are required.
A Simple SwiftUI Form

Let's go!

Accurate Semantics

We'll start by building a NAriaTextField. Something that allows us to enter text.

The first thing we need to do is to find the correct starting markup. How does SwiftUI "want" us to do this. Starting here is essential. By leveraging default SwiftUI components the operating becomes responsible for a lot of interactions, particularly those with Assistive Technology users, which are difficult for engineers to replicate in custom controls.

Roughly speaking, when we have a label next to an interactive control we want to do something like this:

struct NAriaTextField: View {
  LabeledContent(label) {
    TextField("", text: $value) 
  }
}

NariaTextField

Declarative UI Code should reflect the needs of the user. Here we have a TextField that we want to give a visible label. This code satisfies both of the following WCAG requirements:

  1. 1.3.1 - It must be associated with a nearby visible label.
  2. 4.1.2 - This code keeps you from having to reference Accessibility APIs to do the operating system's job.

Modular Styling

This is obviously incomplete! But, where next?

struct NAriaTextField: View {
  LabeledContent(label) {
    TextField("", text: $value) 
  }.labeledContentStyle(TextFieldStyle()) 
}

Adding TextFieldStyle Separately

Adding a custom LabelContentStyle is something that most SwiftUI engineers don't need to dabble with, however, for your Design System engineers out there this is critical. Having reusable style components is essential. Let's zoom into our TextFieldStyle code.

Styles and Layouts

The world "Style" is a little overloaded in SwiftUI. It can be more helpful to think of this as Layout or the things that a SwiftUI component needs to do to adapt to different devices and screen sizes. We will add conditional layouts based on screen size later.

This part of your code will grow substantially over various phases of development. Especially if you plan to support iOS, iPadOS and watchOS.

struct TextFieldStyle: LabeledContentStyle {
    
    // API
    var name: String
    var isRequired = false
        
    // Implementation
    func makeBody(configuration: Configuration) -> some View {
        VStack(alignment: .leading) {
            
            configuration.label.font(.callout)
                
            configuration.content.font(.body)
                .modifier(RoundedTextFieldModifier(isRequired:isRequired))
            
        }.accessibilityValue(isRequired ? "Required" : "")
            .accessibilityInputLabels([name])
    }
}

TextFieldStyle Provides Layout and Function

When we get a chance to peer into a custom LabeledContentStyle we can not only see how this code can become modular quickly, but understand how it is beneficial. Now we can focus our discussions with our designers around the combinations of intent our control is built to support.

  1. Name: A concise identifier for your control that at least includes the label text.
    1. Normally I would use the word Label here on iOS. But, in this context name is what we actually mean. It's not a visible label, it is the name for the purpose of being a Voice Control input label.
  2. IsRequired: Must the text in this TextField be entered in order to consider the input valid.

Modifiers

Another layer of styling refers to more particular visual styling. In SwiftUI this is handled with Modifiers. Modifiers allow you to change the visual appearance of a component without effecting its semantics. When building your API for these components consider facilitating conversations with those who care about clear communication of visual experiences.

struct RoundedTextFieldModifier: ViewModifier {
    
    // API
    var isRequired: Bool
    
    // Defaults for these makes great design discussions!
    var cornerRadius = 10.0
    var textColor = Color.black
    var borderColor = Color.black
    var padding = 6.0
    var strokeStyleRequired = StrokeStyle(lineWidth: 2.0)
    var strokeStyleOptional = StrokeStyle(lineWidth: 2.0, dash: [10])
          
    // Implementation
    func body(content: Content) -> some View {

        // We'll do this better later
        let strokeStyle = isRequired ? strokeStyleRequired : strokeStyleOptional

        content
            .padding(padding)
            .foregroundColor(textColor)
            .overlay(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .stroke(borderColor, style: strokeStyle)
            )
    }
}

RoundedTextFieldModifier Provides Visual Styling

This style of modularity is particularly valuable because it brings the look and feel of the user interface to the forefront of conversation for the actors that need to and isolates it from those who have other responsibilities. While also allowing you to nerd about about the colors and more subtle layouts and how they communicate with sited users!

Business Benefit

Sometimes accessibility experts struggle to argue for the business value of accessibility. This logic is the foundation of this argument. It surrounds the promise of well structured and architected SwiftUI applications. Any SwiftUI based platform can share the semantics of a NariaTextField, because all platforms know how to display text and accept text input.

The value of modularity and capabilities separation in your code cannot be understated, particularly the separation between layout, styling and semantics.

It allows you to do awesome things like support multiple form factors, devices, and even different platforms... from one code base. It also facilitates conversations around fine tuning visual experiences across a multitude of operating system and device categories.

This is how declarative frameworks like SwiftUI work from a design system perspective. Tell it what you want to do, give it a reasonable layout, and style it. So if you have modular layouts and styles within your design system you can get a lot of things for free. Such as dramatically simpler support for the following:

  1. Device Scaling
  2. Font Scaling
  3. Orientation
  4. Form Factor (iPhone, iPad, TV, Watch, Laptop)
  5. Operating System (iOS, iPadOS, tvOS, watchOS, MacOS)

Form Factor and Operating System are justifiable things to never consider. However, you must support Device Scaling, Font Scaling, and Orientation to be WCAG compliant. Modular semantics and styles are fundamental to accomplishing this maintainably.

Since you have to do it anyway do it right and make more money!

It also allows each of those efforts to evolve at the appropriate pace without fear of breaking things or running into process blockers with your various design oriented teammates.

Create reusable styles and ensure designs are styled in a way that is compatible with that style of thinking. We will dig into more styling code in the future, covering topics like:

  1. Text Scaling
  2. Colors and Styles
  3. Designing For Device

More to come on this topic. It is a blog post this size on its own. Let's continue...

Write Accessible Code

The last step we'll discuss is writing accessible code. And what I mean is, code that is clear and easy for everyone in the development process of that module to understand. It is inevitable that a user experience researcher, designer, project manager, engineer, etc... will be discussing code at some level.

If all participants in a meeting can understand how the business layer of code works it is much more likely that the resulting content will be accessible.

If teams are attempting to have these discussion in raw SwiftUI APIs like Modifiers, Styles, Views and AccessibilityElements they will struggle to empathize with the users and behaviors they are referring to. However, if we ignore the value of organizing by those constructs we will create SwiftUI that doesn't adapt well to Apple's framework updates.

Purposeful Component APIs

Embrace the tools the framework gives you while bringing a simple purposeful public API to the top of your files. Bring the appropriate user interface concerns to light in your public API so you can avoid conversations about SwiftUI specifics of accomplishing them:

struct NAriaTextField: View {

  // API
  var label: String
  var instructions: String
  var isRequired: Boolean = false    
  @State var value = ""

  // Implementation
  var body: some View {
    VStack(alignment: .leading) {     
      LabeledContent(label) {
        TextField("", text: $value).background(Color.gray)
          .accessibilityHint(instructions)
      }.labeledContentStyle(TextFieldStyle())
      Text(instructions)
    }
  }
}

Adding Annotatable Component APIs

Note how we have used our components API to hint at the purpose of those fields. This allows productive conversations over people and behaviors and how they would benefit a user. It also allows us to put very simple instructions and requirements in Annotations Kits and design system documentation.

  1. Label: The Name of the control.
  2. Instructions: A simple sentence, 50 characters max, that helps guide the user to providing the appropriate input.
  3. IsRequired: It is semantic in SwiftUI for boolean APIs to start with "is" to clarify whether or not true is true, or true is false.
    1. Defaults to false... sorry. Defaults to NOT required. 😅
  4. Value: The value of the text field which you can populate with an initial value if you'd like.
    1. Defaults to empty.

By simplifying our API down to the things that matter for user experience we facilitate productive conversations between complex disciplines, while leaving more technical conversations to more appropriate audiences. And may in fact be considering a different consumer of our API in different contexts.

Clear concise and understandable code leads to accessible and maintainable applications.

Clear Business Logic

The end product is being able to discuss behaviors that impact user experience over code that looks like:

struct SimpleForm: View {
  
  var title: String = "Name"
  
  @FocusState var focusState: Bool
  
  var body: some View {
    
    VStack {
      
      Text(title).font(.headline)
      
      Form {
        NAriaTextField(
          label: "First",
          instructions:"First Name is required.",
          isRequired: true
        )
        
        NAriaTextField(label:"Middle")
        
        NAriaTextField(
          label:"Last",
          instructions:"Last Name is required.",
          isRequired: true
        )
      }
    }
  }
}

A Simple Form

You don't have to be a SwiftUI expert to understand what is happening here. By building our components in this way we allow our teams to come together on the parts of the user experience they have to agree upon for any of this to make sense.

They can then be productive individually in making their parts of the experience work well for all users. UI Engineers can have deeper meetings about styles with UI Designers and UX Engineers can have deeply meaningful conversations with platform accessibility specialists about semantics.

Accessible Code gets used accessibly and opens the gates to effective cross discipline communication.

Conclusion

By understanding the way development teams get work done and the interactions that are taking place we can split our SwiftUI into pieces of conversation that need to take place to deliver quality products. Providing clear public APIs that consider the context and consumer of design and code review we facilitate productive conversations that help work move faster and innovation happen more rapidly.

Roughly speaking the categories of conversations we want to encourage and their actors as as follows:

  1. Semantics of components should be easy for accessibility experts, engineers, and user experience designers to discuss until their blue in the face.
  2. Styles and layouts should support your designers way of communicating with you and the tooling they use to do so... probably Figma.
  3. Business logic should be clear and concise and result in code that facilitates full team conversations including stakeholders.

By separating our code along these lines we are in harmony with the way SwiftUI wants to be written AND facilitate effective conversations in the correct contexts. This brings our designers and accessibility experts closer to the code in a way that leads to faster development processes and more accessible products that remain accessible when platform updates inevitably occur.

Which saves you time, money, and allows you to keep up with an ecosystem that expects perfection.

Thanks for reading. Much more to come.

Featured Posts

SwiftUI Design Systems

Device and Text Size

Applying WCAG 2.2

iOS Accessibility Test Plan