ios-performance — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-performance (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.
[unowned self] only when the closure's lifetime is strictly shorter than the captured object (e.g., parent owns child, child's closure references parent).map, filter, forEach, reduce, compactMap, sorted(by:) are non-escaping. The closure executes synchronously and releases captures immediately.AnyObject (or be marked @objc). Strong delegates create retain cycles between owner and delegate.@Observable tracks property access per-view, so only views reading a changed property re-evaluate. ObservableObject with @Published invalidates ALL observing views on ANY published property change.VStack evaluates ALL children upfront. Use List for very large datasets (10,000+) since it reuses cells..task automatically cancels when the view disappears. Use .task(id:) to restart when a dependency changes.URLSession, DateFormatter, JSONDecoder, NumberFormatter, NSRegularExpression are expensive to create. Use shared instances or caches.| Metric | Target | Critical Threshold |
|---|---|---|
| Cold launch | <400ms to first frame | >2s triggers watchdog kill |
| Warm launch | <200ms | >1s feels broken |
| Frame rate | 60 FPS (120 on ProMotion) | <45 FPS is noticeable jank |
| Frame budget | 16.67ms (8.33ms at 120Hz) | >33ms = visible dropped frame |
| Memory (typical) | <100MB resident | >500MB risks jetsam on older devices |
| Memory (spike) | <200MB peak | Varies by device class |
| CPU idle | <3% | >5% drains battery |
| CPU active task | <80% sustained | 100% = thermal throttling |
| API response (perceived) | <200ms | >1s needs loading indicator |
| App size (download) | <50MB (OTA limit: 200MB) | >200MB requires Wi-Fi |
| Disk writes | <1MB/min sustained | Excessive writes degrade flash |
| Symptom | Instrument / Tool | Likely Cause | First Action |
|---|---|---|---|
| UI freezes / hangs | Time Profiler | Main thread blocking (sync I/O, heavy computation) | Check main thread call stack |
| Memory grows over time | Allocations + Memory Graph | Retain cycle or unbounded cache | Take heap snapshots, compare generations |
| Sudden memory spike | Allocations (VM Tracker) | Large image decode, bulk data load | Check transient allocations |
| Purple "memory" warning | Memory Graph Debugger | Retain cycle between objects | Trace reference chains |
| Dropped frames | Core Animation instrument | Offscreen rendering, layer blending | Enable "Color Blended Layers" |
| Battery drain | Energy Log | Excessive CPU, location, network polling | Check CPU/network wake frequency |
| Slow cold launch | App Launch instrument | Too many dylibs, heavy +load/init, sync main | Profile pre-main vs post-main |
| Slow scrolling (SwiftUI) | SwiftUI instrument | Non-lazy stacks, excessive redraws | Check body evaluation count |
| Slow scrolling (UIKit) | Time Profiler + Core Animation | Cell height calculation, offscreen rendering | Profile cellForRow time |
| Network slow | Network instrument | No HTTP/2 reuse, large payloads, no compression | Check connection count and sizes |
| Build slow | Xcode build timeline | Type inference, large files, no parallelism | Add -warn-long-function-bodies |
// CORRECT: [weak self] in escaping closure
func fetchData() {
networkService.fetch { [weak self] result in
guard let self else { return }
self.update(with: result)
}
}
// CORRECT: [unowned self] ONLY when lifetime is guaranteed
class Parent {
lazy var handler: () -> Void = { [unowned self] in
self.doSomething() // Parent always outlives its own lazy property
}
}
// NOT NEEDED: Non-escaping closure — no capture cycle possible
let names = users.map { $0.name } // map is non-escaping
let adults = users.filter { $0.age >= 18 } // filter is non-escaping
items.forEach { print($0) } // forEach is non-escaping
// CORRECT: Weak delegate
protocol DataServiceDelegate: AnyObject {
func didUpdate(_ data: Data)
}
class DataService {
weak var delegate: DataServiceDelegate?
}// BAD: One large view — any state change re-evaluates entire body
struct ProfileView: View {
@State private var name = ""
@State private var bio = ""
@State private var avatarURL: URL?
@State private var posts: [Post] = []
var body: some View {
ScrollView {
avatarSection // Change to name re-evaluates avatar too
bioSection
postsSection // All 500 post cells re-evaluated
}
}
}
// GOOD: Extracted subviews — state changes localized
struct ProfileView: View {
var body: some View {
ScrollView {
AvatarSection() // Only re-evaluates when avatar changes
BioSection() // Only re-evaluates when bio changes
PostsSection() // Only re-evaluates when posts change
}
}
}
// GOOD: LazyVStack for scrollable content
struct PostsSection: View {
let posts: [Post]
var body: some View {
LazyVStack { // Only creates visible cells + prefetch buffer
ForEach(posts) { post in
PostRow(post: post)
}
}
}
}
// GOOD: .task with auto-cancellation
struct UserDetailView: View {
let userID: String
@State private var user: User?
var body: some View {
content
.task(id: userID) { // Cancels & restarts if userID changes
user = try? await api.fetchUser(userID)
}
}
}// OLD (iOS 14+): ObservableObject — ALL views re-evaluate on ANY change
class UserViewModel: ObservableObject {
@Published var name = "" // Change triggers ALL observers
@Published var email = "" // Change triggers ALL observers
@Published var avatarURL: URL? // Change triggers ALL observers
}
// NEW (iOS 17+): @Observable — only views reading changed property re-evaluate
@Observable
class UserViewModel {
var name = "" // Only views reading `name` re-evaluate
var email = "" // Only views reading `email` re-evaluate
var avatarURL: URL? // Only views reading `avatarURL` re-evaluate
}Always profile on a real device (not Simulator). Use Release configuration for accurate measurements.
+load methods in ObjC code (move to +initialize or lazy init)__attribute__((constructor)))lazy var for expensive propertiesapplication(_:didFinishLaunchingWithOptions:)Scene phase detection instead of heavy AppDelegate setup// BAD: Synchronous file read on main thread
let data = try! Data(contentsOf: largeFileURL)
// GOOD: Async file read
let data = try await Task.detached {
try Data(contentsOf: largeFileURL)
}.value// BAD: Creates new DateFormatter per iteration (expensive!)
for event in events {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
labels.append(formatter.string(from: event.date))
}
// GOOD: Reuse formatter
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
for event in events {
labels.append(formatter.string(from: event.date))
}// BAD: Creates ALL 10,000 views upfront
ScrollView {
VStack {
ForEach(items) { item in // 10,000 items = 10,000 views in memory
ItemRow(item: item)
}
}
}
// GOOD: Only creates visible views
ScrollView {
LazyVStack {
ForEach(items) { item in // ~20 views in memory at a time
ItemRow(item: item)
}
}
}// BAD: Timer retains self, self retains timer → cycle
class PollingService {
var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
self.poll() // Strong capture → retain cycle
}
}
}
// GOOD: Weak capture
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
self?.poll()
}
}// BAD: Task keeps running after view disappears
.onAppear {
Task {
while !Task.isCancelled {
await refresh()
try await Task.sleep(for: .seconds(30))
}
}
}
// GOOD: .task auto-cancels on disappear
.task {
while !Task.isCancelled {
await refresh()
try? await Task.sleep(for: .seconds(30))
}
}For deep dives, see:
references/memory.md — ARC, retain cycles, value vs reference types, copy-on-write, debuggingreferences/swiftui-perf.md — View identity, @Observable, lazy containers, images, .taskreferences/instruments.md — Time Profiler, Allocations, Leaks, Energy, Core Animation, SwiftUI instrumentreferences/optimization.md — App launch, network, battery, build performance, anti-patterns~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.