Monitoring for iOS with MetricKit: Getting Started

Learn how to use MetricKit to monitor power, performance, and diagnostics in your iOS apps. By Adam Rush.

Leave a rating/review
Download materials
Save for later
Share

For a long time, iOS apps were considered too small and lightweight to worry about monitoring their performance, unlike web apps. In modern times, however, iOS apps are getting bigger, more complex and doing more behind the scenes than ever before. Being able to remotely monitor how your app is running on many different devices is now vital.

Some iOS monitoring tools have been available for a long time. For example, you could query the device to see what OS is running or whether it’s using a Wi-Fi or cellular connection. Another common practice is using a third-party analytics tool, like Firebase or New Relic, to help capture what a user’s experience is really like.

However, there’s so much more diagnostic information that could help iOS developers! Apple listened and, in iOS 13, introduced MetricKit. This tool makes it much easier to retrieve diagnostic data from the device and make it available for remote monitoring for iOS. In this tutorial, you’ll explore MetricKit APIs by using Xcode to simulate receiving diagnostics.

You’ll do this by:

  • Adding MetricKit to the starter app.
  • Loading sample data into Xcode.
  • Using MetricKit to view app use statistics.

Looking Inside MetricKit

With MetricKit, you can receive diagnostic data from the OS about the physical device. It sends a report containing data for the last 24 hours in JSON format. This is helpful, for example, if you want to relay this data to your own server. You can then visualize and analyze that data and use it to improve the performance of your app.

New APIs in iOS 14

MetricKit has been around since iOS 13, but it received some significant improvements in iOS 14.

First, it introduced a brand new payload called MXDiagnosticPayload. Previously, you only received the MXMetricPayload. The new MXDiagnosticPayload gives much more information, such as crashes and exceptions. This is a great example of how Apple is continuously exploring ways of expanding frameworks.

Furthermore, Apple has introduced a brand-new performance metric called MXAppExitMetric. This object represents the types of exits made by the app in the foreground and background. This information can help you discover why users are leaving your app and in what state.

Getting Started

Download the sample project using the Download Materials button at the top or bottom of this tutorial.

For this tutorial, you’ll work with Shopping Trolley, an app that displays a simple shopping list for different types of fruit — because the world clearly needs more fruit-listing apps. :]

Note: Before you begin, you should know that MetricKit only works on a real iOS device and isn’t compatible with the simulator. So, you’ll need to plug in a real device to get this working end-to-end.

Testing MetricKit with a real iOS device

Start by opening ShoppingListTableViewController.swift. Add this code to the top of the file.

import MetricKit

Now, you can access the MetricKit framework.

Next, add this code into viewDidLoad():

let metricManager = MXMetricManager.shared
metricManager.add(self)

This accesses MetricManager provided by the framework. Then add self, the ShoppingListTableViewController, as a subscriber to metricManager, which enables it to listen for a metrics payload from the OS.

Next, you just need to add one final piece of code to the bottom of ShoppingListTableViewController.swift.

extension ShoppingListTableViewController: MXMetricManagerSubscriber {
  func didReceive(_ payloads: [MXMetricPayload]) {
    guard let firstPayload = payloads.first else { return }
    print(firstPayload.dictionaryRepresentation())
  }

  func didReceive(_ payloads: [MXDiagnosticPayload]) {
    guard let firstPayload = payloads.first else { return }
    print(firstPayload.dictionaryRepresentation())
  }
}

This is an extension on your view controller that conforms to MXMetricManagerSubscriber, which contains two methods that can receive payloads from MetricKit. In this case, when receiving the payload, you simply print it to the console. However, in your production version, this is a good place to record the metrics, such as making that API call to your server.

Congratulations! You’ve integrated MetricKit. :]

Understanding MetricKit APIs

The framework’s interface is straightforward, and you can see how much thought Apple put into it during development. The framework includes:

  • A manager class with a subscriber protocol.
  • Classes for each category of metrics and diagnostics.
  • Payload classes for the reported data.
  • Classes for measurement units, such as cellular bars. This is amazing!
  • Classes for representing accumulated data such as histograms. They’ve done the hard work again!

Understanding MXMetricManager

MXMetricManager is the beating heart of the MetricKit framework. This is the shared object that manages your subscription for receiving on-device daily metrics.

MetricKit starts accumulating reports for your app after calling shared for the first time. To start receiving metrics reports, you first call add(_ :) with a class that conforms to the MXMetricManagerSubscriber protocol.

The system then delivers these reports once per day at most. Each report contains the metrics from the past 24 hours along with any previously undelivered reports.

The Manager also has remove(_ :), which allows you to remove a subscriber at any point.

happy Swifty riding a rocket

Implementing MXSignpostMetric

A huge benefit of using the MetricKit framework is that you can now incorporate your very own metrics inside the ones Apple provides “out of the box”. This is incredibly powerful because it means you can add custom metrics to the reports you send to your server, allowing you to dig even further.

Start by adding a custom metric to log when a user loads ShoppingListTableViewController. Add fruitsLogHandle directly below the existing fruit.

let fruitsLogHandle = MXMetricManager.makeLogHandle(category: "Fruits")

This creates a handle, which is a bit like a bucket, to hold your custom metrics.

Add this code to the end of viewDidLoad().

mxSignpost(
  .event,
  log: fruitsLogHandle,
  name: "Loading Fruits TableViewController")

mxSignpost logs a custom metric within the fruits bucket when the view controller has finished loading for the user. This is a simple example, and you could really expand on it further. For example, if your app has a video player, logging when the stream starts and finishes could be useful.

Implementing MXMetricPayload

You can receive two different payloads from MetricKit. Begin with MXMetricPayload — this is an object that encapsulates the daily metrics report. Remember: To trigger the MetricKit daily report, you must have a real device plugged in! If you don’t have a device, you can still read through the tutorial, as a sample JSON payload is provided.

Build and run on the device.

Shopping Trolley app showing shopping list with apples, bananas and strawberries

Navigate back to Xcode and select DebugSimulate MetricKit Payloads.

selecting Simulate MetricKit Payloads in Xcode

This triggers a sample daily report with the same shape as the reports you’ll receive organically. You should see both of the payloads printed on the console.

 AnyHashable("cellularConditionMetrics"): {
    cellConditionTime =     {
        histogramNumBuckets = 3;
        histogramValue =         {
            0 =             {
                bucketCount = 20;
                bucketEnd = "1 bars";
                bucketStart = "1 bars";
            };
            1 =             {
                bucketCount = 30;
                bucketEnd = "2 bars";
                bucketStart = "2 bars";
            };
            2 =             {
                bucketCount = 50;
                bucketEnd = "3 bars";
                bucketStart = "3 bars";
            };
        };
    };

The code sample above reproduces a small snippet of the full payload showing cellularConditionMetrics. This aspect of the payload provides rich data about the cellular conditions your user experienced during the last 24 hours of using your app. It also delves further by telling you how many times they were on one bar, two bars or three bars of service. You can use bucketCount to create a histogram. Imagine how helpful it could be to know the average amount of time your users spend on certain bars of signal!

Understanding MXDiagnosticPayload

MXDiagnosticPayload encapsulates diagnostics data given by the device, such as:

  • Performance: crash reporting & exceptions
  • Responsiveness: application hang rate
  • Disk Access: disk read & writes

Inspect a sample of this payload.

[AnyHashable("crashDiagnostics"): <__NSArrayM 0x283764390>(
{
    callStackTree =     {
        callStackPerThread = 1;
        callStacks =         (
                        {
                callStackRootFrames =                 (
                                        {
                        address = 74565;
                        binaryName = testBinaryName;
                        binaryUUID = "BE6FD323-B011-4E67-925B-A60362A1ADFA";
                        offsetIntoBinaryTextSegment = 123;
                        sampleCount = 20;
                    }
                );
                threadAttributed = 1;
            }
        );
    };
    diagnosticMetaData =     {
        appBuildVersion = 1;
        appVersion = "1.0";
        deviceType = "iPhone13,3";
        exceptionCode = 0;
        exceptionType = 1;
        osVersion = "iPhone OS 14.4 (18D52)";
        platformArchitecture = arm64e;
        regionFormat = GB;
        signal = 11;
        terminationReason = "Namespace SIGNAL, Code 0xb";
        virtualMemoryRegionInfo = "0 is not in any region.  Bytes before following region: 4000000000 REGION TYPE                      START - END             [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL UNUSED SPACE AT START ---> __TEXT                 0000000000000000-0000000000000000 [   32K] r-x/r-x SM=COW  ...pp/Test";
    };
    version = "1.0.0";
}

The code sample above reproduces a small snippet of the full payload, showing crashDiagnostics. This captures a crash the user experienced. It includes diagnosticMetaData with helpful details like OS and app version.

Because this is a crash, the payload also has callStackTree, which you’ll explore next.

Understanding MXCallStackTree

The more you explore MetricKit, the more you can see how it powers Apple’s own tools, such as Xcode Organizer. Once your app has gone live to users, you’re still able to see the reports generated by MetricKit in Organizer, even if you don’t import the framework.

MetricKit data for Launch Time displayed in Xcode Organizer

As you can see, this app is live and has real data to explore right in Xcode. This is because Apple presents most metrics data for free. However, importing the MetricKit framework means you can leverage this data further and link it with your own metrics. It also means you have greater flexibility on how it’s displayed if your server powers custom visualizations.

MXCallStackTree is a great example of data to leverage better via MetricKit. Not only do you get reports for things like crashes and exceptions, but you also get the StackTrace provided in JSON. This could be extremely useful in going further by linking crashes to actual stack traces, so you can fix those pesky bugs.

Understanding MXAppExitMetric

MXAppExitMetric is a brand-new object provided in iOS 14 that represents how users are leaving your app. Exit types include:

  • Foreground exit
  • Background exit

Users could exit an app for many reasons. It could be an intentional decision to close the app, but sometimes the OS terminates the app — at worst, because of low memory or an exception.

In each category, you can get diagnostics on the number of:

  • Normal App Exits: The app exited normally from the foreground or background.
  • Abnormal App Exits: The app exited abnormally from the foreground or background.
  • Memory Resource Limit: The system terminated the app from the foreground or background for using too much memory.
  • Bad Access / Exception: The system terminated the app from the foreground or background for attempting an invalid memory access.

Remember: Each category is usually a property that then has relationships back to the histogram-structured data. Plus, it also links to other classes, such as MXStackTree, which means you can link certain app exits back to actual crashes.

How powerful is that? Incredibly powerful!

Viewing in Organizer

Apple provides a certain level of data for free to visualize in Organizer. This is part of a project to incorporate data from App Store Connect right in Xcode.

Organizer selected in Xcode

You don’t get access to everything provided by MetricKit in Organizer, but you can expect to find:

  • Crashes
  • Disk Writes
  • Energy Usage
  • Battery Usage
  • Hang Rate
  • Launch Time
  • Memory
  • Scrolling

Disc Writes selected in Xcode Organizer

Apple has compiled the most commonly used pieces of data here, and hopefully, they’ll keep adding to this list.

Generating Graphs & Reports

Each section described above is already rendered into various charts. Usually, data is visualized as a histogram as well as a JSON payload. You can see how MetricKit data can be consumed and displayed to great effect.

MetricKit histogram of Scroll Hitch Rate in Xcode Organizer

A histogram for Scrolling metrics is shown above. It represents scrolling hitch time for your users, which is essentially how long it takes before they can scroll on your views. In this app, you can see a massive decrease in scroll hitching, but then a sudden increase. This is certainly something you’d want to explore and figure out what changed between those versions.

Metric histogram of On-Screen Battery Usage in Xcode Organizer

Here’s another example from a live app in the App Store. This particular graph displays Battery Usage, specifically battery usage for when users are active in the app.

The level of detail is quite amazing! You can see per section of your app — Networking, Display, etc. — what most affected battery usage. In this example, the app takes 8.29% of the user’s battery per day on average. It’s unclear what the benchmark would be here, but this gives you the power to explore and determine where to make improvements.

Where to Go From Here?

You can download the final project using the Download Materials button at the top or bottom of this tutorial.

MetricKit is a massive step forward in monitoring for iOS your live apps. To use effectively, it does require some thinking about what’s important for your users. For example, if your app has lots of content that performs scrolling — such as a news-reading app — then you might want to focus on scrolling metrics.

Once you’ve determined what’s important, you can decide how to consume that data and how to present it. For example, you could have a small Vapor Swift API that consumes the data and stores it in a database with a small front end dashboard that converts it into histograms. Integrating MetricKit itself is the easiest part! :]

For an in-depth video course on using Vapor with MetricKit, check out a fully working Swift Vapor API for monitoring for iOS.

Want to learn more? WWDC has a fantastic video on What’s New in MetricKit.

Now on to monitoring for iOS apps and improving your users’ experience! If you have questions or comments, please join the discussion below.