I've reviewed hundreds of SwiftUI projects. The same mistakes keep showing up — and they're not the ones you'd expect.
These aren't syntax errors. They're architectural decisions that seem fine at first but create real pain as your app grows.
Mistake #1: Putting Everything in the View
This is the most common one. Views that are 300+ lines with networking calls, state management, data transformation, and UI all mixed together.
The problem:
struct ProfileView: View {
@State private var user: User?
@State private var isLoading = false
@State private var posts: [Post] = []
@State private var error: String?
var body: some View {
VStack {
// 200 lines of UI code mixed with logic
}
.onAppear {
Task {
isLoading = true
let url = URL(string: "https://api.example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
user = try JSONDecoder().decode(User.self, from: data)
// ... more networking code
}
}
}
}
The fix: Use MVVM. Your View should only describe UI. Everything else goes in a ViewModel.
@MainActor
class ProfileViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var error: String?
func loadProfile() async {
isLoading = true
defer { isLoading = false }
// All logic lives here
}
}
struct ProfileView: View {
@StateObject private var vm = ProfileViewModel()
var body: some View {
// Clean UI code only
}
}
Mistake #2: Abusing @State for Everything
@State is for local, view-owned data. If data needs to be shared between views or persisted, you need a different tool.
Rule of thumb:
- Data owned by this view only →
@State - Data passed from parent →
@Bindingorlet - Shared data (ViewModel) →
@StateObject/@ObservedObject - App-wide data →
@EnvironmentObject
Getting this wrong leads to data being out of sync across your app.
Mistake #3: Not Breaking Down Views
If your view body has more than ~50 lines, it probably needs to be broken into subviews.
Don't do this:
var body: some View {
VStack {
// Header section (30 lines)
// Profile card (40 lines)
// Stats section (25 lines)
// Posts list (50 lines)
// Footer (20 lines)
}
}
Do this:
var body: some View {
VStack {
HeaderSection()
ProfileCard(user: user)
StatsSection(stats: stats)
PostsList(posts: posts)
FooterView()
}
}
Smaller views = easier testing, better previews, cleaner diffs.
Mistake #4: Ignoring the Environment
SwiftUI's environment system is incredibly powerful but underused. Instead of passing data through 5 levels of view hierarchy, inject it once.
// Instead of prop drilling:
ParentView(theme: theme) → ChildView(theme: theme) → GrandchildView(theme: theme)
// Use environment:
ParentView()
.environmentObject(theme)
// Any descendant can access it:
struct GrandchildView: View {
@EnvironmentObject var theme: ThemeManager
}
Mistake #5: Fighting Navigation Instead of Embracing It
Pre-iOS 16 navigation was painful. But many developers still use old patterns when NavigationStack exists.
// Modern navigation pattern
NavigationStack(path: $router.path) {
RootView()
.navigationDestination(for: Route.self) { route in
switch route {
case .profile(let id): ProfileView(id: id)
case .settings: SettingsView()
case .detail(let item): DetailView(item: item)
}
}
}
This gives you programmatic navigation, deep linking support, and clean architecture.
Mistake #6: Not Using ViewModifiers
If you're applying the same set of modifiers to multiple views, create a custom ViewModifier.
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
// Usage: clean and consistent
ProfileCard().cardStyle()
StatsCard().cardStyle()
Mistake #7: Skipping Previews
Previews aren't just a nice-to-have. They're your fastest feedback loop. Every view should have a preview with realistic data.
#Preview {
ProfileView()
.environmentObject(ThemeManager())
.environmentObject(Router())
}
#Preview("Loading State") {
ProfileView()
.environmentObject(ThemeManager())
}
#Preview("Error State") {
ProfileView()
.environmentObject(ThemeManager())
}
Preview every state: loading, error, empty, populated. Your future self will thank you.
The Pattern
Notice something? Most of these mistakes come from the same root cause: treating SwiftUI like UIKit.
SwiftUI has its own mental model. The faster you adopt it, the better your code becomes.
I've built 27 products with SwiftUI and packaged all these patterns into production-ready templates and components. If you want to skip the learning curve and write clean SwiftUI from day one:
t.me/SwiftUIDaily — daily tips, code reviews, and ready-to-use SwiftUI resources.
Which of these mistakes have you made? (No judgment — I made all of them.) Share your experience in the comments.
Top comments (0)