Overview
SwiftUI is the default UI stack for new iOS work. Apple’s OS and Xcode versions now track the year: iOS 27 and Xcode 27 (Swift 6.4) shipped in September 2026, following iOS 26 and Xcode 26. New projects created in Xcode 26 or later default to MainActor isolation, so view and view-model code needs no @MainActor annotation (swift-concurrency). Language rules are in swift; persistence is in swiftdata and core-data.
Use @Observable for model state and own it with @State
Use the Observation macro (iOS 17+) instead of ObservableObject and @Published; it tracks reads per property, so a view re-renders only for properties it read. Own the model in the view that creates it with @State, and pass it to children as a plain parameter or with @Bindable for two-way bindings.
@Observable
final class FeedModel {
var posts: [Post] = []
func load() async { /* ... */ }
}
struct FeedView: View {
@State private var model = FeedModel()
var body: some View {
List(model.posts) { PostRow(post: $0) }.task { await model.load() }
}
}With the iOS 27 SDK, @State initializes an @Observable class lazily, once per view lifetime (back-ported to iOS 17), so FeedModel() is no longer rebuilt on every view re-init. The matching source break: assigning a @State property inside init() when it also has a default value is now a compile error. Remove the default, as Apple’s TN3211 describes.
Drive navigation with a typed path
Keep the path as @State and map destination types to views once at the root. Deep links and state restoration then mutate the array.
enum Route: Hashable { case post(Post.ID), user(User.ID) }
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: Route.self) { route in
switch route {
case .post(let id): PostDetail(id: id)
case .user(let id): UserDetail(id: id)
}
}
}Inject dependencies through the environment
Use @Environment instead of singletons so tests and previews swap implementations at the root. The @Entry macro replaces the old EnvironmentKey boilerplate.
extension EnvironmentValues {
@Entry var analytics: any Analytics = NoopAnalytics()
}
RootView().environment(\.analytics, FirebaseAnalytics())Pass an @Observable model the same way with .environment(model) and read it with @Environment(Model.self).
Preview every state with stub data
Use #Preview with inline data for loading, empty, and error states. Previews that touch the network or disk are flaky. Add a static func preview(...) on each model so previews and tests share the stub, and use @Previewable @State for state a preview needs.
Keep body cheap and declarative
body re-runs on every dependency change, so move sorting, filtering, and formatting into the model. Use .task(id:) for async work tied to a value (it cancels and restarts when the id changes), and .onChange(of:) only for side effects on a transition. Extract subviews when body stops fitting on one screen. Profile with the Instruments SwiftUI template rather than guessing, and use Self._printChanges() to see what invalidated a view. Xcode 27 unifies the common builders under ContentBuilder, which Apple says substantially improves type-checking performance, including when you target earlier releases; small views still preview faster.
Let system components adopt the new design
Build with the current SDK so standard controls pick up the Liquid Glass appearance, and prefer system toolbars, tab bars, and sheets over custom chrome. Apple’s WWDC26 SwiftUI session says iPhone apps become resizable on iOS 27 (for example under iPhone Mirroring or on iPad) and to size views by size class rather than device idiom.
Treat accessibility as part of the view
Every icon-only control needs .accessibilityLabel; add .accessibilityHint only when the label is ambiguous. Informative images need a description. Check the largest accessibility Dynamic Type size in a preview and fix overflow with ViewThatFits or scrolling, then audit with the Accessibility Inspector.