
Mobile engineers at American Express Global Business Travel often need to load Egencia trip details, traveler profiles, and policy data without blocking the UI. This question evaluates whether you can explain Swift concurrency clearly and apply it to production app flows.
Explain how you handle asynchronous programming in Swift.
Address these points:
async/await, Task, and structured concurrency work in Swift?The interviewer expects a practical engineering explanation rather than language trivia. Discuss the core concurrency model, common APIs, trade-offs versus older callback-based approaches, and how you would structure code in an iOS app that fetches multiple pieces of Egencia data.
Swift's async/await lets you write asynchronous code in a sequential style. It improves readability over nested completion handlers and makes error propagation and cancellation easier to reason about.
func loadTrip() async throws -> Trip {
try await api.fetchTrip()
}
Structured concurrency means child tasks are scoped to a parent task, so lifetimes and cancellation are easier to manage. This reduces leaks, orphaned work, and inconsistent app state.
async let trip = api.fetchTrip()
async let profile = api.fetchTravelerProfile()
let result = try await (trip, profile)
A Task starts asynchronous work from synchronous code, such as a button tap or view lifecycle method. MainActor ensures UI state is updated on the main thread, which is required for UIKit and SwiftUI correctness.
Task {
let trip = try await api.fetchTrip()
await MainActor.run {
self.viewModel.trip = trip
}
}
Swift tasks support cooperative cancellation, so long-running work should check for cancellation and stop early when appropriate. Errors flow naturally with throws, which makes async code easier to compose than callback-based error handling.
try Task.checkCancellation()
let data = try await api.fetchPolicy()
Task groups are useful when the number of concurrent operations is dynamic, such as loading a variable number of trip segments. They let you spawn child tasks, gather results, and handle partial failures with more control.
let segments = try await withThrowingTaskGroup(of: Segment.self) { group in
for id in ids {
group.addTask { try await api.fetchSegment(id: id) }
}
var result = [Segment]()
for try await segment in group {
result.append(segment)
}
return result
}