Modern iOS development has changed significantly.
An iOS application is no longer simply a collection of UIViewController classes, storyboards, delegates, and callback-based network requests.
Production applications increasingly need to deal with:
- asynchronous APIs
- concurrency and thread safety
- complex navigation
- offline behavior
- persistence
- testing
- modularization
- design systems
- analytics and observability
- App Store releases
- legacy UIKit code
- scalable team ownership
For a production application, the objective should not simply be:
Make the screen work.
The objective should be:
Build software that remains
maintainable,
testable,
observable,
scalable,
and understandable
as the product grows.
A simplified modern iOS architecture might look like this:
User
│
▼
SwiftUI View
│
│ User Actions
▼
State / ViewModel
│
│ async/await
▼
Use Case / Service
│
▼
Repository
├───────────────┐
▼ ▼
Remote API Local Data
URLSession SwiftData / Core Data
└───────┬───────┘
▼
Domain
This is not the only valid architecture.
The technologies themselves are also not the architecture.
The important question is:
Which responsibilities belong where?
Let's go through the major parts.
1. Swift as the Foundation
For native iOS development, Swift should be the default language for new code.
Modern Swift provides language features that make application state and domain models significantly safer:
- value types
- optionals
- enums with associated values
- protocols
- generics
- async/await
- actors
Sendable- pattern matching
For example, a screen state can be represented explicitly:
enum DeviceState {
case loading
case loaded([Device])
case error(String)
}
Instead of maintaining unrelated variables such as:
var isLoading = false
var hasError = false
var devices: [Device] = []
which can accidentally create contradictory states:
isLoading = true
hasError = true
devices = non-empty
an enum makes valid states explicit.
switch state {
case .loading:
ProgressView()
case .loaded(let devices):
DeviceList(devices: devices)
case .error(let message):
ErrorView(message: message)
}
This idea becomes increasingly important as applications grow.
State should be modeled deliberately rather than emerging from unrelated Boolean properties.
2. SwiftUI Should Usually Be the Default for New UI
SwiftUI has become the natural starting point for new Apple-platform UI development.
Instead of imperatively changing interface elements:
Find label
↓
Change label
↓
Hide spinner
↓
Reload table
↓
Enable button
SwiftUI encourages a declarative model:
State
↓
UI
For example:
struct DeviceScreen: View {
let state: DeviceState
var body: some View {
switch state {
case .loading:
ProgressView()
case .loaded(let devices):
DeviceList(devices: devices)
case .error(let message):
Text(message)
}
}
}
The interface describes what should exist for the current state.
When that state changes, SwiftUI updates the relevant parts of the view hierarchy.
This is conceptually similar to modern declarative UI frameworks elsewhere:
State changes
↓
UI representation changes
It reduces the amount of manual synchronization between the application state and the interface.
Current iOS roles heavily reflect this direction. SwiftUI appears repeatedly alongside Swift, while production teams still expect engineers to understand UIKit and mixed codebases. LinkedIn
3. UIKit Is Not Dead
A common mistake is assuming that a modern iOS project means:
SwiftUI everywhere
UIKit nowhere
Real production applications are rarely that simple.
Many mature applications contain:
SwiftUI
+
UIKit
+
legacy components
+
third-party SDKs
For example:
SwiftUI Feature
│
▼
UIViewControllerRepresentable
│
▼
Existing UIKit Component
or the opposite:
UIKit Navigation
│
▼
UIHostingController
│
▼
SwiftUI Screen
A senior iOS engineer should therefore understand both frameworks.
UIKit knowledge remains especially useful for:
- existing production applications
- custom collection layouts
- advanced text handling
- mature navigation stacks
- older SDK integrations
- incremental SwiftUI migration
- debugging legacy screens
A healthy migration strategy is often:
Existing UIKit app
│
├── maintain stable UIKit features
│
├── introduce SwiftUI for new screens
│
└── gradually migrate where valuable
not:
Rewrite the entire application.
Multiple current senior iOS openings explicitly require both SwiftUI and UIKit, particularly where companies are modernizing existing applications. Lever
4. Keep Business Logic Out of SwiftUI Views
SwiftUI views should primarily:
Render state
+
Send user actions
They should not become containers for:
HTTP requests
database access
authentication
business rules
analytics decisions
large state machines
Avoid:
struct CheckoutView: View {
var body: some View {
Button("Pay") {
Task {
let response = try await URLSession.shared
.data(from: paymentURL)
// parse response
// update database
// calculate discount
// send analytics
// update UI
}
}
}
}
A healthier direction is:
SwiftUI View
│
│ Action
▼
ViewModel / State Holder
│
▼
Use Case / Service
│
▼
Repository / API
For example:
struct CheckoutView: View {
let viewModel: CheckoutViewModel
var body: some View {
Button("Pay") {
Task {
await viewModel.pay()
}
}
}
}
The view remains focused on presentation.
That makes it easier to:
- preview
- test
- reuse
- reason about
- modify independently
5. State Management Matters More Than the Name of the Architecture
Developers often ask:
Should I use MVVM, Clean Architecture, TCA, MVC, VIPER, or something else?
That is useful, but it is not the first question.
The more important questions are:
Where does state live?
Who can mutate it?
How does UI observe it?
Where are side effects executed?
How does data move through the application?
A common architecture might look like:
View
│
│ Action
▼
ViewModel
│
│ calls
▼
Service / Use Case
│
▼
Repository
For example:
@Observable
@MainActor
final class DeviceViewModel {
private(set) var state: DeviceState = .loading
private let repository: DeviceRepository
init(repository: DeviceRepository) {
self.repository = repository
}
func load() async {
state = .loading
do {
let devices = try await repository.devices()
state = .loaded(devices)
} catch {
state = .error(error.localizedDescription)
}
}
}
The view can then remain simple:
struct DeviceScreen: View {
@State var viewModel: DeviceViewModel
var body: some View {
DeviceContent(state: viewModel.state)
.task {
await viewModel.load()
}
}
}
Apple's Observation framework provides native support for observable state, while modern job descriptions frequently mention MVVM, Clean Architecture, TCA, MVP, or similar approaches rather than one universal standard. Apple Developer
The lesson is:
Architecture should make state and side effects predictable.
6. Observation and Modern SwiftUI State
Modern SwiftUI provides several ways of managing state.
Typical tools include:
@State
@Binding
@Environment
@Observable
They solve different problems.
Local view state
@State private var isPresented = false
Good for state owned directly by a view.
Parent-owned state
@Binding var isEnabled: Bool
The child can mutate state owned elsewhere.
Shared dependencies
@Environment(\.apiClient)
private var apiClient
Useful for dependencies or shared environment values.
Observable models
@Observable
final class ProfileModel {
var name = ""
var isLoading = false
}
A common mistake is promoting every property to global observable state.
A useful rule is:
Keep state as local as possible
but as shared as necessary.
This reduces accidental coupling.
7. Swift Concurrency Should Be the Default Mental Model
Modern iOS applications perform many asynchronous operations:
Networking
Database access
File operations
Image loading
Authentication
Media processing
Background work
Bluetooth
Location
Cloud synchronization
Blocking the main thread is not acceptable.
Swift Concurrency provides:
async
await
Task
actor
TaskGroup
AsyncSequence
A network call might look like:
func devices() async throws -> [Device] {
let (data, response) =
try await URLSession.shared.data(from: url)
return try JSONDecoder()
.decode([Device].self, from: data)
}
The caller becomes straightforward:
let devices = try await repository.devices()
instead of nested completion handlers.
Apple's concurrency model also includes actor isolation and Sendable checking to prevent unsafe data sharing between concurrency domains. Apple Developer
8. Understand @MainActor
UI state normally belongs on the main actor.
For example:
@MainActor
final class DeviceViewModel {
var devices: [Device] = []
func load() async throws {
devices = try await repository.devices()
}
}
This communicates an important guarantee:
Mutable UI state
↓
Main Actor
However, @MainActor should not be used blindly everywhere.
Code that performs:
CPU-intensive work
image processing
audio processing
large parsing tasks
may need different isolation.
The goal is not:
Put @MainActor on everything.
The goal is:
Make concurrency ownership explicit.
9. Actors for Shared Mutable State
Consider a token store accessed concurrently:
final class TokenStore {
var token: String?
}
Multiple tasks might modify it simultaneously.
An actor can provide isolation:
actor TokenStore {
private var token: String?
func update(_ token: String) {
self.token = token
}
func currentToken() -> String? {
token
}
}
Conceptually:
Task A ──┐
Task B ──┼──► Actor ──► Protected state
Task C ──┘
Actors are particularly useful for:
- authentication state
- caches
- synchronization engines
- websocket state
- shared mutable resources
Swift 6's stricter concurrency checking makes these boundaries increasingly important in production code. Apple Developer
10. Networking: Prefer a Dedicated Client Layer
A view should not know how HTTP works.
A ViewModel ideally should not know much either.
A common structure is:
View
↓
ViewModel
↓
Repository
↓
API Client
↓
URLSession
Define an abstraction:
protocol APIClient {
func send<T: Decodable>(
_ request: URLRequest
) async throws -> T
}
Then implement it:
final class URLSessionAPIClient: APIClient {
func send<T: Decodable>(
_ request: URLRequest
) async throws -> T {
let (data, response) =
try await URLSession.shared.data(for: request)
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(T.self, from: data)
}
}
Now the rest of the application depends on an abstraction rather than directly on URLSession.
That helps with:
- testing
- authentication
- retries
- logging
- mocking
- API migrations
11. REST and GraphQL Are Both Common
Production iOS applications frequently consume:
REST
GraphQL
WebSockets
Streaming APIs
REST remains extremely common.
GraphQL is also visible in modern product companies; Freetrade, for example, is migrating toward a unified GraphQL API, and idealo lists GraphQL as relevant iOS experience. Ashby
The architecture should avoid coupling screens directly to either transport.
Prefer:
UI
↓
Domain
↓
Repository
↓
REST / GraphQL implementation
rather than:
SwiftUI Screen
↓
GraphQL query
everywhere in the UI layer.
12. The Repository Pattern
The UI should not care whether data originates from:
REST API
GraphQL
Core Data
SwiftData
File
Cache
CloudKit
Keychain
Define the behavior:
protocol DeviceRepository {
func devices() async throws -> [Device]
func device(
id: Device.ID
) async throws -> Device
}
The implementation decides where the data actually comes from:
DeviceRepository
│
┌───┴────┐
▼ ▼
Remote Local
API Storage
For an offline-capable application:
Server
│
▼
Repository
│
├──► Local database
│
└──► Application
The repository becomes the synchronization boundary.
13. Persistence: SwiftData, Core Data, Keychain, and Files Solve Different Problems
Not all data belongs in the same storage system.
SwiftData
Useful for modern model persistence when its deployment and feature constraints fit the application.
Apple's SwiftData APIs provide model storage through ModelContainer, ModelContext, and query integration with SwiftUI. Apple Developer
Example:
@Model
final class Favorite {
var id: UUID
var title: String
init(
id: UUID,
title: String
) {
self.id = id
self.title = title
}
}
Core Data
Still extremely relevant for:
- existing production apps
- mature migration requirements
- complex persistence
- long-lived codebases
UserDefaults
Good for small preferences:
Theme
Sort order
Onboarding flag
Simple settings
Not for large application databases.
Keychain
Use for sensitive credentials such as:
Authentication tokens
Secrets
Secure identifiers
Files
Useful for:
Audio
Video
Images
Downloads
Large binary resources
Choose storage based on the nature of the data.
14. Offline-First Should Be an Architectural Decision
For many mobile applications, connectivity is unreliable.
A weak design assumes:
Internet exists
↓
Request succeeds
↓
Screen works
A stronger design considers:
Online
Offline
Slow connection
Timeout
Partial response
Expired session
Stale cache
Conflict
Retry
An offline-first architecture might look like:
Server
▲
│ Sync
▼
UI ─► Repository ─► Local Database
The UI primarily observes local state.
The repository synchronizes with remote state.
Current iOS roles explicitly call out offline-first architecture and Core Data/SwiftData experience, especially for applications expected to operate under unreliable connectivity. Ashby
15. Navigation Is Application State Too
Navigation often begins simply:
NavigationStack {
HomeView()
}
But large applications quickly add:
Deep links
Authentication
Tabs
Sheets
Push notifications
External URLs
Nested flows
State restoration
Navigation can therefore become architectural state.
SwiftUI provides NavigationStack and NavigationPath for programmatic navigation. Apple Developer
For example:
enum Route: Hashable {
case profile
case device(UUID)
case settings
}
Then:
@State
private var path: [Route] = []
The navigation hierarchy can become:
App
│
▼
Navigation State
│
├── Home
├── Device
├── Profile
└── Settings
For larger applications, some teams introduce:
Coordinator
Router
Navigation Store
The important point is not the name.
It is:
Navigation should remain understandable as the application grows.
16. Dependency Injection Does Not Require a Huge Framework
Consider:
final class DeviceViewModel {
let repository =
DeviceRepositoryImpl()
}
The ViewModel now creates its own dependency.
That makes testing and replacement harder.
Instead:
final class DeviceViewModel {
private let repository: DeviceRepository
init(
repository: DeviceRepository
) {
self.repository = repository
}
}
Now production can provide:
DeviceRepositoryImpl()
and tests can provide:
MockDeviceRepository()
Conceptually:
Protocol
▲
│
┌─┴──────────────┐
│ │
Real Mock
Repository Repository
Dependency injection gives us:
Loose coupling
+
Testability
+
Replaceable implementations
+
Clear dependencies
You do not necessarily need a large dependency-injection framework.
Constructor injection is often enough.
17. Clean Architecture — Without Turning Every Function Into a Layer
A larger iOS application can benefit from separating:
Presentation
Domain
Data
Infrastructure
For example:
Presentation
├── SwiftUI
├── ViewModel
└── Navigation
Domain
├── Models
├── Use Cases
└── Repository Protocols
Data
├── Repository Implementations
├── DTOs
└── Persistence
Infrastructure
├── URLSession
├── Keychain
├── Analytics
└── Logging
A feature flow might become:
DeviceView
↓
DeviceViewModel
↓
ObserveDevices
↓
DeviceRepository
↓
DeviceRepositoryImpl
/ \
Remote API SwiftData
However, do not create:
View
↓
ViewModel
↓
UseCase
↓
Interactor
↓
Manager
↓
Repository
↓
DataSource
↓
Service
↓
APIClient
for a trivial operation.
Architecture exists to control complexity.
It should not manufacture complexity.
18. MVVM Is Common — But It Is Not a Religion
MVVM remains one of the most common architectural terms in iOS job descriptions. LinkedIn
A simple version is:
Model
▲
│
ViewModel
▲
│
View
But a common anti-pattern is creating enormous ViewModels:
ProfileViewModel
├── networking
├── navigation
├── analytics
├── persistence
├── validation
├── business rules
├── payments
└── 2,000 lines
A ViewModel should coordinate presentation state.
Complex business capabilities can move into dedicated services or use cases.
View
↓
ViewModel
├── AuthenticationService
├── PaymentService
└── ProfileRepository
19. TCA Can Be Valuable, But Should Solve a Real Problem
The Composable Architecture can be useful for teams that want highly explicit:
State
Action
Reducer
Effect
Dependency
Conceptually:
Action
↓
Reducer
↓
State
↓
View
It can provide strong predictability for large applications.
But introducing it to a small application simply because it is sophisticated can add unnecessary complexity.
Use TCA because the team benefits from its model.
Not because:
"Senior iOS projects use TCA."
Current hiring shows that companies recognize TCA alongside MVVM and Clean Architecture, but not as a universal requirement. Ashby
20. Modularization with Swift Package Manager
As applications grow, one target containing everything can become difficult to maintain.
A possible structure is:
App/
│
├── Core/
│ ├── Networking/
│ ├── Persistence/
│ ├── Analytics/
│ ├── DesignSystem/
│ └── Models/
│
├── Features/
│ ├── Home/
│ ├── Profile/
│ ├── Payments/
│ └── Settings/
│
└── App/
├── Navigation/
└── Composition/
Swift Package Manager can provide module boundaries:
App
│
├── FeatureHome
├── FeatureProfile
├── DesignSystem
├── Networking
└── Persistence
Apple explicitly supports organizing reusable application code through local Swift packages, and modular architecture with SPM appears directly in modern iOS job requirements. Apple Developer
Benefits include:
- clearer dependencies
- faster team ownership
- reusable components
- isolated tests
- improved build organization
- reduced accidental coupling
However:
60 packages
does not automatically mean:
good architecture
Modularization should solve actual organizational or technical problems.
Qonto is a useful real-world example: its current iOS codebase describes more than 35 independent modules rather than one monolithic application target. Lever
21. A Design System Should Be a Real Engineering Component
Large products should avoid reinventing:
Button
Typography
Spacing
Colors
TextField
Card
Loading indicator
inside every feature.
Instead:
DesignSystem
├── Colors
├── Typography
├── Spacing
├── Buttons
├── Inputs
├── Cards
├── Icons
└── Components
A feature then depends on the design system:
PrimaryButton(
title: "Continue",
action: continueFlow
)
instead of recreating styling.
This improves:
Consistency
Accessibility
Migration
Brand changes
Developer velocity
Design-system work is increasingly cross-platform; current roles at Whatnot and ZEAL explicitly describe synchronized SwiftUI, Compose, and web component systems. Ashby
22. Error Handling Should Be Designed, Not Added Later
Production systems do not live entirely on the happy path.
Consider:
No network
Timeout
401
403
500
Malformed JSON
Expired token
Disk full
Cancelled Task
Background transition
Server unavailable
Instead of displaying:
"Something went wrong"
for every condition, errors should be classified.
For example:
enum APIError: Error {
case unauthorized
case unavailable
case invalidResponse
case decoding
case network(Error)
}
Then presentation can decide:
unauthorized
→ login
network unavailable
→ retry
server unavailable
→ temporary error
decoding failure
→ report diagnostic
Error handling is architecture.
23. Cancellation Is Part of Async Architecture
Mobile users change their minds constantly.
They:
open a screen
leave immediately
start search
type again
change filters
background the app
Long-running tasks should often be cancellable.
Example:
.task(id: query) {
await search(query)
}
When the identity changes, SwiftUI can cancel the previous task.
Code should respect cancellation:
try Task.checkCancellation()
Ignoring cancellation can produce:
- wasted network requests
- outdated UI
- race conditions
- unnecessary CPU work
24. Testing Should Exist at Multiple Levels
A production application should not depend entirely on manual testing.
A practical testing pyramid might look like:
UI / E2E
▲
/ \
/ \
Integration
▲
/ \
/ \
Unit Tests
Unit tests
Useful for:
Business rules
Reducers
ViewModels
Formatting
Validation
Use cases
Integration tests
Useful for:
Repository + API
Repository + database
Authentication
Persistence
Serialization
UI tests
Useful for:
Critical user journeys
Login
Checkout
Navigation
Onboarding
Snapshot tests
Useful for:
Design system components
Visual regressions
Multiple states
Different device sizes
Snapshot testing is not theoretical: Qonto currently describes maintaining more than 450 snapshot tests in its iOS engineering workflow. Lever
25. Swift Testing and XCTest
Modern Swift projects can use both:
Swift Testing
XCTest
Swift Testing provides APIs such as:
@Test
func totalCalculation() {
let result = calculateTotal(
items: [10, 20]
)
#expect(result == 30)
}
It integrates with Swift concurrency and supports parameterized and parallel tests. Apple Developer
XCTest remains important, particularly for:
- existing test suites
- UI automation
- mature projects
Async XCTest code can directly use async/await. Apple Developer
A modern project does not need to rewrite every XCTest immediately.
Migration can be incremental.
26. Design for Testability
Consider:
final class ProfileViewModel {
private let api =
RealProfileAPI()
private let storage =
RealStorage()
}
Testing this class requires real infrastructure.
Instead:
final class ProfileViewModel {
private let api: ProfileAPI
private let storage: ProfileStorage
init(
api: ProfileAPI,
storage: ProfileStorage
) {
self.api = api
self.storage = storage
}
}
Now tests can use:
Fake API
Fake Storage
Controlled responses
Controlled failures
Testability is not something added after the architecture is complete.
Good architecture naturally makes testing easier.
27. Performance: Measure Before Optimizing
Do not optimize because:
"This looks expensive."
Use evidence.
Measure
↓
Locate bottleneck
↓
Form hypothesis
↓
Change
↓
Measure again
iOS engineers should be comfortable investigating:
- main-thread hangs
- memory growth
- leaks
- excessive SwiftUI updates
- rendering hitches
- CPU usage
- launch time
- network behavior
- disk I/O
- battery usage
Apple's Instruments tooling includes SwiftUI-specific analysis for long view updates and excessive update frequency. Apple Developer
The workflow should be:
"I measured this."
not:
"I think SwiftUI is slow."
28. Memory Management Still Matters
Swift uses Automatic Reference Counting.
That does not mean memory problems disappear.
For example:
class Player {
var onFinish: (() -> Void)?
}
If a closure captures the owner strongly:
player.onFinish = {
self.showNextTrack()
}
you can create a retain cycle.
A common solution is:
player.onFinish = { [weak self] in
self?.showNextTrack()
}
Senior iOS work still requires understanding:
strong references
weak references
unowned references
retain cycles
object lifetimes
especially in:
- delegates
- callbacks
- media
- Combine
- Objective-C interoperability
- long-lived services
29. Production Observability Matters
Testing cannot reproduce every production condition.
Real users have:
different devices
different OS versions
poor networks
low storage
memory pressure
background transitions
unexpected workflows
Production applications should capture:
Crash reports
Performance metrics
Analytics
Logs
Feature metrics
Apple's MetricKit can report real-world data including crashes, hangs, launch performance, CPU, memory, disk and network-related metrics. Apple Developer
Third-party tools may include:
Firebase Crashlytics
Sentry
Datadog
New Relic
depending on the organization.
The principle is:
Ship
↓
Observe
↓
Measure
↓
Improve
30. CI/CD Is Part of the Architecture
A modern iOS repository should not depend on:
"Works on my Mac."
A pull request might trigger:
Pull Request
│
▼
Resolve Packages
│
▼
Build
│
▼
Lint
│
▼
Unit Tests
│
▼
Integration Tests
│
▼
UI / Snapshot Tests
│
▼
Archive
Release automation might continue:
Merge
↓
Build
↓
Sign
↓
Upload
↓
TestFlight
↓
QA
↓
App Store
Common tooling includes:
GitHub Actions
Bitrise
Fastlane
Xcode Cloud
Tuist
Qonto's current iOS workflow, for example, lists GitHub Actions, Bitrise, Firebase Test Lab, Sonar, Fastlane, and Tuist. Lever
The goal is not to collect CI tools.
It is to make:
incorrect changes difficult to ship
and:
correct changes easy to release
31. App Store Delivery Is Part of iOS Engineering
Production ownership does not stop when code is merged.
iOS engineers should understand:
Versioning
Signing
Provisioning
TestFlight
App Store Connect
App Review
Entitlements
Privacy manifests
Release rollout
Crash monitoring
Rollback strategy
For consumer products, this knowledge is often part of the job itself. Current iOS roles repeatedly mention TestFlight, App Store releases, monitoring, and owning features through production rather than stopping at implementation. Ashby
32. Objective-C Is Legacy — But Still Valuable
New application code is generally written in Swift.
However, mature iOS applications may contain:
Objective-C
Objective-C++
C
C++
An experienced iOS developer should at least be able to:
- read Objective-C
- debug mixed projects
- understand bridging headers
- work with Objective-C APIs
- integrate existing native libraries
You do not need to start new features in Objective-C simply to prove that you know it.
But being unable to work inside an existing Objective-C codebase can still be limiting for senior roles.
33. C++ and Objective-C++ When They Actually Make Sense
Most iOS applications do not need C++.
There are legitimate cases:
Audio / DSP
Computer vision
Game engines
Existing C++ SDKs
Performance-sensitive algorithms
Cross-platform native libraries
The bridge often looks like:
Swift
│
▼
Objective-C++
│
▼
C++
Keep this boundary small.
Do not expose complex C++ ownership semantics throughout the Swift codebase.
34. AI Is Becoming Part of the iOS Engineering Workflow
A notable change in current software-engineering job descriptions is explicit expectation around AI-assisted development.
Companies increasingly mention:
Claude Code
Codex
Copilot
AI code review
Agentic workflows
The healthy model is not:
AI writes code
→ merge
It is:
Engineer defines problem
↓
AI assists implementation
↓
Engineer reviews
↓
Tests validate
↓
Production metrics verify
Modern iOS engineers should know how to use AI as an accelerator without outsourcing engineering judgment.
Current product teams including Qonto and several recent mobile openings explicitly discuss AI coding tools as part of engineering work. Lever
35. A Practical Modern iOS Stack
There is no universal stack.
But a production iOS project in 2026 could reasonably look like:
Language
└── Swift
UI
├── SwiftUI
└── UIKit where required
State
├── Observation
├── @Observable
├── @State
└── @Environment
Concurrency
├── async / await
├── Task
├── Actors
└── Sendable
Architecture
├── MVVM where useful
├── Unidirectional Data Flow
├── Repository Pattern
├── Clean Architecture where justified
└── TCA for teams that benefit from it
Navigation
├── NavigationStack
├── NavigationPath
└── Coordinator / Router for complex flows
Networking
├── URLSession
├── Codable
├── REST
└── GraphQL where appropriate
Persistence
├── SwiftData
├── Core Data
├── Keychain
├── UserDefaults
└── File storage
Dependency Management
└── Swift Package Manager
Modularization
├── Local Swift Packages
├── Feature modules
└── Core modules
Testing
├── Swift Testing
├── XCTest
├── UI Tests
├── Integration Tests
└── Snapshot Tests
Performance
├── Instruments
└── MetricKit
Build / CI
├── Xcode
├── GitHub Actions / Bitrise / Xcode Cloud
├── Fastlane
└── Tuist where useful
Distribution
├── TestFlight
└── App Store Connect
Monitoring
├── Crash reporting
├── Analytics
└── Production performance metrics
Interop
├── Objective-C when required
└── Objective-C++ / C++ when required
Again:
The stack is not the architecture.
Libraries and frameworks change.
Responsibility boundaries are what make the system maintainable.
36. The Bigger Picture
If there is one diagram worth remembering, it is this:
USER
│
▼
┌─────────────────┐
│ SwiftUI │
│ View │
└────────┬────────┘
│
Actions
│
▼
┌─────────────────┐
│ State / │
│ ViewModel │
└────────┬────────┘
│
async/await
│
▼
┌─────────────────┐
│ Use Case / │
│ Domain Service │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Repository │
└────────┬────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Remote API │ │ Local Data │
│ URLSession │ │ SwiftData / │
│ REST/GraphQL │ │ Core Data │
└───────────────┘ └───────────────┘
Around that core:
Dependency Injection
Testing
Navigation
Design System
Logging
Analytics
Performance Monitoring
CI/CD
App Store Delivery
support the application.
A healthy project should make it possible to answer questions such as:
Where does UI state live?
Who owns this network call?
What happens when the request fails?
Can this component be tested without the network?
What happens if the Task is cancelled?
Which actor owns this mutable state?
Can this feature work offline?
Who owns navigation?
Can this module be changed independently?
How do we detect a production regression?
How does this code reach TestFlight?
If these questions are difficult to answer, adding another framework probably will not solve the underlying architectural problem.
The purpose of architecture is not to create the most impressive folder structure.
It is to make change safe.
A good iOS architecture allows a team to add features, replace infrastructure, test behavior, investigate production failures, and adopt new Apple frameworks without having to understand or rewrite the entire application every time.
That is ultimately what scalable mobile engineering is about.