App Development with Swift common mistakes
App Development with Swift · Understand the traps and how to avoid them.
Passes a struct or an array into a function, mutates the parameter inside the function, and then expects the caller's original value to reflect the change.
Swift value types are copied on assignment and on argument passing, so the function mutates its own copy. Candidates carry reference-semantics intuition from languages where objects and arrays behave like shared references, or they have only used classes in Swift.
What to do instead
Decide on the semantics deliberately: mark the parameter inout and call with & when the caller's value must change, or return a new value and reassign it at the call site. If shared identity is genuinely needed, model the type as a class instead of a struct.
Writes `let ratio = total / count` with two `Int` values and expects a fractional result, or writes `let x: Double = someInt` and expects the compiler to convert the integer implicitly.
Swift performs no implicit numeric conversions between integer and floating-point types, and / on integers truncates toward zero. Languages with silent promotion make both behaviours look like they should just work, so the mistake survives until runtime output looks wrong.
What to do instead
Convert explicitly at the boundary: Double(total) / Double(count). When dividing for display, convert before dividing rather than after, and let numeric literals stay untyped so the compiler can infer the needed type from context.
Writes several `case` labels expecting C-style fall-through into one body, or leaves out a `default` clause and expects the switch to compile and silently skip unmatched values.
Swift switch must be exhaustive and cases do not fall through automatically; each case ends at its closing brace. Muscle memory from C, Java, or JavaScript leads candidates to assume shared bodies and optional exhaustiveness.
What to do instead
Use comma-separated patterns (case .a, .b:) when cases share a body, and add fallthrough only when it is genuinely intended. Cover every case, or add default; for enums from other modules, include @unknown default to stay warning-free as cases are added.
Writes a function that stores a closure for later execution but does not mark the parameter `@escaping`, then 'fixes' the compiler error by removing or inlining the closure storage entirely.
Escaping is a lifetime annotation the compiler enforces: a closure stored beyond the call must be marked @escaping. Candidates unfamiliar with the concept read the diagnostic as an arbitrary restriction rather than a statement about how long the closure lives.
What to do instead
Mark stored closures @escaping and reason about ownership: use [weak self] or [unowned self] when the closure may outlive self. Verify the decision by checking that deinit runs, or by profiling for leaks rather than guessing.
Declares a method on a struct that assigns to one of its properties but omits `mutating`, then works around the error by changing the stored property or by converting unrelated code rather than the method signature.
A struct method cannot mutate self unless it is marked mutating, and a mutating method cannot be called on an instance declared with let. Candidates treat structs as lightweight classes and expect instance methods to mutate freely.
What to do instead
Add mutating to the method and declare the instance with var at the point of use. If the type really needs identity and shared mutation, convert it to a class; otherwise keep the struct and make mutation explicit in the API.
Pattern-matches an enum with associated values as `case .success:` and then tries to read the payload as a property, or compares a raw-value enum directly against its raw type without using `rawValue`.
Associated values exist only inside the case pattern and are not stored properties, while raw values are a separate representation that requires conversion. Both look like ordinary enum payloads to candidates coming from languages where enums carry a single scalar value.
What to do instead
Bind the payload in the pattern (case let .success(payload): or case .failure(let error):) and use the bound name inside the branch. For raw-representable enums, use rawValue to read and init?(rawValue:) to construct, handling the optional that results.
Force-unwraps optional-returning APIs (`first`, dictionary subscripts, `Int(String)`, `as?`) and implicitly unwrapped outlets with `!` throughout the code, producing runtime traps instead of visible failures.
Force unwrap makes the compiler error disappear and often works on the happy path exercised during development, so the defect surfaces only for nil-producing input. Implicitly unwrapped outlets add a second trap source when a storyboard connection is missing or renamed.
What to do instead
Exit early with guard let/if let, supply a default with ??, or handle the failure explicitly with do/catch. For outlets, prefer non-optional @IBOutlet connections verified in Interface Builder, and reserve fatalError for states that are truly impossible by construction.
Uses `try?` on a throwing operation whose success is required (a file write, a save, a network decode), then continues as if it succeeded; or declares a value inside a `do` block and references it after the `catch`.
try? discards the error and converts the result to an optional, which quietly changes control flow. The do block also introduces its own scope, so a value bound inside it is invisible afterwards, and candidates read the block as a plain grouping construct.
What to do instead
Prefer do/try/catch with specific error cases, or try? only where a nil result is genuinely acceptable and handled. Declare result variables before the do block, and mark the enclosing function throws when the error should propagate to the caller.
Reads or sets `frame`, `bounds`, or `center`, or performs manual layout arithmetic, inside `viewDidLoad` and then treats the resulting sizes as final for the first render.
viewDidLoad runs after loading but before Auto Layout has solved constraints and before the view has been added to a window, so geometry is not yet meaningful. The values can appear plausible under one device size, which hides the bug during development.
What to do instead
Do geometry-dependent work in viewDidLayoutSubviews (guarding against repeated calls) or after forcing layout with layoutIfNeeded, and express layout with constraints and stack views rather than hard-coded frames. Use viewWillAppear only for sizes that are already settled.
Declares a delegate or callback property as a plain strong `var` without constraining the protocol to `AnyObject` and without `weak`, so the delegate is retained and `deinit` never runs.
Protocols can be adopted by structs, so weak requires a class-bound protocol; when the compiler rejects weak, candidates remove it to silence the error rather than constraining the protocol. The resulting retain cycle leaks silently and is easy to miss without leak tooling.
What to do instead
Declare protocol SomeDelegate: AnyObject and weak var delegate: SomeDelegate?. For closure-based callbacks, capture [weak self] and unwrap inside the closure. Confirm the fix by checking that deinit is reached, or by running the memory graph and Leaks instrument.