The hidden O(n²) in your SwiftUI List: that per-row .contains is scanning the whole array every redraw
Ran into this in a code review recently and it turned into a rabbit hole I think is worth sharing, because the code looks completely reasonable and the cost is invisible until the list grows.
The setup: a List of posts, and for each row you check wheteher it's favorited.
List(posts) { post in
PostRow(
post: post,
isFavorite: favorites.contains(where: { $0.id == post.id })
)
}
Nothing here looks wrong. But .contains(where:) scans up to every favorite, for every row. That's O(n × m), and it re-runs on every body evaluation, which during a scroll is a lot. With a few hundred posts and a few hundred favorites you're doing tens of thousands of comparisons per frame. It shows up as scroll jank you can't easily place, because no single line looks expensive.
The fix is to build a Set of the favorite IDs once, above the List, so each row becomes an O(1) lookup:
let favoriteIDs = Set(favorites.map(\.id))
List(posts) { post in
PostRow(post: post, isFavorite: favoriteIDs.contains(post.id))
}
O(n × m) becomes O(n + m). The part that bit me: it has to be built above the List, not inside the row closure. Build it inside and you're rebuilding the Set every row, which is worse than where you started.
Two things I'm curious about from people who've shipped more of this than I have:
Do you reach for this proactively, or only after Instruments points at it? I've gone back and forth on whether pre-optimizing membership checks is worth the readability cost on small lists.
And is there a cleaner pattern than a manually-built ID Set for this, something with diffable data sources or a computed lookup, that holds up in production?
(I've been making short videos connecting interview algorithms to real iOS code, and this one came from that. Happy to link it if it's useful, but the discussion is the part I actually care about.)