When I started learning SwiftUI two years ago, I made every mistake in the book. After building 10+ production apps, here are the most common pitfalls I see beginners fall into — and how to fix each one.
1. Putting Everything in One View
The mistake:
struct ContentView: View {
@State private var username = ""
@State private var password = ""
@State private var isLoggedIn = false
@State private var posts: [Post] = []
@State private var isLoading = false
// ... 20 more state variables
var body: some View {
// 200+ lines of nested views
}
}
The fix: Break your views into small, focused components. Each view should do ONE thing.
struct LoginView: View {
@State private var username = ""
@State private var password = ""
var body: some View {
VStack(spacing: 16) {
UsernameField(text: $username)
PasswordField(text: $password)
LoginButton(username: username, password: password)
}
}
}
Rule of thumb: If your view file is over 100 lines, it's time to extract subviews.
2. Using @State When You Need @StateObject
The mistake:
struct ProfileView: View {
@State var viewModel = ProfileViewModel() // WRONG
var body: some View {
Text(viewModel.name)
}
}
The problem: @State will recreate your view model every time the parent view re-renders.
The fix:
struct ProfileView: View {
@StateObject var viewModel = ProfileViewModel() // CORRECT
var body: some View {
Text(viewModel.name)
}
}
Quick rule:
-
@StateObject— you CREATE the object (owner) -
@ObservedObject— you RECEIVE the object (child) -
@EnvironmentObject— you access a SHARED object
3. Force Unwrapping Optionals in Views
The mistake:
Text(user.name!) // Crash if nil
Image(user.avatarURL!) // Crash if nil
The fix: Always provide fallbacks:
Text(user.name ?? "Anonymous")
if let avatarURL = user.avatarURL {
AsyncImage(url: avatarURL)
} else {
Image(systemName: "person.circle.fill")
}
Your app should never crash because of a nil value in the UI.
4. Ignoring the Environment
The mistake: Hardcoding values that should adapt:
Text("Hello")
.foregroundColor(.black) // Invisible in dark mode!
.font(.system(size: 16)) // Ignores accessibility
The fix: Use semantic colors and dynamic type:
Text("Hello")
.foregroundStyle(.primary) // Adapts to dark mode
.font(.body) // Respects user's text size settings
Always test your app in:
- Dark mode
- Large text sizes (Accessibility)
- Different device sizes
5. Nesting NavigationStack Inside Child Views
The mistake:
struct ContentView: View {
var body: some View {
NavigationStack {
DetailView()
}
}
}
struct DetailView: View {
var body: some View {
NavigationStack { // DOUBLE NavigationStack!
Text("Detail")
}
}
}
This creates a double navigation bar and weird behavior.
The fix: Only ONE NavigationStack at the root level:
struct ContentView: View {
var body: some View {
NavigationStack {
DetailView() // No NavigationStack here
}
}
}
6. Making Network Calls in View init or body
The mistake:
struct PostsView: View {
@State var posts: [Post] = []
var body: some View {
List(posts) { post in
Text(post.title)
}
// This gets called on EVERY re-render:
.onAppear { loadPosts() }
}
}
onAppear fires every time the view appears, including when you come back from a navigation push.
The fix: Use .task and track loading state:
struct PostsView: View {
@State var posts: [Post] = []
@State var hasLoaded = false
var body: some View {
List(posts) { post in
Text(post.title)
}
.task {
guard !hasLoaded else { return }
posts = await API.fetchPosts()
hasLoaded = true
}
}
}
.task is automatically cancelled when the view disappears — no memory leaks.
7. Not Using Preview Properly
The mistake: Writing code, building to simulator, checking the result, going back to code. Slow feedback loop.
The fix: Use #Preview for instant feedback:
#Preview {
ProfileCard(user: .preview)
.padding()
}
// Create preview data:
extension User {
static var preview: User {
User(name: "Daniil", role: "iOS Developer")
}
}
Previews save hours of development time. Use them for every view.
8. Using GeometryReader Everywhere
The mistake:
GeometryReader { geo in
Text("Hello")
.frame(width: geo.size.width * 0.8)
}
GeometryReader takes up all available space and breaks layouts.
The fix: Use built-in layout modifiers first:
Text("Hello")
.frame(maxWidth: .infinity) // Full width
.padding(.horizontal) // With padding
// Or use percentages with flexible frames:
HStack {
Color.blue.frame(maxWidth: .infinity) // 50%
Color.red.frame(maxWidth: .infinity) // 50%
}
Only use GeometryReader when you truly need the parent's exact dimensions.
9. Forgetting about Animation
The mistake: Abrupt state changes with no transitions:
Button("Toggle") {
showDetail = true // Instant, jarring change
}
The fix: Add withAnimation for smooth transitions:
Button("Toggle") {
withAnimation(.spring(duration: 0.3)) {
showDetail = true
}
}
// Or use the .animation modifier:
Rectangle()
.frame(width: isExpanded ? 200 : 100)
.animation(.easeInOut, value: isExpanded)
Subtle animations make your app feel polished and professional.
10. Not Handling Loading and Error States
The mistake:
struct PostsView: View {
@State var posts: [Post] = []
var body: some View {
List(posts) { post in
PostRow(post: post)
}
}
}
What does the user see while data is loading? An empty screen. What if the request fails? Still an empty screen.
The fix: Always handle three states:
struct PostsView: View {
@State var posts: [Post] = []
@State var isLoading = true
@State var error: Error?
var body: some View {
Group {
if isLoading {
ProgressView("Loading posts...")
} else if let error {
ContentUnavailableView(
"Failed to load",
systemImage: "wifi.slash",
description: Text(error.localizedDescription)
)
} else if posts.isEmpty {
ContentUnavailableView.search
} else {
List(posts) { post in
PostRow(post: post)
}
}
}
.task { await loadPosts() }
}
}
Quick Reference Table
| Mistake | Fix |
|---|---|
| Massive views | Extract subviews at 100 lines |
| @State for objects | Use @StateObject |
| Force unwrapping | Provide default values |
| Hardcoded colors | Use semantic styles |
| Nested NavigationStack | One at root level |
| onAppear for API calls | Use .task modifier |
| No previews | Create #Preview for every view |
| GeometryReader overuse | Use built-in layout first |
| No animations | Add withAnimation |
| Missing loading states | Handle loading/error/empty |
Want More SwiftUI Resources?
I've put together a collection of free resources for iOS developers — SwiftUI checklists, architecture templates, and practical guides.
Check them out:
What's the worst SwiftUI mistake you've made? Share in the comments!
Follow me for more SwiftUI content: @peasee163
Top comments (0)