Disclosure: This article was created or edited with AI assistance. Revised on 13 October 2026 to remove promotional links and unsupported outcome claims. It is educational guidance, not a verified personal case study or a measured result. Examples are illustrative; use only your own documented experience and measurements.
The sections below present options and illustrative examples. Adapt them to the requirements of your project or role; they do not establish a universal rule or promise a result.
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)
}
}
For an ObservableObject model, use StateObject when the view owns the object. State does not subscribe to that model's objectWillChange publisher. An @observable model uses different integration and can be owned with State.
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)
}
// Called when this view appears; body evaluation alone is not onAppear:
.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
}
}
}
SwiftUI requests cancellation of the view-associated task when its lifetime ends. Cancellation is cooperative; this does not guarantee the absence of retained objects or 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")
}
}
Use previews to inspect the states and layouts you need to support.
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 |
| ObservableObject ownership | Use StateObject when the view owns it; @observable has different integration |
| 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 |
What's the worst SwiftUI mistake you've made? Share in the comments!
Scope of the code examples
These are separate teaching snippets, not a complete tested application or a production-ready package. Match availability to your deployment target, provide omitted model/service definitions, and check errors, cancellation, accessibility and persistence before adapting them.
Observation and ownership
The StateObject/ObservedObject examples refer to ObservableObject. For an @observable model, use the Observation integration appropriate to its owner, including State for view-owned storage and Bindable when bindings are needed. See Apple's migration guide.
Top comments (0)