iosNudge

Common Mistakes

Chapter 8 of 8 6 min read

How to read this chapter

Each mistake has a weak answer and a better answer. The weak answers are real patterns interviewers see every week. Most of them are not "wrong". They are just missing the reason, the trade-off or the mobile angle.

1. Designing before asking

Weak: "OK, a chat app. So we have a ChatViewController, and it calls the API..."

Better: "Before I design, a few questions. Is it one-to-one or group chat? Must it work offline? How fresh must messages be? Let me write down the scope."

Without requirements, you may design the wrong app for 40 minutes. Spend the first 5 minutes on scope.

2. Designing the backend

Weak: "We put a load balancer in front of the chat servers, shard the messages by user ID, and use Kafka for fan-out..."

Better: "I'll treat the backend as a black box with this API. The interesting part for us is the client: the local database, the send queue and the WebSocket."

The round is about the app. Talk about the server only where it touches the app: endpoints, data format, push and pagination.

3. Buzzwords without reasons

Weak: "I'd use VIPER, Combine, Core Data and Realm."

Better: "I'd use MVVM, because it fits SwiftUI and keeps logic testable. For storage I need queries over thousands of messages, so I'd use SQLite or Core Data. The trade-off is setup cost compared to plain JSON files."

Every tool you name needs a "because". If you cannot say why, do not name it.

4. Only the happy path

Weak: "The view model loads the posts and the list shows them."

Better: "The screen has five states: loading, loaded, empty, error, and offline with saved data."

enum ScreenState<Value> {
    case loading
    case loaded(Value)
    case empty
    case failed(message: String, canRetry: Bool)
    case offline(saved: Value)
}

Real apps spend most of their code on the unhappy paths. Show that you know it.

5. Ignoring the network

Weak: "When the user taps Send, we call POST /messages."

Better: "When the user taps Send, the message is saved locally with a 'sending' state and shown at once. An outbox sends it when we are online, with an idempotency key so retries are safe."

Phones lose the network in elevators, trains and basements. A mobile design that assumes a perfect network is a weak design.

6. One giant view model

Weak: "The FeedViewModel calls URLSession, parses JSON, saves to Core Data and formats the dates."

Better: "The view model only holds screen state. A repository decides between disk and network. An HTTP client does the requests. Each one is behind a protocol."

// Weak: the view model does everything, so nothing can be tested alone
final class FeedViewModelWeak {
    func load() async throws -> [Post] {
        let (data, _) = try await URLSession.shared.data(from: feedURL)
        return try JSONDecoder().decode([Post].self, from: data)
    }
}

// Better: one job, one injected dependency
@MainActor
final class FeedViewModelBetter {
    private(set) var posts: [Post] = []
    private let repository: FeedRepository

    init(repository: FeedRepository) {
        self.repository = repository
    }

    func load() async throws {
        posts = try await repository.fetchFeed(cursor: nil).posts
    }
}

7. Singletons everywhere

Weak: "Any screen can call NetworkManager.shared or Database.shared."

Better: "Dependencies are passed in through init from one composition root. Tests pass fakes. There is no hidden global state."

Singletons hide dependencies and make tests hard. It is fine to have one instance of something. It is not fine for everyone to reach it globally.

8. No trade-offs

Weak: "I'll use a WebSocket."

Better: "I'll use a WebSocket while the app is open, because messages must arrive in about a second. The cost is battery use and reconnect logic. When the app is in the background, I'll close it and rely on push notifications."

Use this pattern: choice, reason, cost, mitigation. Mitigation means how you reduce the cost.

9. Over-engineering

Weak: "For the to-do app, I'll use CRDTs, 15 Swift packages and a custom DI framework."

Better: "For a to-do app with one user and two devices, last-write-wins per item is enough. I'll keep one app target with folders. If the team grows, I'd split it into modules."

Senior engineers pick the simplest design that meets the requirements, and say how it can grow later.

10. Forgetting the main thread

Weak: "The data comes back and we update the table view."

Better: "Networking and parsing run off the main thread. The view model is @MainActor, so every UI update happens on the main thread. Swift 6 checks this at compile time."

@MainActor
@Observable
final class ProfileModel {
    private(set) var name = ""
    private let api: ProfileAPI

    init(api: ProfileAPI) {
        self.api = api
    }

    func load() async {
        // The request runs off the main actor. We come back here to set `name`.
        name = (try? await api.profile().name) ?? "Unknown"
    }
}

11. Forgetting memory

Weak: "We download the images and show them in the cells."

Better: "Images are downsampled to the cell size. A 1080 x 1080 image is about 4.7 MB in memory, but a 130-point cell only needs about 0.6 MB. The memory cache has a cost limit, and there is a disk cache for repeat visits."

A number like this makes your point concrete. Interviewers remember it.

12. Breaking old app versions

Weak: "We rename user_name to display_name in the API."

Better: "Old app versions stay installed for months. So the API only adds fields. The app ignores unknown fields and maps unknown enum values to a fallback. For a real break, we bump to /v2 or use a forced update."

enum PostKind: String, Decodable {
    case photo, video, unknown

    init(from decoder: any Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self = PostKind(rawValue: raw) ?? .unknown   // a new kind from the server does not crash old apps
    }
}

13. Skipping privacy and accessibility

Weak: "We save the auth token in UserDefaults."

Better: "The token goes in the Keychain. Personal files use data protection. Logs never contain personal data. All controls have VoiceOver labels, and text uses Dynamic Type."

These take one minute to say and show maturity.

14. Bad time management

Weak: 20 minutes on requirements, then a rushed diagram, and no deep dives.

Better: Follow time boxes: about 5 minutes for requirements, 8 for the diagram, 10 for data and API, 17 for deep dives, and 5 for the wrap-up. If you are late, say: "In the interest of time, I'll skip X and go to Y."

15. Going silent

Weak: Two minutes of silent drawing.

Better: Think out loud: "I'm deciding between a database and JSON files. The list can have thousands of items and we need sorting, so I'm leaning to a database."

The interviewer can only score what they hear. Silence also stops them from giving you hints.

16. Never checking in

Weak: A 30-minute monologue.

Better: "Does this level of detail work for you?" or "Should I go deeper on sync or move on to images?"

The interviewer often has a topic in mind. Checking in helps you find it.

A final checklist

Before you say "that's my design", check that you covered:

  • Functional and non-functional requirements, and what is out of scope
  • A diagram with layers and a single source of truth
  • Swift types for the data model
  • Endpoints, pagination and the key protocols
  • Loading, empty, error and offline states
  • 2 or 3 deep dives with trade-offs
  • Main thread, memory and battery
  • Privacy and accessibility
  • Testing, metrics and rollout
  • A 30-second summary at the end

You will not cover all of it deeply. That is fine. Mentioning each one briefly still shows you know it matters.