iOS / Swift
iOS is one vendor, one toolchain and one set of rules — and, in exchange, a platform where the hardware, the OS and the language are designed together. Swift is a modern, strongly typed language with no garbage collector, which makes memory something you reason about rather than something that happens to you. This page covers reference counting, SwiftUI against UIKit, concurrency, and the release process that surprises teams arriving from the web.
- Memory
- Counted, not collected
- UI
- SwiftUI, UIKit underneath
- Release
- Through review
Who owns this object
Swift has no garbage collector. Every class instance carries a count of how many strong references point at it, the compiler inserts the increments and decrements for you, and the moment the count reaches zero the object is deallocated — immediately and deterministically. That determinism is a real advantage: there are no collection pauses, memory is released the instant it stops being needed, and deinit runs at a predictable point.
The price is that reference counting cannot see a cycle. If two objects hold strong references to each other, each one keeps the other's count above zero forever, and neither is ever freed — even after everything else in the program has forgotten they exist. Nothing crashes; memory simply grows. The three places this reliably happens are a delegate pointing back at its owner, a parent and child that both hold each other, and a closure that captures self while the object holds the closure.
Two habits keep this from becoming folklore. Put a print or a breakpoint in deinit on the screens you care about while developing — if it never fires when you navigate away, you have a cycle, and you found it in ten seconds instead of in a memory graph three months later. And run the Leaks and Allocations instruments before a release; Xcode's memory graph debugger will draw you the cycle directly, arrows and all.
SwiftUI or UIKit
There are two UI frameworks on the platform and the honest position is that you need to be able to read both. UIKit is imperative: you create views, hold references, and mutate them in response to events. SwiftUI is declarative: you describe what the interface should look like for a given state and the framework works out the changes — the same mental model as React, arrived at independently.
SwiftUI
- Far less code for the same screen, with live previews while you type.
- One framework across iPhone, iPad, Mac, Watch and TV.
- Each release adds capability, so your minimum iOS version decides what you may use.
- Complex custom layout and fine control still fall back to UIKit through a wrapper.
UIKit
- Fifteen years of answers, libraries and behaviour nobody has to guess at.
- Complete control over layout, transitions and every edge case.
- What every existing app is written in, so you will read it regardless.
- More code, more state to keep synchronised by hand, more places to get it wrong.
For a new app in 2026, start in SwiftUI and drop into UIKit where you need to — the interoperation works in both directions and is a normal thing to do, not an admission of defeat. For an existing UIKit app, adding SwiftUI screen by screen is a well-trodden path and much safer than a rewrite. The thing that actually determines whether SwiftUI is available to you is your minimum supported iOS version, because the framework gained most of its serious capability after its first release.
Concurrency without the callbacks
Swift added structured concurrency and it replaced a decade of nested completion handlers with async / await that reads in order. The part that matters more than the syntax is the actor: a type that protects its own state by allowing only one task inside it at a time, checked by the compiler rather than by your discipline with locks. Data races stop being a class of bug you hunt and start being a compile error.
The one rule that never changes on this platform: UI work happens on the main thread. @MainActor says so in the type system — mark a view model with it and the compiler guarantees its methods run there. Updating a view from a background thread is undefined behaviour that usually looks like it works and occasionally corrupts the screen or crashes, which is the worst possible failure profile because it survives testing.
How this shows up in real delivery
The part that surprises teams arriving from the web is that shipping is not a deploy. A release goes through App Store review, which takes hours to days and can be rejected — most often for permission strings that do not explain themselves, for account deletion that the app must offer if it offers sign-up, for payments that route around Apple's system, or for privacy declarations that do not match what the app actually collects. None of that is arbitrary, but all of it has to be planned into the schedule rather than discovered on the release day.
Two habits shorten that loop. Ship every build to TestFlight, which is fast, gives you real devices and real crash reports, and is where you find the problems review would find. And keep the number of supported iOS versions deliberate: adoption of a new version on this platform is fast, so dropping the oldest one each year is usually uncontroversial and it is what unlocks the framework features that remove code.
Where it degrades
- Retain cycles through delegates and closures, which leak silently and never crash.
unownedused whereweakwas meant, which turns a leak into a crash on a real device.- View controllers that grow until networking, parsing and navigation all live in one file.
- UI updated from a background thread, which usually appears to work and corrupts state occasionally.
- A continuation that is not resumed on every path, leaving the caller suspended with no error.
- Permission prompts requested at launch instead of at the moment the feature needs them.
- Review requirements — deletion, privacy, payments — discovered on the day the release was due.
When to use it
Use it when
- Products where the iOS experience is the product — performance, polish and platform conventions.
- Anything built on a capability Apple ships first — widgets, live activities, on-device models, new hardware.
- Apps where deterministic memory and no collection pauses genuinely matter.
- Teams that can support one platform properly rather than two approximately.
Avoid it when
- Products that need both platforms with one team and one budget — that is what the cross-platform tools are for.
- Anything requiring same-day fixes with no review queue; over-the-air code updates are not permitted here.
- Business models that conflict with the store's payment rules — resolve that before writing code.
- A team with no Mac hardware and no capacity to learn the toolchain; there is no way around Xcode.
Found this useful?
Share it with someone who is working on the same problem.