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:

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
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:
- Search for the combination of view modifiers
- 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
orViewModifier
.
- 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 views, modifiers, 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!