SwiftUI Expert — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited SwiftUI Expert (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A SwiftUI screen lives or dies on state ownership: put state in the wrong place and every keystroke re-evaluates the whole tree, lists stutter, and animations tear. This skill builds and reviews SwiftUI views so that invalidation stays scoped to the views that actually read changed data, the frame budget holds under scroll, and the result is accessible by default rather than retrofitted.
Work in this order because state design determines everything downstream - fixing state after the view hierarchy is built means rewriting the hierarchy.
Collect before writing any view code. Where the user does not know, use the default and label it a guess.
Pick the narrowest wrapper that works:
@State - local, view-owned, value-type state. Also the correct owner for an @Observable model instance created by the view.@Binding - a two-way reference to state owned elsewhere. Pass a binding only when the child mutates; pass a plain value when it only reads.@Observable (Observation framework) - model objects; views observe only the properties they read, so invalidation is per-property, not per-object.@Environment - dependency injection down the tree for services and shared models.Rule: state lives at the lowest common ancestor of every view that reads or writes it - no lower (it resets when the view disappears) and no higher (it widens invalidation).
body small; extract subviews so a change invalidates one row, not the screen. A body that exceeds roughly 50 lines or three levels of nesting is a split candidate.item.title, not the whole store - so only affected views re-render.@ViewBuilder helpers instead of giant nested closures.body; compute it in the model and hand the view finished values. body must stay in the microsecond range because it can run every frame during animation.The budget is 8.3ms per frame at 120Hz, 16.7ms at 60Hz - everything on the main thread for that frame, not just your row. Practical rules:
List or LazyVStack for anything above ~50 rows so rows render on demand; a plain VStack in a ScrollView builds every row up front.ids from your data (item.id), never array indices or UUID() computed in body - unstable ids destroy row identity, causing flicker, lost scroll position, and lost row state.AsyncImage or a caching image pipeline.withAnimation { model.expanded.toggle() }..animation(_:value:) scoped to a specific value over implicit global animations, which animate things you did not intend.matchedGeometryEffect for shared-element transitions; transition for insert/remove.accessibilityReduceMotion: replace movement with opacity fades when it is set.accessibilityLabel/accessibilityValue; group related elements with accessibilityElement(children: .combine) so a row reads as one sentence, not five fragments.#Preview variants for dark mode, large Dynamic Type, and empty/loading/error states with sample data.Bad - the whole screen invalidates on every keystroke because the search text lives in a model the entire tree observes, and rows receive the whole store:
@Observable final class ScreenStore {
var searchText = ""
var items: [Item] = []
}
struct ContactList: View {
@State private var store = ScreenStore()
var body: some View {
VStack {
TextField("Search", text: $store.searchText)
ForEach(store.items) { item in
ContactRow(store: store, item: item) // row observes everything
}
}
}
}Good - search text is view-local, rows get only the values they read, and the list is lazy with stable ids:
struct ContactList: View {
@State private var searchText = ""
@Environment(ContactModel.self) private var model
var body: some View {
List(model.filtered(searchText)) { item in
ContactRow(name: item.name, isOnline: item.isOnline)
}
.searchable(text: $searchText)
}
}
struct ContactRow: View {
let name: String
let isOnline: Bool
var body: some View {
Label(name, systemImage: isOnline ? "circle.fill" : "circle")
}
}Typing in the good version invalidates the List content closure and the rows whose filtered membership changed - nothing else.
Produce working SwiftUI view code plus a one-paragraph state map stating, for each piece of state: its owner, its wrapper, and which views read vs write it. For reviews, produce a findings list ordered by frame-budget impact, each with the fix.
body evaluation - it loops or triggers undefined-behavior warnings.onAppear with a raw Task - use .task {}, which cancels automatically on disappear.Color, Image, AnyView) in models; models must stay testable without UI.equatable() or caching before fixing ownership; the bug will resurface in the next view that observes the same object.A finished view passes when:
safeAreaInset, scrollDismissesKeyboard).reduceMotion is respected.For app-wide profiling beyond a single screen, pair with mobile-perf-profiler.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.