Printing to the Console

In this section, you’ll learn two ways your app can send messages about how it’s running. When using breakpoints, you have to stop your app to inspect things and then restart it. But sometimes, you want the app to leave little messages you can look at later if you’re trying to troubleshoot. You want logging.

Two log destinations are available to you when you’re developing. There’s the Xcode log and the system log. So far, in this lesson and in your prior lessons that used Playgrounds, you’ve seen the Xcode log. You’ve written to the log by putting either print or dump commands in your code.

print("This will get written to the console log.")
print("So will this, and the value of \(thisVariable)")

dump(anArray)

If you put these commands in your code, when Xcode executes them, the message writes to the console log. You can watch the messages in real time or open the Report navigator in Xcode to see older ones. Every time you build and run your code in Xcode, a new log is created.

Using print and dump are quick ways to leave yourself messages when you’re writing your code so that you can monitor what the app is doing without making it pause. For simple apps or temporary messages, they work pretty well.

However, they have drawbacks that become obvious as apps get more complicated. First, they need to be on the main thread to work correctly. For example, if you have a print or dump inside some network code that works in the background, it’ll likely output at odd times or not at all.

Also, print and dump use system interrupts to print to the console. If you’ve got some code where execution timing is critical, a print can throw the timing off.

Finally, print can leak sensitive information into the logs, like API keys or user passwords, so removing all of them before you deploy your app to the App Store is essential.

Unified Logging

Because of the shortcomings of print, especially around leaking sensitive information and multi-threaded apps, Apple introduced a new logging framework a few years ago in iOS 14.

With Logger, you can do a few extra tricks with log messages. Now, messages can:

  • Be recorded without impacting the timing of code.
  • Be recorded safely on any thread.
  • Have different severity levels, some of which won’t persist to disk.
  • Be marked as containing private data, so they’re hashed.
  • Have more intricate formatting options.
  • Be categorized.

When you write an app to use logging well, a lot of troubleshooting can happen without having to trace through the code looking for problems. Additionally, users can send logs to app developers when they’re experiencing a problem and so developers understand what’s happening on the user’s device.

To set up simple logging, you first need to import the framework. Usually, the import happens in the main App file or some other global area so that you can log from anywhere.

import OSLog

Your app can have one logger, but creating a different logger for different categories is more common. Perhaps you have one for app events and a different one for networking. You can have as many as you want. When reviewing logs, you can filter based on the categories.

The other data you need to supply a logger is the subsystem. This can be any string, but using your app identifier is typical because the logger writes your messages to the Xcode console when you’re using build and run. But it also writes to the system console for the device, both when you build and run and when your app runs without Xcode. So, its messages are in the same console as all of the other messages the device generates. The device generates thousands of logging messages as it runs. Using a subsystem makes it easy to filter for only your messages.

let movementLogger = Logger(subsystem: "com.example.bragbookapp", category: "movement")
let imageLogger = Logger(subsystem: "com.example.bragbookapp", category: "images")

The code above creates two loggers for the com.example.bragbookapp subsystem. In the console, you’ll be able to filter for the subsystem and then also filter for the category. It creates the loggers with a let. Once you create them, you don’t want the subsystem or category to change. If you decide to make a new category later, create a new logger with that category.

Once you create the loggers, you can use them in your code.

movementLogger.log("backward button clicked")

imageLogger.log("\(dogImages[currentImage])")

Notice that when you want to log the value of a variable like dogImages[currentImage], you must put it into an interpolated string before using it with the logger.

Using .log sets the log level to normal. Any logs set to debug, info or the default appear in the Xcode console with a plain background.

Logs with .warning or .error will appear in the console as yellow and red.

imageLogger.error("The image caused an error \(dogImages[currentImage])")

As mentioned earlier, the unified logs go into the Xcode and system logs. You can also open the Console.app on your Mac and see the logs there.

When you open the Console.app, your Mac console will be the default. However, you can also switch to the console of any of your simulators or attached iOS devices.

The Console app shows your Mac, other Apple devices, and any running simulators.
The Console app shows your Mac, other Apple devices, and any running simulators.

Once you’ve switched to the simulator, click the Start or Now buttons to start the flow of messages. You’ll see a lot of messages, and why you’ll need to filter this flow to find anything useful.

Control the flow of messages in the console using its buttons. You can also search and filter.
Control the flow of messages in the console using its buttons. You can also search and filter.

In the filter area, add a filter for the subsystem by typing:

subsystem: com.example.bragbookapp

Now, only messages that you log will appear in the logger. As you exercise the app, you’ll see more messages appear.

The logging system won’t send debug level messages out to the system console. So, as you’re working with debug level messages, you don’t have to worry about leaking information. Also, by default, the logger will redact string interpolated variables. When your app is connected to the debugger and logging, they’re not redacted, but when your app is generally collecting logs, they will be.

Alternatively, you can mark a string as .sensitive. When you do that, the logger will hash and log the string. This can be useful if you’re looking through the logs and want to see if a string has the same value in different places but don’t want to have the actual value in the logs.

For strings you want to see in the logs, mark them as .public. You mark variable privacy in the interpolation. Here are some examples:

imageLogger.log("This string is ok to show \(dogImages[currentImage], privacy: .public)")
imageLogger.log("This string should be hashed so I can look for other occurences of it \(aString, privacy: .sensitive)")

Number types are the reverse. They’re generally not redacted, but you might want them to be hidden:

movementLogger.log("Employee salary \(aSalary, privacy: .private)")

No privacy information is applied when a device is connected to the console and the debugger. All of the log messages are visible.

Generally, the system only stores debug and info messages in memory and doesn’t write them to the console or save them on disk. For other message types, such as default, error, and warning, the system writes them to disk, up to a storage limit, so you can review them later.

You can learn more about logging behavior and how to use logs at the Apple Documentation page for logging. There’s also a section there about how to format your log messages.

Being smart about writing documentation, comments, and logging can help you trace through your code and follow the logic. Then, once you’ve decided what part of your code warrants further examination, you can use breakpoints to find and fix any issues.

See forum comments
Download course materials from Github
Previous: Breakpoints Demo Next: Logging Demo