Supporting Dark Mode: Appearance-Sensitive Preferences

One of the challenges I dealt with in MarsEdit was how to adapt my existing user-facing color preferences to Dark Mode:

Screenshot of MarsEdit's preferences for text foreground and background colors.

The default values and any previously saved user customizations would be pertinent only to Light Mode. I knew I wanted to save separate values for Dark Mode, but I worried about junking up my preferences panel with separate labels and controls for each mode. After playing around with some more complicated ideas, I settled on simplicity itself: I would register and save separate values for each user-facing preference, depending on whether the app is in Dark Mode right now.

When a user switches modes, the visible color preferences in this panel change to the corresponding values for that mode. I reasoned that users are most likely to use these settings to fine-tune the appearance of the app as it appears now, and it would be intuitive to figure out if they wanted to customize the colors separately for each mode.

How did I achieve this? I decided to bottleneck access to these preferences so that as far as any consumer of the preference is concerned, there is only one value. For example, any component of my app that needs to know the “text editing color” consults a single property “bodyTextColor” on my centralized preferences controller. This is what the supporting methods, along with the property accessor, look like:

func color(forKey key: String, defaultColor: NSColor) -> NSColor {
   let defaults = UserDefaults.standard
   guard let color = defaults.rsColor(forKey: key) else {
      return defaultColor
   }
   return color
}

func setColor(color: NSColor, forKey key: String) {
   let defaults = UserDefaults.standard
   defaults.rsSetColor(color, forKey: key)
}

var bodyTextPreferenceKey: String {
   if NSApp.isDarkMode {
      return bodyTextColorForDarkModeKey
   } else {
      return bodyTextColorForLightModeKey
   }
}

@objc var bodyTextColor: NSColor {
   get {
      let key = self.bodyTextPreferenceKey
      return self.color(forKey: key, defaultColor: .textColor)
   }
   set {
      let key = self.bodyTextPreferenceKey
      self.setColor(color: newValue, forKey: key)
      self.sendNotification(.textColorPreferenceChanged)
   }
}

Because the getter and setter always consult internal properties to determine the current underlying defaults key, we always write to and read from the semantic value that the user expects. The only thing the user interface for my Preferences window needs to do is load up the color pane with the value from the getter, and respond to changes by writing with the setter.

You might be wondering what happens to the Preferences interface when the application’s appearance changes. I thought at first I would make the UI controller subscribe to appAppearanceChanged notifications, and reload the contents of the UI to match the current values of pertinent color preferences. It turned out that I didn’t even need to do that. Why?

In my centralized preferences controller, where the bottleneck methods shown above are implemented, each of the pertinent setters sends a notification that the color preference has changed. This allows observers throughout the app to update the color as needed when, for example, the user chooses a new value. If you’re using KVO or another technique for notifying clients of changes, this same approach can be adapted to that style.

After ensuring that all clients are notified when a color preference “changes,” we can reuse that same mechanism to proliferate changes in color preferences that happen to correlate with a change in appearance mode. I simply added an observer to “appAppearanceChanged” within the preference controller, where such notifications are translated into “color preference changed” notifications. The Preferences window observes these, and updates the UI. My text editors notice them, and reconfigure their appearance. Every concerned component of my app is guaranteed to know current colors regardless of how or why they have changed.

It’s worth noting that this is a scenario where using a solution like appearance-sensitive colors might also make sense. Instead of taking responsibility for notifying clients whenever these preferences change, a single color instance that adapts to current appearance would get the job done. This is a situation where even using an asset catalog doesn’t help. Because the colors in this situation are user-generated at run-time, they can’t be stored in a catalog, but a custom subclass of NSColor could achieve the same effect.

Supporting Dark Mode: Adapting Colors

Given the dramatic visual differences between appearances, virtually every color in your app will need to be varied at drawing time to work well with the current appearance.

Use Semantic Colors

The good news is all of Apple’s built-in, semantically named colors are automatically adapted to draw with a suitable color for the current appearance. For example, if you call “NSColor.textColor.set()” in light mode, and draw a string, it will render dark text, whereas in dark mode it will render light text.

Seek out areas in your app where you use hard-coded colors and determine whether a system-provided semantic color would work just as well as, or better than, the hard-coded value. Among the most common fixes in this area will be locating examples where NSColor.white is used to clear a background before drawing text, when NSColor.textBackgroundColor will do the job better, and automatically adapt itself to Dark Mode.

Vary the Color at Drawing Time

In scenarios where a truly custom color is required, you have a few options. If the color is hard-coded in a drawing method, you may be able to get away with simply querying NSAppearance.current from the point where the drawing occurs. For example, a custom NSView subclass that simply fills itself with a hard-coded color:

override func draw(_ dirtyRect: NSRect) {
   super.draw(dirtyRect)

   // Custom yellow for light mode, custom purple for dark...

   let lightColor = NSColor(red:1, green:1, blue:0.8, alpha:1)
   let darkColor = NSColor(red:0.5, green:0.3, blue:0.6, alpha:1)

   if NSAppearance.current.isDarkMode {
      darkColor.setFill()
   } else {
      lightColor.setFill()
   }

   dirtyRect.fill()
}

Use Asset Catalogs

Special cases like above are fine when you draw your own graphics, but what about views that the system frameworks draw for you? For example, because NSBox supports drawing a custom background color, you’re unlikely to implement a custom view exactly like the one above. Its functionality is redundant with what NSBox provides “for free.” But how can you ensure that NSBox will always draw in the right color for the current appearance? It only supports one “fillColor” property.

A naive approach would involve paying attention to changes in appearance, and re-setting the fill color on NSBox every time. This would get the job done, but is more complicated and error-prone than simply setting a named system color like “textBackgroundColor” and letting it handle all the details of accommodating the current appearance.

Luckily, Apple provides a mechanism for adding custom named colors that behave the same way as standard colors do. Colors that are included in an asset catalog can be identified by name, and can be varied in the catalog to represent distinct colors depending on the appearance they are used in. In short, by defining all your hard-coded colors in asset catalogs, you can treat custom named colors like “funkyBackgroundColor” the same as standard colors like “textBackgroundColor.”

Ah, but there’s a catch. To take advantage of these fancy new asset catalog features, you need to not only build your app with Xcode 10, but you need to build it on a machine that is running macOS 10.14 or greater. Evidently the ability to generate suitable asset catalogs depends on some system functionality that Xcode 10 can’t (or at least doesn’t) replicate when running on 10.13. If you try to use these features in Xcode 10 on 10.13, you’ll run into warnings like:

warning: Named colors referencing system colors must be compiled on 10.14 to maintain dynamic behavior at runtime. Using fixed values for color 'textColor'

and

Varying images and colors by appearance requires building on macOS 10.14 or later.

Because of the utility of asset colors for both colors and images, I strongly recommend updating to Xcode 10 and building on 10.14.

Appearance-Sensitive Colors

If for some reason you can’t yet build your app with Xcode 10 on macOS 10.14, you might eke out a stopgap solution similar to mine. Because I started adopting Dark Mode shortly after WWDC 2018, before Xcode 10 or macOS 10.14 were final, I felt I needed something to simulate the behavior of catalog colors, but implemented independently of them.

I developed a class for my apps called RSAppearanceSensitiveColor, a subclass of NSColor that wraps multiple colors. If you decide to do something similar, be sure to take a look at the subclassing notes in NSColor’s documentation to ensure you override all the required methods. The most functionally important methods to override are “set”, “setFill”, and “setStroke”, because these methods are called at runtime by code that is responsible for drawing. If, for example, you implement a subclass that stores “lightModeColor” and “darkModeColor” as variants, you could add an internal helper method to resolve it:

var currentColor: NSColor {
   if NSAppearance.current.isDarkMode {
      return self.darkModeColor
   }
   else {
      return self.lightModeColor
   }
}

This takes advantage of the NSAppearance.isDarkMode property I described in Checking Appearances, making it easy to specialize based on the current appearance. Remember: that’s the one that is currently being used to perform drawing, etc. When a client that is configured with one of our colors, NSBox for example, calls our set() before drawing its background:

override public func set() {
   self.currentColor.set()
}

The actual drawing color will be appropriate for the current appearance, just as with Apple’s semantic and catalog-backed colors.

Supporting Dark Mode: Responding to Change

To support Dark Mode elegantly in your app, you need to not only initialize your user interface appropriately for the current appearance, but also be prepared to adapt on the fly if the user changes appearance after your interface is already visible on the screen.

In situations where you use semantic colors or consult the current appearance at drawing time, there’s nothing else to be done. Since changing the appearance invalidates all views on the screen, your app will redraw correctly without any additional intervention.

Other situations may demand more customized behavior. I’ll give a couple examples later in this series, but for now just take my word that you may run into such scenarios.

If you have a custom view that needs to perform some action when its appearance changes, the most typical way to handle this is by implementing the “viewDidChangeEffectiveAppearance” method on NSView. This is called immediately after your view’s effectiveAppearance changes, so you can examine the new appearance, and update whatever internal state you need to before subsequent drawing or event handling methods are called:

class CustomView: NSView {
   override func viewDidChangeEffectiveAppearance() {
      // Update appearance related state here...
   }
}

If you need to react to appearance changes in the context of an NSViewController, your best bet may be to use Key Value Observing (KVO) to observe changes to the “effectiveAppearance” property on the view controller’s view. This approach will set your view controller up to be notified around the same time the view itself would be.

In some situations it’s more interesting to know whether, at a high level, the application’s appearance has shifted in a significant way. For this you can observe “effectiveAppearance” on the NSApplication.shared instance. There is no standard NotificationCenter notification for this, but I found it useful enough in my code bases that I added my own.

By centralizing KVO observation of NSApp.effectiveAppearance, I can translate all such changes into NotificationCenter-based broadcasts that any component of my app can observe. To implement something like this, first declare an NSNotification.Name extension in some high-level class such as your application delegate:

extension NSNotification.Name {
   public static let appAppearanceChanged =
      NSNotification.Name("appAppearanceChanged")
}

Then implement the centralized KVO somewhere early in launch, such as in applicationDidFinishLaunching:

observer = NSApp.observe(\.effectiveAppearance) { (app, _) in
   let nc = NotificationCenter.default
   nc.post(name: .appearanceChanged, object: app)
}

Now any code in your app that needs to be alerted when the app changes appearance can observe NSNotification.Name.appAppearanceChanged and respond by adjusting whatever it needs to.

Supporting Dark Mode: Checking Appearances

As you adapt your app to support Dark Mode, you may run into situations where you need to determine, in code, what the active appearance is. This is most typical in custom views that vary their drawing based on the current appearance, but you may also need to test higher level appearance traits to determine, for example, whether the whole application is being run “in Dark Mode” or not.

Current and Effective Appearances

To work effectively with Dark Mode support, you must appreciate the distinction between a few key properties that are all of type NSAppearance. Take some time to read the documentation and watch the videos I referenced in Educational Resources, so you really understand them. Here is a capsule summary to use as reference in the context of these articles:

  • appearance is a property of objects, such as NSApplication, NSWindow, and NSView, that implement the NSAppearanceCustomization protocol. Any such object can have an explicit appearance deliberately set on it, affecting the appearance of both itself and any objects that inherit appearance from it. As a rule, views inherit the appearance of their window, and windows inherit the appearance of the app.
  • effectiveAppearance is a property of those same objects, taking into account the inheritance hierarchy and returning a suitable appearance in the likely event that no explicit value has been set on the object.
  • NSAppearance.current or +[NSAppearance currentAppearance] is a class property of NSAppearance that describes the appearance that is currently in effect for the running thread. Practically speaking you can think of this property as an ephemeral drawing variable akin to the current fill color or stroke color. Its value impacts the manner in which drawing that is happening right now should be handled. Don’t confuse it with high-level user-facing options about which mode is set for the application as a whole.

High-Level Appearance Traits

As you modify your code to respect the current or effective appearance, you will probably need to make high level assessments like “is this appearance light or dark?” Because of the aforementioned complication that there are many types of NSAppearance, that they can be nested, etc., it’s not possible to simply compare the current appearance with a named appearance. Instead, you use a method on NSAppearance designed to evaluate which high-level appearance it is most like. If it’s a matter of simply distinguishing between light and dark appearances, you can use something like this:

let mode = NSAppearance.current
let isDark = mode.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua

In my applications, I found myself testing NSAppearance instances for lightness or darkness commonly enough that I implemented an “isDarkMode” property in an extension to NSAppearance:

// NSAppearance extension

@objc(rsIsDarkMode)
public var isDarkMode: Bool {
   let isDarkMode: Bool

   if #available(macOS 10.14, *) {
      if self.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
         isDarkMode = true
      } else {
         isDarkMode = false
      }
    } else {
        isDarkMode = false
    }

    return isDarkMode
}

As you can see this is laden with the requisite tests to ensure it works even if the app is running on a pre-10.14 Mac. A nice side-effect of centralizing this code in one place, is eliminating the need to include those checks at every call point.

This helper method is handy for working with NSAppearance instances, but in some contexts what I really want to know is bluntly: is this app running in dark mode or not? To accommodate this, I extend NSApplication to offer an identically named property:

// NSApplication extension

@objc(rsIsDarkMode)
public var isDarkMode: Bool {
   if #available(macOS 10.14, *) {
      return self.effectiveAppearance.isDarkMode
   } else {
      return false
   }
}

This method takes advantage of NSAppearance.isDarkMode, so it doesn’t have to replicate any of the important, but slightly clumsy “bestMatch” code either.

Having these convenient methods at my fingertips has been a great aid because it allows me to quickly answer the high-level question of whether an appearance is dark or not, regardless of whether I’m implementing drawing code or making a higher-level semantic interpretation of the application’s user-facing appearance.

Supporting Dark Mode: Opting In

To run any app in Dark Mode, you must to be running macOS Mojave 10.14 or greater. By default, all existing apps will run in Light Mode even if the system is configured to run in Dark Mode. An app that is launched on macOS Mojave will run in Dark Mode when two criteria are met:

  1. The system considers the app to be compatible with Dark Mode
  2. The running application’s appearance is set to Dark Aqua

An app’s compatibility with Dark Mode is determined by a combination of the SDK it was built against, and the value of the “NSRequiresAquaSystemAppearance” Info.plist key. If your app is built against the 10.14 SDK or later, it will be considered compatible unless the key is set to YES. If your app is built against the 10.13 SDK or earlier, it is considered incompatible unless the Info.plist key is set to NO.

When a compatible app is launched, its appearance is set to match the user’s system-wide preference, as selected in the System Preferences “General” tab. To streamline development, Apple also provides a switch in Xcode itself so that the appearance of a running app can be switched on the fly without affecting the appearance of other apps running on your Mac.

Although the Xcode switch is handy for making quick comparisons between modes, there is not, as far as I know, any mechanism to always launch an app from Xcode in Dark Mode when the system is in Light Mode, or vice-versa. If you strongly prefer one mode over the other, you may want to build in affordances to your app that support debugging in “the other mode” when you need to. For example, in the build settings for your app, find “Other Swift Flags,” and add “-DDEBUG_DARK_AQUA”:

Screnshot of the Xcode build settings for Other Swift Flags with -DDEBUG_DARK_AQUA set as the value.

Then, somewhere early in your app’s launch, you can conditionally force the appearance if specified:

func applicationDidFinishLaunching(_ notification: Notification) {
   #if DEBUG_DARK_AQUA
      NSApp.appearance = NSAppearance(named: .darkAqua)
   #endif
}

This arrangement will allow you to run Xcode and other apps in Light Aqua while debugging your own app in Dark Mode.

Supporting Dark Mode: Educational Resources

The first trick to tackling any new challenge is getting your technical references sorted. My advice to all developers is to watch the pertinent WWDC sessions, read the high-level documentation, and to immerse oneself in the Dark Mode aesthetic by perusing other applications that support it:

WWDC Sessions

It goes without saying that every Mac developer should watch What’s New in Cocoa for macOS. This is always a great overview that touches on the major changes to follow up on in other sessions. For Dark Mode in particular, be sure to watch both the Introducing Dark Mode and Advanced Dark Mode sessions. These include a lot of excellent advice about both high-level design considerations and low-level practical uses of the NSAppearance API.

Documentation

Apart from the reference documentation on NSAppearance, be sure to read the longer-form Supporting Dark Mode in Your Interface, and Providing Images for Different Appearances. These high-level guides will orient you to the types of work you will likely need to do in your app. Finally, the macOS 10.14 Release Notes include a number of details about Dark Mode, and particularly about special background blending modes and how they affect the user interface of an application.

Immerse Yourself

Although dark interfaces are nothing new, Apple’s official take on it with macOS Mojave establishes specific aesthetic choices. You’ll want to become acquainted with the decisions Apple has made so you can make the right call in your own app. I recommend switching your macOS Mojave Mac to Dark Mode and running as many apps as possible in Dark Mode to get a sense for the prevailing aesthetics.

The vast majority of Apple’s own apps have been tastefully adapted, and a growing number of 3rd party titles are listed on the Mac App Store as Apps That Look Great in Dark Mode. I’m honored that one of my own apps, MarsEdit, is listed there.

Supporting Dark Mode: Introduction

I spent a good part of the summer learning about macOS Mojave’s new Dark Mode theme, and how Mac apps can support the theme both in technical and practical ways. I adapted MarsEdit, Black Ink, FlexTime, and FastScripts to the new interface style.

During that process, I learned a lot about where to look for advice, and how to handle common scenarios. I’d like to share that advice with folks who have yet to undertake this work.

The gist of what I have to share comes from tackling challenge after challenge in my own apps. Some interfaces adapted effortlessly to Dark Mode, some needed only a little finessing, while others demanded relatively hard-core infrastructural changes.

My advice will focus on the dichotomy of Light Mode and Dark Mode. The Mac’s appearance support is more nuanced than that. NSAppearance supports a hierarchy of appearances that build upon one another. The light and dark modes are the two most prominent user-facing examples, but variations such as high contrast modes should also be considered.

These articles are loosely organized in order from more fundamental to more arcane, with a priority on establishing knowledge and techniques in earlier articles that you may need to reference in later articles. Feel free to jump around if you’re looking for something special:

Finder Quick Actions

In the What’s New in Cocoa for macOS session at WWDC 2018, Apple announced Quick Actions, which are handy little contextual tasks you can invoke on selected items in the Finder, either from a contextual menu or from the Preview side panel.

The emphasis in the session was on creating Quick Actions via Automator. There, it’s as simple as creating a new workflow document, selecting “Quick Action” from the template palette, and saving. It even puts it in the right place (~/Library/Services).

Essentially, Quick Actions appear to be macOS Services, which have a long history and which Automator has previously been able to create. In fact in macOS Mojave betas, the Quick Action document seems to completely supersede the “Service” type.

But what about native applications that want to provide Quick Actions? I didn’t see anything in the WWDC session to address this scenario, so I started poking around myself. When you click the “More…” button in Finder’s Preview panel, it opens up System Preferences’s Extensions settings, focused on a special “Finder” section. In the list are several built-in extensions.

I thought these were likely to be implemented as binary app extensions, so I instinctively control-clicked on one. A “Reveal in Finder” option appeared, so I selected it. Sure enough, they live inside Finder itself, and are packaged as “.appex” bundles, the same format that Apple supports for 3rd-party applications.

What’s handy about finding an example of an app extension you want to emulate, is you can open up its bundle and examine the Info.plist. Apple’s approach to identifying app extensions’s capabilities and appearance is based heavily on the specification of values in an NSExtension entry. Looking at one of Apples models, I saw confirmation that at least this variant was of type “com.apple.services” and that its attributes included many useful values. NSExtensionActivationRule, are substantially documented, and can be used to finely tune which types of target items an extension can perform useful actions on.

Others, such as NSExtensionServiceAllowsFinderPreviewItem and NSExtensionServiceFinderPreviewIconName do not appear to be publicly documented yet, but one can guess at what their meaning is. I’m not sure yet if the icon name has to be something public or if you can bundle a custom icon and reference it from the extension. I was alerted on Twitter to at least one other key: NSExtensionServiceAllowsTouchBarItem, which evidently triggers the action’s appearance in the Touch Bar while a qualified item is selected.

Dumping AppKit’s framework binary and grepping for likely matches reveals the following key values which are pretty easy to guess the meaning of:

NSExtensionServiceAllowsFinderPreviewItem
NSExtensionServiceFinderPreviewLabel
NSExtensionServiceFinderPreviewIconName
NSExtensionServiceAllowsTouchBarItem
NSExtensionServiceTouchBarLabel
NSExtensionServiceTouchBarIconName
NSExtensionServiceTouchBarBezelColorName
NSExtensionServiceToolbarPaletteLabel
NSExtensionServiceToolbarIconName
NSExtensionServiceToolbarIconFile
NSExtensionServiceAllowsToolbarItem

Of course, until these are documented, and even when they are, until macOS Mojave 10.14 ships, you should consider these all to be preliminary values which could disappear depending on further development by Apple of the upcoming OS.

Apple Events Usage Description

In case you haven’t heard, macOS Mojave is bringing a new “Apple Events sandbox” that will affect the behavior of apps that send Apple Events to other apps either directly, or by way of running an AppleScript. I wrote more about this on my non-development blog, Bitsplitting: Reauthorizing Automation in Mojave.

Typically, the system is simply supposed to ask users whether they approve of the Apple Events being sent from one app to another, but in some instances it seems the events are rejected without ever prompting the user. My friend Paul Kim of Hazel fame asked in a developer chat room whether any of us were having this kind of trouble specifically with apps built in Xcode 10, against the macOS 10.14 SDK.

This rang a bell for me on two levels: first, I had seen a similar behavior with FastScripts, which I eventually fixed by switching it to a much newer build system, and dropping support for versions of macOS older than 10.12. It was the right time for me to make those changes, so I didn’t mind doing it, but I never quite understood why the behavior was happening.

I noticed after building and running on my developer Mac (which is running the 10.14 beta), I was still experiencing the behavior Paul described. With FastScripts, these kinds of alerts pop up all over the place, because by virtue of running scripts, users are often incidentally sending Apple Events to other apps. Here’s a simple example script that I can run via FastScripts:

tell app "Preview"
    get document 1
end tell

When I run this with a version of FastScripts that was linked against the 10.14 SDK, I get this error message from FastScripts itself. The system never prompts to grant permission:

Scripting error received when attempting to run a script from FastScripts

Until Paul mentioned his own problems, I glossed over these failures because I was satisfied that my production built versions, linked against the 10.13 SDK, were “working fine.” But Paul’s report got me thinking: was it possible there is some unspoken contract here, whereby linking against the 10.14 SDK opens up my app to additional privacy related requirements?

I tapped into Xcode’s Info.plist editor for FastScripts, added a new field, and typed “Privacy” on a hunch, because I’ve come to realize that Apple prefixes the plain-English description for most, if not all, of their “usage explanation” Info.plist fields with this word:

Screenshot of the list of Privacy-related Info.plist strings that appears

Aha, that first one looks promising. You can right-click on an Info.plist string in Xcode to “Show Raw Keys/Values”, and doing so reveals that the Info.plist key in question is “NSAppleEventsUsageDescription”. After adding the key to my app, I built and run again, and running the same script as above now yields the expected authorization panel:

Screenshot of Apple's standard panel requesting access for FastScripts to control another app.

I’m not sure if requiring the usage description string is intentional or not, but it’s probably a good idea even if, as in the case of FastScripts, you have to be pretty vague about what the specific usage is. I don’t think this usage string was covered in the WWDC 2018 session about macOS security. Hopefully if you’ve run into this with your Mac app, this post will help you to work around the problem.

Let it Rip

In the latest Mojave public beta, I noticed a foreboding warning in the console when I build and run FastScripts, my macOS scripting utility:

FastScripts [...] is calling TIS/TSM in non-main thread environment, ERROR : This is NOT allowed. Please call TIS/TSM in main thread!!!

Ruh-roh, that doesn’t sound good. Particularly with the emphasis of three, count them three, exclamation points! I better figure out what’s going on here. But how?

Sometimes when Apple adds a log message like this, they are kind enough to offer advice about what to do to alleviate the problem. Sometimes the advice implores that we stop using a deprecated method, or in a scenario like this, offers a symbolic breakpoint we might set to zero in on exactly where the offending code lies. For example, a quick survey of my open Console app reveals:

default	11:54:18.482226 -0400	com.apple.WebKit.WebContent	Set a breakpoint at SLSLogBreak to catch errors/faults as they are logged.

This particular warning doesn’t seem to apply to my app, but if it did, I would have something good to go on if I wanted to learn more. With the TIS/TSM warning, however, I have no idea where to go. Do I even use TIS/TSM? What is TSM?

I’ve worked on Apple platforms for long enough to know that TSM stands for Text Services Manager. However, I have also worked on these platforms long enough to forget whether I’ve actually used, or am still using, such a framework in my apps! When these kinds of warnings appear in the console, as many times as not they reflect imperfections in Apple’s own framework code. Is it something Apple’s doing, or something I’m doing, that’s triggering this message?

Ideally we could set a breakpoint on the very line of code that causes this console message to be printed. This can be surprisingly difficult though. There have always been a variety of logging mechanisms. Should you set the breakpoint on NSLog, os_log, printf, fprintf, or write? I could probably figure out a comprehensive method for catching anything that might write to the console, but am I even sure this console method is being generated in my app’s main process? There are a lot of variables here. (Hah! In this particular case, I ended up digging deeper and discovering it calls “CFLog”).

This is a scenario where combining lldb’s powerful “regular expression breakpoints” and “breakpoint commands” can help a great deal. Early in my app’s launch, before the warning messages are logged, I break in lldb and add a breakpoint:

(lldb) break set -r TIS|TSM.*

I’m banking on the likelihood that whatever function is leading to this warning contains the pertinent framework prefixes. It turns out to be a good bet:

Breakpoint 5: 634 locations.

I hit continue and let my app continue launching. Here’s the problem, though: those 634 breakpoint locations include quite a few that are getting hit on a regular basis, and for several consecutive breaks, none of them is triggering the warning message I’m concerned about. This is a situation where I prefer to “let it rip” and sort out the details later:

(lldb) break command add 5
Enter your debugger command(s).  Type 'DONE' to end.
> bt
> c
> DONE
(lldb) c
Process 16022 resuming

What this does is add a series of commands that will be run automatically by lldb whenever breakpoint 5 (the one I just set) is hit. This applies to any of the 634 locations that are associated with the regular expression I provided. When the breakpoint is hit, it will first invoke the “bt” command to print a backtrace of all the calls leading up to this call, and then it will invoke the “continue” command to keep running the app. After the app has run for a bit, I search the debugger console for “!!!” which I remembered from the original warning. Locating it, I simply scroll up to see the backtrace command that had most recently been invoked:

  thread #7, stop reason = breakpoint 5.568
    frame #0: 0x00007fff2cd520fd HIToolbox`TSMGetInputSourceProperty
    frame #1: 0x00000001003faef2 RSFoundation`-[RSKeyboardStatus update](self=0x00006000002b8c00, _cmd="update") at RSKeyboardStatus.m:56
    [...]

Command #2 'c' continued the target.
2018-08-14 12:15:40.326538-0400 FastScripts[16022:624168] pid(16022)/euid(501) is calling TIS/TSM in non-main thread environment, ERROR : This is NOT allowed. Please call TIS/TSM in main thread!!! 

Sure enough, that’s my code. I’m calling TSM framework functions to handle key translation for FastScripts’s keyboard shortcut functionality, and I’m doing it (gasp!) from thread #7, which is certainly not the main thread. I oughta be ashamed…

But I’m proud, because I tracked down the root of the problem pretty efficiently using lldb’s fantastic breakpoint commands. Next time you’re at a loss for how or where something could possibly be happening, consider the possibility of setting a broad, regular expression based breakpoint, and a series of commands to help clarify what’s happening when those breakpoints are hit. Then? Let it rip!