Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Advisory
Suggest reviewing before use
Use MetricKit for low-overhead production telemetry that complements local Instruments and Xcode Organizer analysis. On iOS and iPadOS 27, prefer the Swift-first MetricManager report sequences. Keep MXMetricManager only in an explicit iOS 26 compatibility branch.
Beta-sensitive: The iOS/iPadOS 27 surface below is based on Apple's current beta documentation. It has not been locally compiler-verified because Xcode 27 is unavailable in this environment. Re-check the linked Apple documentation and compile with the shipping Xcode 27 SDK before release.
Load MetricKit Extended and Compatibility Patterns when implementing durable ingestion, detailed report analysis, or the iOS 26 compatibility path.
At app launch, create and retain one long-lived MetricManager. Start exactly one consumer task for metricReports and one for diagnosticReports.
Both properties expose nonthrowing AsyncSequence values:
metricReports: some AsyncSequence<MetricReport, Never>diagnosticReports: some AsyncSequence<DiagnosticReport, Never>Apple documents that concurrent consumers of one sequence can receive nondeterministic subsets. Fan out only after the single consumer receives and durably stores a report; delayed subscription can miss reports.
import MetricKit
@available(iOS 27.0, *)
final class MetricsService {
private let manager = MetricManager()
private var metricTask: Task<Void, Never>?
private var diagnosticTask: Task<Void, Never>?
func start(
persistMetric: @escaping @Sendable (MetricReport) async -> Void,
persistDiagnostic: @escaping @Sendable (DiagnosticReport) async -> Void
) {
guard metricTask == nil, diagnosticTask == nil else { return }
let manager = manager
metricTask = Task {
for await report in manager.metricReports {
await persistMetric(report)
}
}
diagnosticTask = Task {
for await report in manager.diagnosticReports {
await persistDiagnostic(report)
}
}
}
deinit {
metricTask?.cancel()
diagnosticTask?.cancel()
}
}The persistence closures are application-specific. Implement them with the durable-first workflow below rather than dropping, logging only, or directly uploading each report. If state-scoped metrics are needed, construct the manager with the documented init(enabledStateReportingDomains:) initializer and the required domains.
MetricReport is Codable and Sendable. It describes an interval through:
timeRange: DateIntervalenvironment metadataintervalEntries for full-day and shorter interval measurementsstateEntries for measurements associated with application statesMetric reports normally arrive on a daily cadence. Persist the complete report before extracting individual results.
For daily analysis, read the documented fullDayEntry and switch over its MetricResult values:
let entry = report.intervalEntries.fullDayEntry
for result in entry.values {
switch result {
case .hangTime(let metric):
analyzeHangTime(metric)
case .peakMemory(let metric):
analyzePeakMemory(metric)
case .timeToFirstDraw(let metric):
analyzeLaunch(metric)
case .signpostInterval(let metric):
analyzeSignpost(metric)
@unknown default:
preserveUnknownMetric(result)
}
}Use @unknown default so a beta or future result does not make the ingestion pipeline brittle. Preserve the raw encoded report even when the current app does not understand a result.
DiagnosticReport is Codable and Sendable. It contains a timeRange, required environment metadata, and one DiagnosticResult.
Diagnostics are individual, event-based reports intended for prompt delivery when MetricKit produces them. Do not assume every crash, hang, or resource event generates a report; system sampling and eligibility still apply.
After durable storage, route the result explicitly:
switch report.result {
case .crash(let diagnostic):
analyzeCrash(diagnostic)
case .hang(let diagnostic):
analyzeHang(diagnostic)
case .cpuException(let diagnostic):
analyzeCPUException(diagnostic)
case .diskWriteException(let diagnostic):
analyzeDiskWrites(diagnostic)
case .appLaunch(let diagnostic):
analyzeLaunch(diagnostic)
case .memoryException(let diagnostic):
analyzeMemory(diagnostic)
@unknown default:
preserveUnknownDiagnostic(report)
}The iOS/iPadOS 27 diagnostic types are CrashDiagnostic, HangDiagnostic, CPUExceptionDiagnostic, DiskWriteExceptionDiagnostic, AppLaunchDiagnostic, and MemoryExceptionDiagnostic. The memory-exception case is new in iOS 27.
Useful fields include:
| Diagnostic | Important fields |
|---|---|
CrashDiagnostic | callStackTree, exception type/code/reason, signal, virtual-memory region, termination category/reason |
HangDiagnostic | callStackTree, hangDuration |
CPUExceptionDiagnostic | callStackTree, totalCPUTime, totalSampledTime |
DiskWriteExceptionDiagnostic | callStackTree, totalBytesWritten |
AppLaunchDiagnostic | callStackTree, launchDuration |
MemoryExceptionDiagnostic | callStackTree |
Choose a small telemetry vocabulary that matches the investigation rather than exporting every result. Prioritize the relevant categories: responsiveness and terminations; runtime, CPU, memory, network, and storage; or launch, display/GPU, and custom intervals.
Aggregate by app version and environment metadata, compare distributions rather than single values, and correlate regressions with releases. Avoid treating a daily aggregate as a precise trace of one user action.
Load the Key Metric Result Catalog when mapping exact MetricResult cases into a parser or dashboard.
The iOS 27 CallStackTree replaces MXCallStackTree. It is Codable and Sendable and exposes:
forEachFrame for frame traversalcallStackThreads for thread-oriented analysisbinaryInfo for image and binary metadataStore the complete diagnostic report before flattening or symbolication. Preserve binary identifiers and offsets so server-side symbolication can use matching archives and dSYMs.
Create an OS log through the manager, then use mxSignpost for begin/end measurement:
let log = manager.logHandle(category: "ImagePipeline")
mxSignpost(.begin, log: log, name: "Decode")
await decodeImage()
mxSignpost(.end, log: log, name: "Decode")MetricKit exposes the aggregate as MetricResult.signpostInterval. Do not search for a signpostMetrics array on MetricReport.
Use mxSignpost when MetricKit resource measurement is required. Apple documents that intervals created with OSSignposter and a MetricKit log handle do not populate the resource-measurement fields that mxSignpost provides.
Keep high-volume local tracing separate from the small set of stable production intervals used for MetricKit aggregation.
Treat report delivery as an at-least-once ingestion problem:
MetricReport or DiagnosticReport with JSONEncoder.let data = try JSONEncoder().encode(report)
try await durableOutbox.enqueue(data, kind: .metric)durableOutbox is an application-owned abstraction, not a MetricKit API. Do not perform a synchronous network upload inside the sequence consumer.
Durable local storage is the recovery mechanism for reports received through the modern sequences; do not make successful ingestion depend on an assumed modern backfill API.
Use trackLaunchTask(id:onTrackingError:_:) on the manager for work that extends beyond time to first draw:
await manager.trackLaunchTask(
id: "bootstrap-data",
onTrackingError: { error in
recordLaunchTrackingError(error)
}
) {
await bootstrapApplication()
}The API is @MainActor and has synchronous and asynchronous overloads. The task closure's result and thrown error propagate to the caller; a MetricManager.LaunchTaskError is reported through onTrackingError without interrupting the tracked work. Results appear as MetricResult.extendedLaunch.
Use stable LaunchTaskID values and track only launch-critical work.
For an app that still deploys to iOS 26, use an availability boundary:
MetricManager and consume both async report sequences.MXMetricManager.shared, an MXMetricManagerSubscriber, and the legacy payload callbacks.The legacy APIs remain the only documented branch with pastPayloads and pastDiagnosticPayloads. MXMetricManager is deprecated in iOS 27, so isolate it behind availability checks rather than mixing legacy payloads into the modern pipeline.
Load iOS 26 Compatibility for the full subscriber, legacy signpost, past-payload, and extended-launch patterns.
Use Xcode Organizer to inspect Apple-aggregated production metrics, hangs, crashes, and regressions before building a custom backend. Use direct MetricKit ingestion when the product needs custom correlation, retention, alerting, or integration with an existing observability system.
Do not expect development-device runs to reproduce the population, cadence, or aggregation of production reports.
| Task | Use instead |
|---|---|
| Reproduce a problem locally or record a detailed trace | debugging-instruments |
| Diagnose retained objects or memory graph paths | ios-memgraph-analysis |
| Tune SwiftUI invalidation, identity, or scrolling code | swiftui-performance |
| Study structured EnergyKit impact data | energykit |
| Design general logging and os_signpost strategy | swift-logging |
MetricKit identifies production symptoms and trends. Route the actual code fix to the skill that owns the affected subsystem.
| Mistake | Correction |
|---|---|
| Uploading directly in the sequence loop | Encode and enqueue locally first; upload asynchronously later. |
| Assuming every diagnostic event produces a report | Treat diagnostics as sampled, system-produced evidence. |
Looking for pastPayloads on MetricManager | Keep legacy backfill only in the iOS 26 MXMetricManager branch. |
| Parsing only known enum cases | Preserve the raw report and use @unknown default. |
Using MXMetricManager.makeLogHandle in the iOS 27 branch | Use manager.logHandle(category:). |
| Using paired extend/finish launch calls in the iOS 27 branch | Use trackLaunchTask. |
| Treating MetricKit aggregates as local traces | Reproduce with Instruments and signposts. |
MetricManager and one consumer per report sequence.@unknown default.manager.logHandle(category:) and mxSignpost.trackLaunchTask.MXMetricManager path is isolated behind availability checks..tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-ettrace-performance
ios-localization
ios-memgraph-analysis
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
references
swift-charts
references
swift-codable
references
swift-concurrency
swift-formatstyle
references
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references