Swift Best Practices for iOS Apps in 2026

For Swift in 2026, adopt strict concurrency and Swift 6 language mode from day one, mark view models @MainActor, use @Observable over ObservableObject, and make types Sendable so the compiler catches data races before your users do.
Part of the complete app development guide.
On this page
- Start with strict concurrency, not after
- Sendable is not optional anymore
- Treat optionals like the contract they are
- Prefer value types until you have a reason not to
- Keep views dumb, put logic elsewhere
- Write tests that survive refactors
- Ship with tooling that catches problems early
- Where local context matters
- FAQ
- Do I have to migrate to Swift 6 concurrency right now?
- Is SwiftUI ready for production apps in 2026?
- How much test coverage is enough?
Most Swift codebases don’t fail because of one bad decision. They rot slowly: force unwraps that never crash until they do, view controllers doing six jobs, and async code tangled into callbacks nobody wants to touch. Here’s what I actually reach for when building iOS apps in 2026, and the habits that keep a project healthy two years in.
Start with strict concurrency, not after
Swift 6 landed with strict concurrency checking as the default posture, and if you’ve been putting off the migration, the cost only grows. The good news is the language gives you clear tools now. Mark your view models and services as actors or @MainActor where appropriate, and let the compiler find the races before your users do.
@MainActor
@Observable
final class ProfileViewModel {
private(set) var profile: Profile?
private(set) var isLoading = false
private let service: ProfileService
init(service: ProfileService) {
self.service = service
}
func load() async {
isLoading = true
defer { isLoading = false }
do {
profile = try await service.fetchProfile()
} catch {
// surface a user-facing error state here
}
}
}
Note the @Observable macro. The old ObservableObject plus @Published combo still works, but the observation system introduced in iOS 17 is leaner and does less unnecessary redraw work. If you’re starting fresh in 2026, use it.
Sendable is not optional anymore
When data crosses an isolation boundary, it needs to be Sendable. Structs with value-type members are usually fine. Classes are not, unless you mark them final and make their stored properties immutable, or wrap access in an actor. Fighting this is a waste of time. Structure your types so the compiler agrees with you.
Treat optionals like the contract they are
Force unwrapping is the single most common source of crashes I see in code review. There are exactly two situations where ! is defensible: an IBOutlet that genuinely cannot be nil after load, and a value you’ve just proven non-nil in the same expression. Everywhere else, unwrap properly.
// Don't
let name = user.profile!.displayName
// Do
guard let profile = user.profile else {
return
}
let name = profile.displayName
// Or, when a default genuinely makes sense
let name = user.profile?.displayName ?? "Guest"
Optional chaining and guard let aren’t just safer, they read better. A reviewer can see the failure path. That matters when someone else inherits your file.
Prefer value types until you have a reason not to
Structs are the default for models, view state, and anything that gets copied around. They’re thread-safe by nature, they play well with SwiftUI’s diffing, and they avoid the shared-mutable-state bugs that plague class-heavy code. Reach for a class when you need identity, inheritance (rare), or reference semantics for a real reason, like a long-lived service or a cache.
- Models and DTOs: struct
- View state and view models: struct or a
@MainActorclass - Services, caches, coordinators: class or actor
- Anything shared across threads: actor
Keep views dumb, put logic elsewhere
SwiftUI makes it easy to stuff business logic into a view body. Don’t. A view should describe what the screen looks like given some state. Derive that state in a view model or a plain function, then test it without spinning up a UI.
struct OrderRow: View {
let order: Order
var body: some View {
HStack {
Text(order.title)
Spacer()
Text(order.total, format: .currency(code: "USD"))
}
}
}
The formatting, the currency code, the “is this overdue” logic: none of that belongs in the row. Keep it in the model or a helper so you can unit test it. When a screen gets complex, this separation is the difference between a two-hour change and a two-day one.
Write tests that survive refactors
Test behavior, not implementation. If your test breaks every time you rename a private method, it’s testing the wrong thing. Use Swift Testing (the newer framework that replaces a lot of XCTest boilerplate) for your unit tests and lean on it.
import Testing
@Test func totalAppliesDiscount() {
let order = Order(items: [.init(price: 10), .init(price: 20)])
let total = order.total(discount: 0.10)
#expect(total == 27.0)
}
Keep tests fast and deterministic. No network calls, no real clocks, no shared singletons. If a test needs a dependency, inject a fake.
Ship with tooling that catches problems early
A few habits pay off across every project:
- Turn on SwiftLint or SwiftFormat in CI so style debates stop happening in review.
- Enable strict concurrency warnings as errors once you’ve migrated. It keeps regressions out.
- Use Xcode Cloud or a CI runner that builds and tests on every PR. Catching a broken build on your machine is fine; catching it after merge is not.
- Profile with Instruments before you guess at performance. Most “slow” apps are doing redundant work on the main thread.
Where local context matters
If you’re building for a New York audience, two things come up more than people expect: offline tolerance and fast cold starts. Commutes on the LIRR or subway mean spotty connections, so design for cached data and graceful retries rather than assuming a strong signal. And users here notice a laggy launch. Keep your startup path lean: defer non-critical work, don’t block the first frame on network calls, and measure with the launch profiler.
None of this is exotic. It’s the boring discipline that separates an app you can still ship features into next year from one you’re afraid to touch. If you’re weighing whether to bring in help on a build, I do custom web development and mobile work for teams that want the foundation done right the first time.
FAQ
Do I have to migrate to Swift 6 concurrency right now?
You don’t have to flip everything at once, but you should start. Enable strict checking per module, fix the warnings, and expand from there. Waiting makes the eventual migration harder, not easier.
Is SwiftUI ready for production apps in 2026?
Yes, for most apps. The remaining gaps are narrow, mostly around very custom rendering or deep UIKit interop. For typical business and consumer apps, SwiftUI is the right default, and you can drop to UIKit where you truly need it.
How much test coverage is enough?
Coverage percentage is a weak goal. Aim to test your business logic, your edge cases, and anything that has broken before. Chasing 100% usually means writing tests that don’t catch real bugs.


