DEV Community

Daniil Kornilov
Daniil Kornilov

Posted on Edited on

Seven SwiftUI design mistakes illustrated with small examples

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.

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

One option: Keep view rendering small and move networking into a service. A view model can coordinate presentation state; MVVM is one possible design.

@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
    }
}
Enter fullscreen mode Exit fullscreen mode

Mistake #2: Abusing @State for Everything

@State owns view-local state. Bindings can share access to that state. Observation and ObservableObject use different ownership patterns; persistence is a separate concern.

Rule of thumb:

  • Data owned by this view only → @State
  • Data passed from parent → @Binding or let
  • 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Do this:

var body: some View {
    VStack {
        HeaderSection()
        ProfileCard(user: user)
        StatsSection(stats: stats)
        PostsList(posts: posts)
        FooterView()
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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.

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)