Engineering · iOS
SwiftUI for Angular Developers: A Practical Guide
Mihajlo Petrović6 min read
Coming to SwiftUI from Angular, the concepts map more cleanly than you expect - and the places they do not map are exactly where you lose a week. Here is what transfers, what traps you, and how to pick the right state wrapper the first time.
I've spent most of my career in Angular. Components, services, dependency injection, RxJS — that's my mental model of how a UI application is supposed to be structured. So when I picked up Swift and SwiftUI to build my first iOS apps, I expected a steep, alien learning curve.
It wasn't alien at all. It was familiar in the wrong places, which is far more dangerous.
This is the guide I wish I'd read before writing my first View.
The Good News: You Already Know 70% of It
SwiftUI is declarative. You describe what the UI should look like for a given state, and the framework figures out the rest. If you've written Angular templates with *ngIf and *ngFor, you already think this way.
Here's the same idea in both worlds:
<!-- Angular -->
<div *ngIf="isLoading; else content">Loading…</div>
<ng-template #content>
<p *ngFor="let item of items">{{ item.name }}</p>
</ng-template>
// SwiftUI
if isLoading {
Text("Loading…")
} else {
ForEach(items) { item in
Text(item.name)
}
}
Same mental model, less ceremony. SwiftUI's version is just Swift code — no template language, no separate compiler, no NgModule to register anything in.
The concepts map cleanly:
| Angular | SwiftUI |
|---|---|
| Component | View struct |
@Input() |
plain let property |
@Output() / EventEmitter |
closure property |
| Service + DI | @EnvironmentObject / @Environment |
BehaviorSubject |
@Published on an ObservableObject |
async pipe |
.task { } modifier |
| Router | NavigationStack |
If you can explain to a junior why a component should be dumb and a service should hold state, you already understand the architecture SwiftUI wants from you.
The Trap: A View Is Not a Component
Here's where Angular experience actively works against you.
In Angular, a component is an object with a lifetime. It's instantiated once, ngOnInit fires once, and you reason about it as a long-lived thing that reacts to changes.
A SwiftUI View is a struct that gets thrown away and recreated constantly. It is a description of the UI, not the UI itself. Your body may be evaluated dozens of times per second.
The consequences hit fast:
- Anything expensive in
bodyruns over and over. No API calls, no sorting a 5,000-item array, no date formatting in a loop. - You cannot store mutable state in a plain property. That's what
@State,@StateObject, and@Observableexist for. - There is no
ngOnInit. The nearest equivalent is.onAppearor.task, and both can fire more than once as views come in and out of the hierarchy.
The first version of my budgeting app made an API call in a computed property. It fired every time the user typed a character in a text field. Classic Angular brain, applied to a framework with completely different rules.
Rule of thumb: if it can't happen 60 times a second without consequence, it doesn't belong in
body.
State: Pick the Right Wrapper the First Time
This is where most beginners lose a weekend, so here is the short version.
@State — private, local, value-type state owned by this view. A toggle, a text field's contents, whether a sheet is open.
struct FilterBar: View {
@State private var query = ""
var body: some View {
TextField("Search", text: $query)
}
}
@Binding — a two-way reference to state owned by a parent. This is Angular's banana-in-a-box, [(ngModel)], made explicit and type-safe.
@StateObject / @ObservedObject — a reference-type view model. @StateObject means "I own this, create it once." @ObservedObject means "someone passed this to me." Mixing these two up is the single most common source of "why does my state keep resetting?"
@EnvironmentObject — dependency injection, SwiftUI style. You inject an object at the root of the tree and any descendant can pull it out. It's Angular's root-provided service, minus the compile-time safety: forget to inject it and you get a runtime crash instead of a build error.
My rule: view-local UI state uses @State, anything with business logic goes in an ObservableObject view model, and cross-screen state (session, settings, the user's data store) is injected via the environment. Almost the same layering I'd use in Angular.
Layout Is Not CSS, and Fighting That Costs You a Week
Coming from Tailwind and flexbox, I expected to control layout by constraining children. SwiftUI works in the opposite direction: the parent proposes a size, the child chooses its own, and the parent then positions it.
Practically, that means:
Spacer()and.frame(maxWidth: .infinity)do most of whatjustify-contentandflex: 1do for you.- Modifier order matters enormously.
.padding().background(.red)and.background(.red).padding()produce visibly different results. - There is no z-index soup.
ZStackand.overlaycover the real cases.
Once you stop trying to translate CSS one property at a time, layout becomes the easiest part of the framework.
What Actually Made Me Faster
Four things, in order of impact:
- Xcode Previews. Hot reload that renders multiple device sizes and both color schemes at once. It's better than anything I have in web development, and it changes how you build screens.
- Small views, aggressively. SwiftUI's type checker gets slow and its error messages get useless when a
bodygrows past ~50 lines. Extracting subviews fixes both. - Learning
Codableearly. Swift's JSON decoding is strict in a way TypeScript interfaces are not — a TypeScript interface is a lie the compiler believes,Codableactually validates at runtime. I now catch shape mismatches on the device instead of in production. - Accepting Apple's design language. Every hour I spent recreating a custom web-style component was an hour I could have spent shipping. Native components look right, behave right, and are accessible for free.
Should You Bother?
If you're a web developer wondering whether iOS is worth the detour: yes, and for a reason that has nothing to do with iOS.
Mobile constraints are brutal. There is no room for a 2 MB bundle, a layout that only works on one screen size, or a loading spinner that shows for three seconds. Building under those constraints made me a noticeably more careful Angular developer too — I think harder about what's actually on screen, what it costs, and what the user is waiting on.
I still reach for Angular for serious web applications. But SwiftUI has become the framework I enjoy opening on a weekend, and that counts for something.
Building something in SwiftUI and want to compare notes? Email me or find me on LinkedIn.
- #swiftui
- #ios
- #angular
- #swift
- #mobile development
Written by
Mihajlo Petrović
Software engineer in Belgrade. Builds his own products and the AI automations that keep them running.