MetricKit has been rebuilt from the ground up in iOS 27 with a contextually rich, Swift-first API that delivers metrics and diagnostics as async streams, plus new capabilities like Metal frame rate metrics, memory exception diagnostics, and per-state metric breakdowns via the StateReporting framework.
โข Entire API rebuilt as a modern Swift-first async stream interface replacing the old MXMetricManagerSubscriber delegate pattern
โข New Metal frame rate metric added for game developers
โข New memory exception diagnostics surface why an app or extension was terminated for exceeding its memory limit
โข Crash diagnostics now include a termination category correlating individual crashes to abnormal termination trends in metrics
โข New StateReporting framework integration allows metrics to be segmented by developer-defined app states (e.g. active tab, feature flags)
โข Async/await streams replace delegate callbacks, making it trivial to collect, encode, and ship metric and diagnostic reports to your analytics server
โข New app-state contextualization lets you attribute scroll hitches, hangs, and CPU usage to specific tabs or app configurations โ no more blended averages hiding real regressions
โข Metal frame rate metrics and memory exception diagnostics are brand new, giving game developers and memory-sensitive apps first-class on-device telemetry without third-party SDKs
A lightweight service that subscribes to MetricKit's new async streams at app launch, encodes incoming reports as JSON, and logs per-state memory and hitch data to the console โ ready to forward to an analytics backend.
import MetricKitโimport UIKit+import StateReporting+import SwiftUIโ// Old delegate-based approach (pre-iOS 27)+// MARK: - App-state domain for tab trackingโclass LegacyPerformanceMonitor: NSObject, MXMetricManagerSubscriber {โ static let shared = LegacyPerformanceMonitor()โ private let manager = MXMetricManager.shared+enum ActiveTab: String, StateValue {+ case reports+ case spending+}โ func start() {โ manager.add(self)โ }+// MARK: - MetricKit service (retain for app lifetime)โ // Called at most once per day, on a background queueโ func didReceive(_ payloads: [MXMetricPayload]) {โ for payload in payloads {โ if let memory = payload.memoryMetrics {โ print("Peak memory: \(memory.peakMemoryUsage)")+@MainActor+final class PerformanceMonitor {+ static let shared = PerformanceMonitor()+ private let manager = MetricManager()+ private let encoder: JSONEncoder = {+ let e = JSONEncoder()+ e.outputFormatting = .prettyPrinted+ return e+ }()++ func start() {+ // Stream metric reports (daily + sub-day breakdowns)+ Task.detached(priority: .utility) { [weak self] in+ guard let self else { return }+ for await report in await self.manager.metricReports {+ await self.handle(report)}โ if let hangs = payload.applicationResponsivenessMetrics {โ print("Hang rate: \(hangs.hangRate)")+ }+ // Stream diagnostic reports (crashes, hangs, memory exceptions)+ Task.detached(priority: .utility) { [weak self] in+ guard let self else { return }+ for await report in await self.manager.diagnosticReports {+ await self.handle(report)}โ // Encode entire payload as JSON for uploadโ let json = payload.jsonRepresentation()โ print("JSON payload: \(json)")}}โ func didReceive(_ payloads: [MXDiagnosticPayload]) {โ for payload in payloads {โ if let crashes = payload.crashDiagnostics {โ for crash in crashes {โ print("Crash signal: \(crash.signal ?? 0)")โ print("Backtrace: \(crash.callStackTree)")+ private func handle(_ report: MetricReport) async {+ // Forward the entire Codable report to your server+ if let data = try? encoder.encode(report),+ let json = String(data: data, encoding: .utf8) {+ print("[MetricKit] Report received:\n\(json.prefix(400))")+ }+ // Drill into memory metrics per interval+ for entry in report.intervalEntries {+ let memoryMetrics = entry.metrics.filter { $0.group == .memory }+ for metric in memoryMetrics {+ switch metric {+ case .peakMemory(let value):+ print("[MetricKit] Peak memory: \(value)")+ default:+ break}}โ if let hangs = payload.hangDiagnostics {โ for hang in hangs {โ print("Hang duration: \(hang.hangDuration)")โ }โ }}}++ private func handle(_ report: DiagnosticReport) async {+ switch report {+ case .crash(let crash):+ print("[Diagnostic] Crash โ reason: \(crash.reason), category: \(crash.terminationCategory)")+ print("[Diagnostic] Backtrace:\n\(crash.backtrace)")+ case .hang(let hang):+ print("[Diagnostic] Hang duration: \(hang.hangDuration)")+ case .memoryException(let mem):+ print("[Diagnostic] Memory exception: \(mem)")+ default:+ break+ }+ }}โ// Usage in AppDelegateโclass AppDelegate: UIResponder, UIApplicationDelegate {โ func application(_ application: UIApplication,โ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {โ LegacyPerformanceMonitor.shared.start()โ return true+// MARK: - Report app state transitions so MetricKit can segment metrics++struct ExpenseApp: App {+ @State private var selectedTab: ActiveTab = .reports+ private let tabDomain = StateDomain<ActiveTab>(id: "com.example.tab")++ var body: some Scene {+ WindowGroup {+ TabView(selection: $selectedTab) {+ Text("Reports").tabItem { Label("Reports", systemImage: "doc") }.tag(ActiveTab.reports)+ Text("Spending").tabItem { Label("Spending", systemImage: "chart.pie") }.tag(ActiveTab.spending)+ }+ .onChange(of: selectedTab) { _, newTab in+ tabDomain.transition(to: newTab)+ }+ .task {+ PerformanceMonitor.shared.start()+ tabDomain.transition(to: selectedTab)+ }+ }}}
iOS 26+ introduces a Deferred Start API for AVCaptureSession that postpones initialization of non-preview outputs (like photo and movie outputs) until after the first preview frame renders, dramatically cutting camera app launch times.
Instruments 27 introduces Run Comparisons โ a new mode that directly diffs two profiling traces side-by-side in a single document to calculate exact performance deltas โ alongside the new Top Functions analysis mode that merges scattered call-tree nodes by self-weight to instantly surface the costliest functions regardless of call hierarchy.
iOS 27 rebuilds MetricKit from the ground up with a modern, Swift-first API that delivers metric and diagnostic reports via async streams, adds Metal frame rate metrics, memory exception diagnostics, and crash termination categories, plus a new StateReporting framework to contextualize metrics by app state.
In-depth guide
iOS 26 โ iOS 27 Migration Guide โMetricManager must be retained for the lifetime of the app โ storing it only in a local variable will cancel the streams. Subscribe to both metricReports and diagnosticReports at app launch (in a detached Task or a dedicated service) to avoid data loss. MetricReports and DiagnosticReports are Codable but the new Swift-first API is not backward-compatible with the old MXMetricManager delegate approach.
Diagnostic and metric delivery depends on system conditions; reports may not arrive on simulator. Metal frame rate metrics require a device with GPU.
iOS/macOS 27 introduces the StateReporting API and look-back trace collection tools, letting developers annotate game state (levels, graphics settings, network) directly into Metal performance traces and retroactively collect hours of performance data without pre-instrumentation.