Free giveaway: Win a ticket for Do iOS Conference. Learn more.
Free: Win a ticket for Do iOS.
Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

SwiftUI Architecture: Structure Views for Reusability and Clarity

As your SwiftUI projects grow, the need for a better SwiftUI architecture grows, as it’s easy for view bodies to become very long. You start with a simple screen, and before you realize it, your body includes dozens of nested VStacks, HStacks, and custom modifiers. At some point, scrolling through the body feels like reading a novel.

In this article, we’re going to look at a realistic example. It’s a large view that we want to break down into more readable pieces. We’ll explore different techniques that make your views readable, maintainable, and reusable — all without over-engineering.

Before we start: there’s no single way out. Use these techniques as inspiration and a toolkit to refine your perspectives as you go.


The Problem: A Growing View Body

Imagine we’re building a simple ArticleListView to show a list of articles:

struct Article {
    let title: String
    let url: URL
}

struct ArticleListView: View {
    let articles: [Article]

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Articles")
                .font(.largeTitle)
                .bold()

            ForEach(articles, id: \.url) { article in
                HStack {
                    VStack(alignment: .leading) {
                        Text(article.title)
                            .font(.headline)
                        Text(article.url.absoluteString)
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                    }
                    Spacer()
                    Image(systemName: "chevron.right")
                        .foregroundStyle(.gray)
                }
                .padding()
                .background(Color(.secondarySystemBackground))
                .cornerRadius(8)
            }
        }
        .padding()
    }
}

This view might look fine at first. But as soon as we add logic for empty states, loading indicators, or cell actions, things get messy. Even in its current state, we can already conclude a few things:

A SwiftUI architecture starts by scanning and detecting optimization possibilities.
A SwiftUI architecture starts by scanning and detecting optimization possibilities.

You’re basically looking at a picture of my mind when parsing a view into a SwiftUI Architecture. I always aim to make my views easy to scan, easy to read. The patterns I see transform into individual containers or subjects, which are an input for finding a proper solution. Which solution depends, and there are several ways to move forward.

FREE 5-day email course: The Swift Concurrency Playbook by Antoine van der Lee

FREE 5-Day Email Course: The Swift Concurrency Playbook

A FREE 5-day email course revealing the 5 biggest mistakes iOS developers make with with async/await that lead to App Store rejections And migration projects taking months instead of days (even if you've been writing Swift for years)

The Temptation: Extensions and Computed Properties

Many developers try to reduce the body size by moving parts of it into extensions. The extensions live in the same file and often add to the length of the file itself. In my opinion, readability does not improve, and we end up with even more lines of code to scan.

Here’s an example of our ArticleListView, but with subviews migrated into an extension:

extension ArticleListView {
    var header: some View {
        Text("Articles")
            .font(.largeTitle)
            .bold()
    }

    func articleRow(for article: Article) -> some View {
        HStack {
            VStack(alignment: .leading) {
                Text(article.title)
                    .font(.headline)
                Text(article.url.absoluteString)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
            Spacer()
            Image(systemName: "chevron.right")
                .foregroundStyle(.gray)
        }
        .padding()
        .background(Color(.secondarySystemBackground))
        .cornerRadius(8)
    }
}

And then they call those helpers in the body:

var body: some View {
    VStack(alignment: .leading, spacing: 8) {
        header
        ForEach(articles, id: \.url, content: articleRow)
    }
    .padding()
}

This works, but it only moves code around — it doesn’t necessarily make your view more maintainable or reusable. The only advantage I see is improved readability of our body, but I prefer a solution that enhances both my body’s readability and the reusability and maintainability of my code. We need a better approach towards an improved SwiftUI Architecture.

The Better Approach: Extract and Reuse Intelligently

Instead of treating extensions and computed properties as dumping grounds, use them strategically.

Here are three better approaches:

1. Extract Dedicated SwiftUI Views

If a part of your view has its own concern, give it its own View:

struct ArticleRow: View {
    let article: Article

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(article.title)
                    .font(.headline)
                Text(article.url.absoluteString)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
            Spacer()
            Image(systemName: "chevron.right")
                .foregroundStyle(.gray)
        }
        .padding()
        .background(Color(.secondarySystemBackground))
        .cornerRadius(8)
    }
}

And use it in your list:

ForEach(articles, id: \.url) { article in
    ArticleRow(article: article)
}

Now your main view reads like a high-level overview — not an implementation detail. Each view ends up having a single responsibility. As soon as we add more states to ArticleRow, we can do this in a single place. It also allows us to more easily define SwiftUI previews for each state. We can #Preview SwiftUI Views using Macros or use @Previewable to easily make dynamic SwiftUI Previews.

2. Create Reusable View Modifiers

When you notice repeated styling, extract it as a modifier:

struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color(.secondarySystemBackground))
            .cornerRadius(8)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

Then use it anywhere:

ArticleRow(article: article)
    .cardStyle()

Modifiers make your design language consistent across views — and they’re incredibly easy to test and reuse. I find this one trickier to detect: when do you know something becomes a design language?

You’ll often find out after developing your app for a little longer. I often find myself copying the same modifiers from view to view. In those cases:

  1. Search for the combination of view modifiers
  2. Replace it with your new ViewModifier

The result allows you to update the styling of your app in a single place with reusable code. Btw, you don’t have to use a view modifier for this. You can also simply use a View extension:

extension View {
    func cardStyle() -> some View {
        self.padding()
            .background(Color(.secondarySystemBackground))
            .cornerRadius(8)
    }
}

In either case, consider improving discoverability using Static Member Lookup in Generic Contexts. It’s a technique that supports discoverability of your SwiftUI architecture solutions.

3. Build Generic View Extensions

If you find yourself writing similar layouts repeatedly, consider generic helpers.

For instance, showing a title section is common:

extension View {
    func sectionHeader(_ title: String) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.title3)
                .bold()
            self
        }
    }
}

You can now write:

VStack {
    ForEach(articles, id: \.url) { article in
        ArticleRow(article: article)
    }
}
.sectionHeader("Articles")

This kind of extension keeps your code readable while promoting a shared UI language. I sometimes prefer these over a SectionHeader view, as it reduces indentation in my code.

However, I realize this one is the most opinionated because it also becomes less clear what the view modifier is doing behind the scenes. Still, it’s how many view modifiers function, so it’s up to you to incorporate this into your coding style.

The Rule of Thumb for a Good SwiftUI Architecture

If you’re unsure whether to extract something, ask:

“Does this piece of UI have a clear purpose and potential to be reused?”

  • If yes → make it a new View or ViewModifier.
  • If not → keep it local or use a computed property.

The goal isn’t to minimize lines of code, but to improve clarity and reusability for a better SwiftUI architecture.

Conclusion

Large SwiftUI bodies are a sign of growth — not failure. They often mean your view evolved with your product. But knowing how to break things down can turn chaos into clean, composable, and testable components. By combining dedicated viewsmodifiers, and generic extensions, you’ll write SwiftUI that’s not just shorter — but smarter.

If you want to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.