Swift Techniques That Actually Matter in 2026: A Practical Comparison

The techniques that matter in 2026 are the ones that remove whole classes of bugs: async/await over completion handlers, structured concurrency with task cancellation, actors for shared mutable state, and value types by default. Pick the one that fixes the problem in front of you.
Part of the complete app development guide.
On this page
- Async/await vs completion handlers
- Structured concurrency vs detached tasks
- Macros vs code generation vs plain generics
- Where macros go wrong
- Value types vs reference types
- SwiftUI vs UIKit in 2026
- How I actually choose
- FAQ
- Is async/await always faster than completion handlers?
- Should I rewrite my UIKit app in SwiftUI?
- When is a Swift macro worth writing?
Most “Swift technique” articles compare language features in a vacuum. That’s not how you pick a technique. You pick it because a specific problem is annoying you and one approach solves it with less code and fewer bugs. Here’s how I actually decide, with the tradeoffs spelled out.
I write Swift and web code for a living, mostly for small teams and solo founders. The advice below is the stuff that has held up across real projects, not conference demos.
Async/await vs completion handlers
If you’re still writing completion handlers in new code, stop. Not because async/await is trendy, but because the compiler now enforces things you used to enforce with discipline. Retain cycles, forgotten main-thread hops, error paths that get swallowed. The language catches them.
// Old way: easy to forget the main thread, easy to leak self
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
api.fetch { result in
DispatchQueue.main.async {
completion(result)
}
}
}
// 2026: the hop back to the main actor is the caller's problem
func loadProfile() async throws -> Profile {
try await api.fetch()
}
// Call site
Task { @MainActor in
do {
let profile = try await loadProfile()
self.profile = profile
} catch {
self.errorMessage = error.localizedDescription
}
}
The catch: async/await doesn’t erase concurrency bugs, it moves them. You still need to think about cancellation and about what happens when a view disappears mid-request. Which is the next section.
Structured concurrency vs detached tasks
This is where people get burned. A Task { } created inside a view or view controller is unstructured. It keeps running after the view is gone unless you cancel it. In SwiftUI, that matters more than it used to because views are cheap and get recreated constantly.
// Unstructured: outlives the view, can write to a dead object
.onAppear {
Task {
self.items = try? await api.fetchItems()
}
}
// Structured: tied to the view's lifetime, cancelled automatically
.task {
items = (try? await api.fetchItems()) ?? []
}
// When you need parallel work with a shared failure mode
func loadDashboard() async throws -> Dashboard {
async let user = api.fetchUser()
async let feed = api.fetchFeed()
async let alerts = api.fetchAlerts()
return try await Dashboard(user: user, feed: feed, alerts: alerts)
}
async let is the underrated one. Three independent requests that all need to succeed before you render? That’s a one-liner instead of a task group you have to hand-roll. Use a TaskGroup only when the number of tasks is dynamic or you need to stream results as they arrive.
Macros vs code generation vs plain generics
Swift macros have been stable for a while now and they’re genuinely useful, but they’re not a default. My rule: reach for a macro when the boilerplate is mechanical and the compiler can’t express it any other way. Reach for generics when the behavior is the same and only the type differs.
// Generics handle "same logic, different type" cleanly
func firstMatch<T: Equatable>(in items: [T], matching value: T) -> T? {
items.first { $0 == value }
}
// A macro earns its place when you'd otherwise copy-paste
// the same conformance block 20 times
@Observable
final class CartViewModel {
var items: [CartItem] = []
var total: Decimal { items.reduce(0) { $0 + $1.price } }
}
Notice that example doesn’t use a macro at all. @Observable is the observation system, and it replaced a lot of the manual ObservableObject and @Published plumbing. If you haven’t migrated, that’s a bigger win than any macro you’d write yourself.
Where macros go wrong
- They hide control flow. If a reader can’t tell what a line expands to, you’ve made the code worse.
- They complicate debugging. Stepping through expanded code is not fun.
- They add build time. Each macro is a compiler plugin doing real work.
Write one when you’ve copy-pasted the same block three times and the block is boring. Not before.
Value types vs reference types
This one rarely changes, but people still get it wrong in 2026. Default to structs for data. Use classes when you need identity, shared mutable state, or you’re bridging to an Objective-C API that expects it. The performance argument for structs is mostly noise at app scale. The correctness argument is not: value semantics mean a struct passed into a function can’t be mutated out from under you by someone else’s thread.
struct Order: Identifiable, Sendable {
let id: UUID
var items: [CartItem]
var status: Status
}
// Safe to pass across actors because it's Sendable and value-typed
@MainActor
func render(_ order: Order) { /* ... */ }
That Sendable conformance is doing real work. Under strict concurrency checking, the compiler will refuse to pass a non-Sendable class across an actor boundary. Structs with Sendable members get it for free. That’s a strong nudge toward value types whether you wanted one or not.
SwiftUI vs UIKit in 2026
For new apps, SwiftUI. For existing apps with a mature UIKit codebase, don’t rewrite. Interop is good enough that you can build new screens in SwiftUI and host them in UIKit, or the reverse, without a big migration project. The technique that matters here is UIHostingController and UIViewRepresentable, not picking a side.
The honest caveat: SwiftUI still has rough edges in very large lists and in anything that needs pixel-level control over layout. If your app is a spreadsheet or a drawing tool, you’ll fight it. If it’s forms, feeds, and settings screens, it’s faster to build and easier to keep correct.
How I actually choose
I ask three questions:
- Does the compiler enforce the invariant I care about? If yes, take the technique that gives it to me.
- Will a new hire read this in six months and know what it does? If no, the cleverness isn’t worth it.
- Does it survive the next OS release without a rewrite? Anything that leans on private behavior doesn’t.
That last one matters more than people admit. I’ve watched codebases get stuck on an old Swift version because someone reached for a technique that was never meant to be stable. When I’m building something that has to last, whether it’s an iOS app or a piece of custom web development for a local business, I optimize for the version of the code that exists two years from now, not the one that looks impressive today.
FAQ
Is async/await always faster than completion handlers?
No, and speed isn’t the point. The performance difference is negligible at app scale. The win is correctness: the compiler catches forgotten main-thread hops and unhandled errors that completion handlers let slide.
Should I rewrite my UIKit app in SwiftUI?
Usually no. Host new SwiftUI screens inside your existing UIKit app with UIHostingController and migrate screen by screen. A full rewrite is a multi-month project with a high chance of shipping regressions you didn’t have before.
When is a Swift macro worth writing?
When you’ve copy-pasted the same mechanical block at least three times and there’s no generic or protocol-based way to express it. If a reader can’t tell what the macro expands to, it’s not worth it.


