Sign in to generate with AI
Technical interview deck — SwiftUI, Swift 6 concurrency, architecture, error handling, and code challenges.
Welcome everyone. Today we're diving deep into a Senior iOS Engineer interview focused on SwiftUI and Swift 6. This is designed to assess the advanced technical skills and architectural thinking required for senior-level iOS development. Over the next 60 minutes, we'll explore real-world scenarios covering everything from clean architecture and MVVM patterns to Swift 6's strict concurrency model. We'll look at code challenges, debugging exercises, and architectural decisions that senior engineers face daily. Let's get started.
Let's walk through how this interview is structured. We'll cover four major areas. First, Architecture and Design, where we'll discuss Clean Architecture principles, MVVM patterns, and modularization strategies. Second, Swift 6 Concurrency, focusing on actors, the Sendable protocol, and data isolation techniques. Third, Error Handling, where we'll explore domain modeling and retry logic. And finally, Code Reasoning, with debugging challenges and tradeoff analysis. The entire session runs 60 minutes with code challenges woven throughout. This isn't just theory — you'll be writing and reviewing actual code.
Here's our first architectural challenge. Imagine you're refactoring a monolithic SwiftUI app with over 200 Views — it's become unmaintainable and testing is nearly impossible. How would you approach modularization? We want to hear your thinking on several key aspects. First, how do you identify bounded contexts — what belongs together? Second, the tricky part: breaking circular dependencies between modules. Third, managing shared state across these new modules without creating coupling. And finally, your migration strategy — would you go incremental, module by module, or attempt a big bang rewrite? Walk us through your approach.
Looking at this diagram, we see the classic Clean Architecture layer structure for iOS apps. At the top, we have SwiftUI Views that depend on ViewModels. The ViewModels then depend on Use Cases, which represent our business logic. Use Cases interact with Repositories, which abstract our data sources. Finally, Repositories coordinate between API Clients for network calls and Local Cache for persistence. Notice the dependency flow: the Presentation layer depends on the Domain layer, and the Domain layer is independent — it doesn't know about data sources. The Data layer implements the interfaces defined in Domain. This separation makes the core business logic testable and framework-independent.
Now for a practical design challenge. We need you to design a type-safe networking layer that handles several requirements. First, it should use Codable for request and response modeling — we want compile-time safety. Second, automatic authentication token injection for every request. Third, implement automatic retry logic for transient failures. And critically, the entire implementation must be concurrency-safe under Swift 6's strict checking. Show us your API design — what protocols would you define? What are the key types? How do you structure the request and response models? Think about extensibility and testability as you design this.
Here's a concurrency challenge. Looking at this code, we have a UserStore class with a cache dictionary and an API client. The getUser function checks the cache first, and if there's no cached user, it fetches from the API and updates the cache. This code compiles in Swift 5, but under Swift 6's strict concurrency checking, it has serious data race issues. Take a moment to identify ALL the problems here. What are the data race conditions? Where can concurrent access cause crashes or data corruption? And most importantly, how would you fix this to be fully Swift 6 compliant?
Here's the correct solution. The first major change: we convert the class to an actor. This is critical because actors provide serial access to their mutable state — only one task can access the cache at a time, eliminating data races. Second, notice we're using throws instead of swallowing errors with try question mark. This gives proper error propagation. Third, and this is important, the User type itself must conform to Sendable for Swift 6 strict concurrency. When you return a User from an actor, Swift needs to ensure it can safely cross that actor boundary. The actor keyword is doing heavy lifting here — it's protecting that cache dictionary from concurrent modification while still allowing async access from multiple tasks.
Let's talk about error handling design. Here's the challenge: create a domain error model that clearly distinguishes between different failure types. You need to handle network failures like timeouts and no connection, API errors with different status codes like 401, 404, and 500, parsing errors when JSON doesn't match your models, and business logic errors specific to your domain. But here's the key requirement: your error type needs to answer two critical questions. First, can this error be retried automatically? And second, what should the UI show the user? Your design should make these decisions explicit and type-safe.
Here's a strong implementation. We have an AppError enum that conforms to both Error and Sendable. It has associated values for each error category — network errors, API errors with status codes, parsing errors, and business logic errors. Now look at the computed properties. The isRetriable property uses pattern matching to determine retry logic — network timeouts and 500-level server errors are retriable, but client errors like 401 or 404 are not. The userMessage property translates technical errors into user-friendly messages. Notice how 401 tells users to sign in again, while business errors pass through their domain-specific reasons. This design makes error handling decisions explicit and testable.
Time for a code review challenge. Looking at this ProfileView implementation, we have a simple SwiftUI view that fetches a user from an API. It has a State property for the user and displays the name and email in a VStack. The task modifier fetches the user when the view appears. This code compiles and might even work in simple cases, but it has multiple serious issues. Take a look and identify ALL the problems — think about error handling, user experience, testability, and architecture. What would you change and why?
Let's break down the problems. First, that try question mark is swallowing errors completely — if the fetch fails, the user sees nothing, no feedback at all. Second, there's no loading state, so the screen stays blank during the network call. Third, no error handling UI means failures are silent. And fourth, the View has a direct dependency on APIClient, making it impossible to test in isolation. Here's a better approach: extract a ViewModel marked with MainActor and Observable. The ViewModel tracks loading state, error state, and the user. It exposes a loadUser function that properly handles all states. Notice how we set isLoading, clear previous errors, and explicitly catch and store any new errors. This ViewModel is testable with a mock API client.
Now looking at the View layer, we have proper state handling. The body uses a Group to conditionally render based on the ViewModel's state. If we're loading, show a ProgressView. If there's an error, show an ErrorView with a retry action. If we have a user, show the ProfileContent. Notice the separation of concerns here: the View is purely declarative, rendering the current state. The ViewModel manages the business logic and state transitions. The task modifier calls loadUser when the view appears, but the View doesn't know or care about API clients or networking details. This architecture is testable, maintainable, and provides proper user feedback for all states.
Here's another Swift 6 challenge. Looking at this User model, we have a struct with an id, name, and a settings property. But UserSettings is a class with mutable properties for theme and notifications. This code fails Swift 6 strict concurrency checks. Can you explain why? The issue is about the Sendable protocol. When types cross actor boundaries or are used in concurrent contexts, Swift 6 requires them to be Sendable. But classes with mutable state aren't Sendable by default because they use reference semantics — multiple references to the same instance can cause data races. How would you fix this to make User fully Sendable?
Here's the correct solution. First, we change UserSettings from a class to a struct, giving us value semantics. Second, all properties are now declared with let, making them immutable. Third, we add explicit Sendable conformance to User, UserSettings, and Theme. This works because structs with immutable Sendable properties are automatically Sendable. The enum Theme is also Sendable since enums with no associated values or only Sendable associated values conform automatically. Why does this matter? In Swift 6, when you pass data between actors or use it in async contexts, the compiler enforces Sendable. This prevents data races at compile time. By using value types with immutable properties, we get safe concurrent access without locks or synchronization.
Let's tackle a performance scenario. You have a SwiftUI List with over 1000 items, and scrolling is noticeably janky. Each row displays an image and some computed formatting — maybe relative timestamps or currency formatting. Users are complaining about the experience. Walk me through your approach. How do you diagnose the issue? What profiling tools would you use? What SwiftUI-specific optimizations would you consider? How would you implement caching? Think about both the diagnostic process and the concrete solutions you'd implement. Remember, we need 60 frames per second for smooth scrolling.
Great question. Start with diagnosis: use Instruments Time Profiler to identify hot paths — where is the CPU spending time? Check if SwiftUI is re-evaluating View bodies unnecessarily. Profile image loading separately — are we decoding images on the main thread? For optimizations, first, move expensive formatting computations to the ViewModel — do them once when data loads, not in the View body. Second, use the Observable macro with granular property observation so changes to one item don't trigger updates elsewhere. Third, implement proper image caching — AsyncImage is convenient but may need a custom cache layer. Fourth, confirm you're using LazyVStack, not VStack — LazyVStack virtualizes rows, only creating views for visible items. Finally, extract row subviews with EquatableView to prevent cascading updates. Remember the key metric: 60fps means you have just 16 milliseconds per frame.
Let's wrap up with common SwiftUI pitfalls that senior engineers must avoid. {{step}}First, expensive body calls. When you put heavy computation directly in the body property, it runs on every render, causing frame drops. Move that logic to your ViewModel. {{step}}Second, reference cycles. Strong captures in Task closures create memory leaks. Always use weak self in async closures that might outlive the view. {{step}}Third, force unwrapping. The exclamation mark operator crashes your app when assumptions fail. Use if let or optional binding for safe unwrapping. {{step}}And fourth, god ViewModels. When a single ViewModel handles everything — networking, business logic, UI state — it becomes untestable and unmaintainable. Split ViewModels by feature or screen for better separation of concerns.
To summarize, this interview assesses five key areas. First, SwiftUI mastery — we're looking for deep knowledge of the Observable macro, understanding of performance patterns, and when to break view hierarchies. Second, Swift 6 concurrency — you need to demonstrate understanding of actors, the Sendable protocol, and data isolation strategies. Third, architecture thinking — can you apply MVVM effectively, plan modularization strategies, and design proper dependency injection? Fourth, error modeling — do you create domain-driven errors with retry strategies baked in? And fifth, code quality — is your code testable, maintainable, and clear? Senior engineers don't just write code that works — they architect systems with clarity and foresight. Thank you for your time today.
Use this presentation as a starting point — edit the content, change the theme, or generate a similar one with AI.