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.