skills/metrickit/SKILL.md
Collect and analyze on-device performance metrics and crash diagnostics using MetricKit. Use when setting up MXMetricManager, handling MXMetricPayload or MXDiagnosticPayload, processing crash/hang/disk-write diagnostics via MXCallStackTree, adding custom signpost metrics, correcting mxSignpost or extended launch measurement code, or uploading telemetry to an analytics backend.
npx skillsauth add dpearson2699/swift-ios-skills metrickitInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Collect aggregated performance metrics and crash diagnostics from production devices using MetricKit. The framework delivers daily metric payloads (CPU, memory, launch time, hang rate, animation hitches, network usage) and diagnostic payloads (crashes, hangs, disk-write exceptions) with call-stack trees for triage.
Register a subscriber as early as possible — ideally in
application(_:didFinishLaunchingWithOptions:) or App.init. MetricKit
starts accumulating reports after the first access to MXMetricManager.shared.
When backfilling, state precisely that pastPayloads and
pastDiagnosticPayloads return reports generated since the last allocation of
the shared manager instance.
import MetricKit
final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
static let shared = MetricsSubscriber()
func subscribe() {
let manager = MXMetricManager.shared
manager.add(self)
// Reports generated since the last allocation of the shared manager.
processMetricPayloads(manager.pastPayloads)
processDiagnosticPayloads(manager.pastDiagnosticPayloads)
}
func unsubscribe() {
MXMetricManager.shared.remove(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
processMetricPayloads(payloads)
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
processDiagnosticPayloads(payloads)
}
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MetricsSubscriber.shared.subscribe()
return true
}
@main
struct MyApp: App {
init() {
MetricsSubscriber.shared.subscribe()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
MXMetricPayload arrives approximately once per 24 hours containing
aggregated metrics. The array may contain multiple payloads if prior
deliveries were missed.
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
let begin = payload.timeStampBegin
let end = payload.timeStampEnd
let version = payload.latestApplicationVersion
// Persist raw JSON before processing
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData, from: begin, to: end)
enqueueMetricProcessing(jsonData)
}
}
Availability: MXMetricPayload — iOS 13.0+, iPadOS 13.0+,
Mac Catalyst 13.1+, macOS 10.15+, visionOS 1.0+
MXDiagnosticPayload delivers crash, hang, CPU exception, disk-write, and
app-launch diagnostics where supported. On iOS 15+ and macOS 12+, supported
diagnostics can arrive as soon as available rather than bundled with the daily
report.
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData)
enqueueDiagnosticProcessing(jsonData)
}
}
In the background processor, inspect the typed diagnostic arrays after the raw payload is durable:
func processDiagnosticPayload(_ payload: MXDiagnosticPayload) {
if let crashes = payload.crashDiagnostics {
for crash in crashes {
handleCrash(crash)
}
}
if let hangs = payload.hangDiagnostics {
for hang in hangs {
handleHang(hang)
}
}
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
handleDiskWrite(diskWrite)
}
}
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
handleCPUException(cpuException)
}
}
#if os(iOS) || targetEnvironment(macCatalyst) || os(visionOS)
if #available(iOS 16.0, macCatalyst 16.0, visionOS 1.0, *),
let launchDiagnostics = payload.appLaunchDiagnostics {
for launchDiagnostic in launchDiagnostics {
handleSlowLaunch(launchDiagnostic)
}
}
#endif
}
Availability: MXDiagnosticPayload — iOS 14.0+, iPadOS 14.0+,
Mac Catalyst 14.0+, macOS 12.0+, visionOS 1.0+. appLaunchDiagnostics
requires iOS 16.0+, iPadOS 16.0+, Mac Catalyst 16.0+, or visionOS 1.0+.
if let launch = payload.applicationLaunchMetrics {
let firstDraw = launch.histogrammedTimeToFirstDraw
let optimized = launch.histogrammedOptimizedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
let extended = launch.histogrammedExtendedLaunch
}
if let runTime = payload.applicationTimeMetrics {
let fg = runTime.cumulativeForegroundTime // Measurement<UnitDuration>
let bg = runTime.cumulativeBackgroundTime
let bgAudio = runTime.cumulativeBackgroundAudioTime
let bgLocation = runTime.cumulativeBackgroundLocationTime
}
if let cpu = payload.cpuMetrics {
let cpuTime = cpu.cumulativeCPUTime // Measurement<UnitDuration>
}
if let memory = payload.memoryMetrics {
let peakMemory = memory.peakMemoryUsage // Measurement<UnitInformationStorage>
}
if let responsiveness = payload.applicationResponsivenessMetrics {
let hangTime = responsiveness.histogrammedApplicationHangTime
}
if let animation = payload.animationMetrics {
let scrollHitchRate = animation.scrollHitchTimeRatio // Measurement<Unit>
}
if let network = payload.networkTransferMetrics {
let wifiUp = network.cumulativeWifiUpload // Measurement<UnitInformationStorage>
let wifiDown = network.cumulativeWifiDownload
let cellUp = network.cumulativeCellularUpload
let cellDown = network.cumulativeCellularDownload
}
if let exits = payload.applicationExitMetrics {
let fg = exits.foregroundExitData
let bg = exits.backgroundExitData
// Inspect normal, abnormal, watchdog, memory, etc.
}
MXCallStackTree is attached to each diagnostic. Use jsonRepresentation() to extract frame data, then symbolicate with atos or by uploading dSYMs to your analytics service.
See references/metrickit-patterns.md for crash/hang handling code and JSON structure details.
Availability: MXCallStackTree — iOS 14.0+, iPadOS 14.0+,
Mac Catalyst 14.0+, macOS 12.0+, visionOS 1.0+
Use mxSignpost with a MetricKit log handle to capture custom performance
intervals. Leave the advanced dso, signpostID, and format parameters at
their documented defaults. Custom metrics appear in the daily MXMetricPayload
under signpostMetrics; call that out when reviewing custom MetricKit
instrumentation. When correcting custom signpost code, explicitly name
MXMetricPayload.signpostMetrics so the caller knows where the data lands.
Do not allocate or pass an OSSignpostID for the basic MetricKit pattern; use
the defaulted mxSignpost(.begin/.end, log:name:) calls unless there is a
specific overlapping-interval reason to do otherwise.
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
mxSignpost(.begin, log: metricLog, name: "DataFetch")
defer { mxSignpost(.end, log: metricLog, name: "DataFetch") }
let data = try await fetchData()
See references/metrickit-patterns.md for signpost emission patterns and reading custom metrics from payloads.
Both payload types provide jsonRepresentation() for serialization. Always
persist raw JSON to disk before processing. Use pastPayloads and
pastDiagnosticPayloads on launch to retrieve reports generated since the last
allocation of the shared manager instance.
See references/metrickit-patterns.md for export code and past payload retrieval.
Track post-first-draw setup work as part of the launch metric on iOS 16+, iPadOS 16+, Mac Catalyst 16+, macOS 13+, and visionOS 1+:
let taskID = MXLaunchTaskID("com.example.app.loadDatabase")
try MXMetricManager.extendLaunchMeasurement(forTaskID: taskID)
defer { try? MXMetricManager.finishExtendedLaunchMeasurement(forTaskID: taskID) }
restoreCachedState()
When correcting extended launch code, include the whole operational contract:
availability is iOS/iPadOS/Mac Catalyst 16+, macOS 13+, and visionOS 1+; call
the throwing MXMetricManager type methods on the main thread; start the first
task before the first scene becomes active; keep task windows overlapping;
finish every task; and stay within the 16-task limit. Extended launch times
appear under histogrammedExtendedLaunch in MXAppLaunchMetric.
Xcode Organizer shows aggregated MetricKit data across opted-in users. Use it for trend analysis alongside on-device collection routed to your own backend.
See references/metrickit-patterns.md for Organizer tab details.
Use this skill for production MetricKit ingestion, payload export, custom
MetricKit signposts, and diagnostic upload/symbolication. Route SwiftUI runtime
stutters, body-update cost, identity churn, and view invalidation fixes to
swiftui-performance. Route local Instruments, LLDB, Memory Graph, and
xctrace workflows to debugging-instruments. When explaining production
telemetry, distinguish daily metric payloads from supported diagnostics that
can arrive as soon as available.
Allocate MXMetricManager.shared and register the subscriber during app startup
so the manager can accumulate reports and deliver any previously undelivered
daily reports. Registering from a later view lifecycle hook is too easy to miss.
// WRONG — subscribing in a view controller
override func viewDidLoad() {
super.viewDidLoad()
MXMetricManager.shared.add(self)
}
// CORRECT — subscribe in application(_:didFinishLaunchingWithOptions:)
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MXMetricManager.shared.add(metricsSubscriber)
return true
}
Only handling MXMetricPayload means you miss crash, hang, and disk-write
diagnostics — the most actionable data MetricKit provides.
// WRONG — only implementing metric callback
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
// CORRECT — implement both callbacks
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
func didReceive(_ payloads: [MXDiagnosticPayload]) { /* ... */ }
Do not assume callback delivery will repeat if your own processing fails. Save the raw JSON before parsing, symbolication, or upload work.
// WRONG — process inline, crash loses data
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
riskyProcessing(p) // If this crashes, payload is gone
}
}
// CORRECT — persist raw JSON first, then process
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
try? json.write(to: localCacheURL()) // Safe on disk
Task.detached { self.processAsync(json) }
}
}
Apple documents that it is safe to process payloads on a separate thread. Keep the subscriber callback small: persist the JSON, then move expensive parsing or uploading out of the callback.
// WRONG — synchronous upload in callback
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let data = p.jsonRepresentation()
URLSession.shared.uploadTask(with: request, from: data).resume() // sync wait
}
}
// CORRECT — persist and dispatch async
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
persistLocally(json)
Task.detached(priority: .utility) {
await self.uploadToBackend(json)
}
}
}
MetricKit aggregates data over 24-hour windows. Payloads do not arrive immediately after instrumenting. Use Xcode Organizer or simulated payloads for faster iteration during development.
MXSignpostIntervalData.makeSignpostID(log:) is not documented MetricKit API.
For basic MetricKit custom metrics, create an MXMetricManager log handle and
call mxSignpost(.begin/.end, log:name:) without OSSignpostID allocation or
custom dso, signpostID, or format arguments.
MXMetricManager.shared.add(subscriber) called in application(_:didFinishLaunchingWithOptions:) or App.initMXMetricManagerSubscriber and inherits NSObjectdidReceive(_: [MXMetricPayload]) and didReceive(_: [MXDiagnosticPayload]) implementedjsonRepresentation() persisted to disk before processingMXCallStackTree JSON uploaded with dSYMs for symbolicationpastPayloads and pastDiagnosticPayloads checked on launch for missed deliveriesMXMetricManager type methods on the main thread and finish every started taskdevelopment
Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, donut, or iOS 26 3D charts; when adding chart selection, scrolling, annotations, axes, scales, legends, or foregroundStyle grouping; when plotting functions with BarPlot, LinePlot, AreaPlot, PointPlot, Chart3D, or SurfacePlot; or when creating heat maps, Gantt charts, grouped bars, sparklines, threshold lines, or spatial visualizations.
data-ai
Select, implement, or migrate between app architecture patterns for Apple platform apps. Use when choosing between MV (Model-View with @Observable), MVVM, MVI, TCA (The Composable Architecture), Clean Architecture, VIPER, or Coordinator patterns; when evaluating architecture fit for a feature's complexity; when migrating from one pattern to another; or when reviewing whether an app's current architecture is appropriate. Scoped to Apple-platform patterns using Swift 6.3, SwiftUI, and UIKit.
development
Apply Swift API Design Guidelines to name, label, and document Swift APIs. Covers argument label rules (prepositional phrase rule, grammatical phrase rule, first-label omission), mutating/nonmutating pair naming (-ed/-ing participle pattern, form- prefix, sort/sorted, formUnion/union), side-effect naming (noun for pure, verb for mutating), documentation comment structure (summary by declaration kind, O(1) complexity rule), clarity at call site, role-based naming, protocol naming (-able/-ible/-ing), default arguments over method families, casing conventions, and terminology. Use when designing new Swift APIs, reviewing naming and argument labels, writing documentation comments, or refactoring for call site clarity.
development
Implement, review, or improve in-app purchases and subscriptions using StoreKit 2. Use when building paywalls with SubscriptionStoreView or ProductView, processing transactions with Product and Transaction APIs, verifying entitlements, handling purchase flows (consumable, non-consumable, auto-renewable), implementing offer codes or promotional/win-back/introductory offers, managing subscription status and renewal state, setting up StoreKit testing with configuration files, or integrating Family Sharing, Ask to Buy, refund handling, and billing retry logic.