SwiftUI Pain Points Are Still Active

May 11, 2026#swiftui#ios#swift

SwiftUI is no longer new. It is used in production apps, Apple keeps expanding it every year, and for many screens it is now the fastest way to build good native UI.

But the community conversation is still not “SwiftUI solved everything.” It is more honest than that. Developers like SwiftUI, ship with SwiftUI, and still run into sharp edges around navigation, performance, previews, layout, OS availability, and UIKit interoperability.

That tension is worth understanding. SwiftUI is not a toy, but it is also not a complete replacement for every UIKit or AppKit workflow.

The good part is real

SwiftUI is excellent for a large amount of app UI:

  • forms
  • settings screens
  • onboarding
  • dashboards
  • lists with moderate complexity
  • widgets
  • watchOS apps
  • rapid prototyping
  • state-driven feature screens

Its biggest strength is not that it uses fewer lines of code. The bigger win is that the UI can be described as a function of state. When the state changes, the view updates. That model is powerful, especially when paired with modern tools like Observation.

For many teams, the question is no longer “Can we use SwiftUI?” The question is “Where does SwiftUI fit cleanly, and where do we still need UIKit?”

Navigation still takes discipline

SwiftUI navigation has improved a lot since the early NavigationView days. NavigationStack gives developers a more explicit way to model navigation state with a path.

That is good, but it also means navigation becomes application state. Once an app has deep links, tabs, modals, authentication flows, restoration, and programmatic routing, navigation is no longer just a few NavigationLinks.

The common pain points are:

  • deciding whether the path should live in a view, coordinator, router, or model
  • keeping deep links and navigation paths consistent
  • coordinating sheets, alerts, tabs, and pushed screens
  • avoiding view code that turns into routing code
  • restoring navigation state without overengineering it

Apple’s documentation explains how NavigationStack manages path state, but architecture is still left to each app. That is where teams often diverge.

A small app can get away with navigation links in views. A bigger app usually benefits from making routes explicit:

enum Route: Hashable {
  case article(id: Article.ID)
  case settings
}

struct RootView: View {
  @State private var path: [Route] = []

  var body: some View {
    NavigationStack(path: $path) {
      ArticleList { article in
        path.append(.article(id: article.id))
      }
      .navigationDestination(for: Route.self) { route in
        switch route {
        case .article(let id):
          ArticleDetail(id: id)
        case .settings:
          SettingsView()
        }
      }
    }
  }
}

This is more code than a simple NavigationLink, but it gives deep links, restoration, and programmatic navigation one shared representation.

Performance problems are usually about identity and invalidation

SwiftUI performance is a recurring community topic because performance issues often feel indirect. You do not always see one obvious slow method. Instead, you see body invalidations, unstable identity, layout recalculation, or expensive work happening inside view construction.

Long lists are where this becomes visible. Jacob Bartlett’s SwiftUI Scroll Performance: The 120FPS Challenge is a good practical look at scrolling, dynamic cell heights, memory, and frame drops. Community discussions like Is SwiftUI finally as fast as UIKit in iOS 26? show that developers are still comparing SwiftUI with UIKit for high-throughput UI.

The boring fixes still matter:

  • keep row identity stable
  • avoid generating IDs inside body
  • avoid expensive formatting, image processing, or data transformation inside views
  • keep observable state narrow
  • avoid invalidating large view trees for tiny changes
  • be careful with type erasure such as AnyView in large lists

The AnyView topic keeps coming up because it can hide structure from SwiftUI’s diffing system. There are useful discussions in the Swift Forums, including SwiftUI and AnyView: Performance benchmarks.

One easy mistake is giving SwiftUI unstable identity:

// Bad: every render creates new identities.
ForEach(articles.map { ArticleRowModel(id: UUID(), article: $0) }) { row in
  ArticleRow(row: row)
}

// Better: use stable identity from your data.
ForEach(articles) { article in
  ArticleRow(article: article)
}

Another common problem is doing work while building the view:

// Bad: repeated date formatting during view updates.
Text(DateFormatter.localizedString(
  from: article.publishedAt,
  dateStyle: .medium,
  timeStyle: .none
))

// Better: format in the model, view model, or a cached formatter.
Text(article.publishedDateText)

SwiftUI can be fast. It just rewards understanding identity and invalidation more than UIKit did.

Previews are still a developer-experience complaint

SwiftUI previews are one of the best ideas in Apple development tooling. When they work, they shorten the feedback loop dramatically.

The problem is that developers still complain about reliability. Large projects, generated code, networking, package dependencies, app extensions, and complex build settings can make previews slow or fragile. A recent community thread titled SwiftUI previews are still a mess in 2025 captures that frustration well.

The practical response is to design views so they preview easily:

  • inject sample data instead of live services
  • keep networking out of views
  • isolate large feature views into smaller components
  • provide lightweight preview models
  • avoid requiring app-wide setup for every preview

That is good architecture anyway, but previews make the cost of bad boundaries visible.

The difference is obvious in preview code:

// Hard to preview: the view creates its own live dependency.
struct HardToPreviewProfileView: View {
  private let client = APIClient.live

  var body: some View {
    ProfileContent(client: client)
  }
}

// Easier to preview: dependency comes from outside.
struct ProfileView: View {
  let client: APIClient

  var body: some View {
    ProfileContent(client: client)
  }
}

#Preview {
  ProfileView(client: .mock)
}

UIKit escape hatches are still normal

Many production apps are hybrid. They use SwiftUI where it is productive and UIKit where they need mature controls, lower-level rendering, precise lifecycle behavior, or APIs that SwiftUI does not expose cleanly.

That is not a failure. SwiftUI was designed to interoperate with UIKit and AppKit through representable wrappers and hosting controllers. The pain point is deciding when the escape hatch is worth it.

Community discussions like UIKit or SwiftUI? where do you stand in 2025? keep returning to the same pattern: SwiftUI is great for most screens, but advanced text rendering, camera flows, maps, custom transitions, AR, Metal, and highly tuned scroll experiences can still push developers back toward UIKit or lower-level frameworks.

The healthy approach is not purity. It is choosing the right layer:

  • SwiftUI for state-driven app UI
  • UIKit/AppKit for mature imperative controls and precise lifecycle hooks
  • Core Animation, Metal, AVFoundation, or custom rendering when frame-level control matters

Sometimes the right answer is a small bridge instead of fighting SwiftUI:

struct LegacyTextView: UIViewRepresentable {
  let text: String

  func makeUIView(context: Context) -> UITextView {
    let view = UITextView()
    view.isEditable = false
    return view
  }

  func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = text
  }
}

That keeps the escape hatch isolated. The rest of the screen can stay SwiftUI.

OS availability shapes architecture

SwiftUI improves through the OS. That is both a strength and a problem.

The newest APIs often require the newest OS versions. If your app supports older iOS releases, you may need availability checks, fallback paths, or older patterns. This is especially noticeable with navigation, Observation, SwiftData, widgets, and newer presentation APIs.

That creates a real product decision. If your users upgrade quickly, adopting the latest SwiftUI APIs is easier. If your app supports enterprise, education, medical, or long-tail consumer devices, your architecture has to absorb more compatibility work.

This is one reason developers still debate UIKit versus SwiftUI for complex apps. UIKit has a longer compatibility history. SwiftUI moves faster, but that speed can fragment your code when deployment targets are conservative.

Availability checks are normal, but they can spread quickly if you do not contain them:

@ViewBuilder
var editor: some View {
  if #available(iOS 17, *) {
    ModernEditor()
  } else {
    LegacyEditor()
  }
}

This is fine for one feature. If half the app looks like this, the deployment target is now an architectural constraint.

State management is better, but not automatic

Observation made SwiftUI state management cleaner. Apple’s Model data documentation now shows @Observable models owned with @State, and that is a better fit for modern SwiftUI than many old ObservableObject patterns.

Still, state architecture is not solved just because the wrappers improved. Large apps still need clear answers:

  • Who owns this state?
  • Is it local, feature-level, or app-wide?
  • Does it need persistence?
  • Does it drive navigation?
  • Can a child mutate it directly, or should it send an action?
  • How much of the view tree should update when it changes?

SwiftUI makes simple state easy. It does not remove the need for architecture.

A useful rule is to keep ownership close to the feature and pass only what the child needs:

@Observable
class ArticleEditorModel {
  var title = ""
  var body = ""
  var isSaving = false
}

struct EditorScreen: View {
  @State private var model = ArticleEditorModel()

  var body: some View {
    Form {
      TitleField(title: $model.title)
      BodyField(text: $model.body)
      SaveButton(isSaving: model.isSaving)
    }
  }
}

The child views do not need the whole model if they only edit one field. Smaller inputs make view updates and dependencies easier to reason about.

The real conclusion

SwiftUI is absolutely worth using. It is the direction of Apple UI development, and it keeps getting more capable.

But the active pain points are real:

  • navigation requires architectural discipline
  • performance depends on identity, invalidation, and layout behavior
  • previews are powerful but still fragile in large projects
  • UIKit escape hatches remain part of production development
  • OS availability affects adoption
  • state management still requires careful ownership boundaries

The best SwiftUI teams are not the ones pretending UIKit is dead. They are the ones that know SwiftUI well enough to use it confidently, and UIKit well enough to reach for it when the abstraction is the wrong tool.

That is probably the most realistic SwiftUI mindset in 2026: use it heavily, understand its model deeply, and keep your escape hatches clean.