← All daysDay 41 of 49 · Part 10: Architecture, testing, and performance
Day 41

Performance

These are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.

Lesson 47

Performance

SwiftUI performance is mostly a question of how often body runs and how much work it does. A view re-renders when state that it reads changes. So keep state as local as you can, read only what you need (which @Observable does for you automatically), and split large views so that each update stays small. Give list items stable ids; unstable ids force SwiftUI to rebuild rows. Use lazy containers for long content. Keep body cheap: do not sort, filter, or format a large collection inside it.

To find problems, use the SwiftUI instrument in Instruments (Xcode 26 and later), which shows which views updated and why. During development, Self._printChanges() inside body prints what caused an update.

Exercise

Profile a screen with the SwiftUI instrument. Find the view that updates most often, explain why it does, and cut its updates by at least half.

After this lesson you should be able to answer
  1. What causes a view's body to run again?
  2. How do unstable ids in a ForEach hurt performance?
  3. Which tool shows you why a view updated?
Resources
How would you test it
Interview problem · Part 10

Testing code that depends on today's date

“How would you unit test this? Take me from I can't to a passing test.”

final class CouponService {
    func canUse(code: String, expiry: Date) -> Bool {
        let used = UserDefaults.standard.bool(forKey: "used_\(code)")
        return !used && Date() < expiry
    }
}

Where this comes from: Senior iOS Interview Preparation (GitHub) · Swift Testing

Show the answer

"I can't" is the right first answer. The result depends on today's date and on whatever an earlier run left on disk. There are two hidden dependencies: the clock, and UserDefaults.standard. Turn each into something the test can supply:

struct CouponService {
    var now: () -> Date = Date.init          // seam 1: the clock
    var isUsed: (String) -> Bool             // seam 2: the disk

    func canUse(code: String, expiry: Date) -> Bool {
        !isUsed(code) && now() < expiry
    }
}

Production code passes the real clock and a closure that reads UserDefaults. The test passes a fixed moment and a constant. One parameterized test then covers the whole table — including the boundary, where an expiry equal to now has to count as expired:

@Test(arguments: [
    (used: false, secondsLeft: 60.0, expected: true),
    (used: true,  secondsLeft: 60.0, expected: false),
    (used: false, secondsLeft: -1.0, expected: false),
    (used: false, secondsLeft: 0.0,  expected: false),   // the boundary: equal is expired
])
func coupon(used: Bool, secondsLeft: Double, expected: Bool) {
    let now = Date(timeIntervalSince1970: 1_000_000)
    let service = CouponService(now: { now }, isUsed: { _ in used })
    #expect(service.canUse(code: "X", expiry: now.addingTimeInterval(secondsLeft)) == expected)
}

What interviewers listen for: That you name the hidden dependencies before you refactor, choose the lightest tool that works (a closure, not a hierarchy of protocols), and go looking for the boundary case yourself.