SwiftUI Requires Platform Knowledge On Top. Case Study: Fonts

SwiftUI very likely is the future of app development. But it cannot, on its own replace UIKit or AppKit. Not yet, maybe not next year, either.

Eventually you will have to drop down a level to implement a custom view, custom navigation, animation, window, or what have you. So for the time being, the two worlds of UIKit and SwiftUI co-exist and complement each other.

It gets weird when you try to (or need to) bridge genuine SwiftUI objects to make them available in UIKit. The other way around is expected, but just take a look at the many hoops the folks at Moving Parts had to jump through to get SwiftUI.Font metadata info into UIKit: https://movingparts.io/fonts-in-swiftui

And I’m not even talking about the weird stuff that uses private API.

My own SwiftUI experiments are both fun and challenging, don’t get me wrong. It’s good.

I’m worried for the next couple of years, though: if cross-platform apps are to become a thing, making them good and feature-complete on every platform requires, well, platform-specific expertise plus knowledge of SwiftUI plus knowledge to bridge these worlds. You’ll need to be fluent in SwiftUI, UIKit, and AppKit. Meanwhile, because you can get 60% or more of the app’s functionality in pure SwiftUI, you will not be exposed to UIKit and AppKit as much.

Well, maybe I’m a slow learner – but I absolutely needed to immerse myself into AppKit for years to become proficient and comfortable. The time it took to create my first app is ridiculous in hindsight. I’m not sure how to learn only the parts of AppKit that you need to complement SwiftUI, for example. I’d wager it’s impossible. Either you know how AppKit works for the most part, or you don’t. So any genuine time saving of SwiftUI for cross-platform app development won’t kick in for years. Same goes for UIKit.

What Do You Get When You Drag and Drop a PNG File From Finder Into an NSTextView?

In short: Image file drag and drop does only produce file URLs, either with security scope-able bookmarks or plain file paths.

Dragging an image file from Finder onto an NSTextView will trigger performDragOperation(_:). You get access to NSDraggingInfo there and can inspect available content. Its draggingPasteboard (shortened to pb here) contains the following data when executed on macOS 12 Monterey:

Code Value
pb.string(forType: .string) nil
pb.string(forType: .URL) nil
NSURL(from: pb) file:///.file/id=xyz123
pb.string(forType: .fileURL) file:///.file/id=xyz123
NSURL(from: pb) as URL? file:///path/to/a.png
URL(string: pb.string(
    forType: .fileURL)!)

Note that the .URL and .fileURL pasteboard types were added in macOS 10.13. Before that, you had to use the NSURL.init(from:) helper, the docs say. The table shows it still works.

Also pay attention to bridging NSURL to Swift URL: this changes the file URL or security scoped bookmark URL to an absolute file path. Accessing a resource with a security scope using these won’t work, I believe. Haven’t tested that yet.

When inspecting all available pasteboard types, you get these:

> pb.types?.map { $0.rawValue }

 "CorePasteboardFlavorType 0x6675726C",
 "Apple URL pasteboard type",

I have no clue what the dyn.-prefixed ones are supposed to be. "NSFilenamesPboardtype" can be interesting, and "public.file-url" reveals what the .fileURL pasteboard type actually is under the hood. (You could maybe add a shim and read the file URL type even in macOS 10.12 and earlier that way – but I haven’t verified if that’s true or if dragging a file on macOS 10.12 does not actually set the "public.file-url" pasteboard type.)

Also note that pb.availableType(from: [.string, .fileContents, .html, .png, .tiff]) will be nil. You would need to use the newer .fileURL type to get a non-nil result, even though NSURL.init(from:) works.

This also shows that there’s no actual image associated witht he drag operation, which may or may not surprise you.

As a bonus, if you inspect the string values of all available types, formatted for readability, you get:

> pb.types?.map { pb.string(forType: $0) }

- public.file-url

- CorePasteboardFlavorType 0x6675726C

- dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

- NSFilenamesPboardType
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

- dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

- Apple URL pasteboard type
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

- com.apple.finder.node

Don't Use Sparkle 2.x With Carthage

An old code base was still using Sparkler v1.27.0. To test the transition to the latest 2.x branch, the one that allows Sandboxing and uses XPC services under the hood, I migrated that project.

Carthage's framework out-of-the-box doesn't work well with code signing

Carthage builds of Sparkle’s v2.x branch don’t work well, though. You would need to do a lot of manual re-signing, otherwise the code signing stage of your build will fails. It does with a simple test project that embeds and signs the framework.

A couple of years ago, manual signing was necessary, but not anymore. Since my M1 Monterey Mac forces me to use Xcode 13 anyway, I figured I might now just as well use SwiftPM – and that works like a charm!

So if you get an error like:

…/SparkleTestApp.app/Contents/Frameworks/Sparkle.framework/Versions/B: code object is not signed at all
In subcomponent: …/SparkleTestApp.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate
Command CodeSign failed with a nonzero exit code

… then you might want to try to switch to SwiftPM or git submodule for Sparkle. (CocoaPods should work, too, but adding that on top of/next to Carthage seems weird.)

In The Archive and the Word Counter, I’m currently using a git submodule-based approach. That’s because I adopted the XPC-based changes quite early and needed to switch between my fork with some adjustments and the upstream code. So this is probably the first time in, what, 4 years?, that I’m relying on the vanilla version of Sparkle again. This increases my confidence in the updater I ship quite a bit.

Use macOS System Selection Colors in LIN for Emacs Line-Selection UIs

The Apple’s Human Interface Guidelines have a list of named system colors for macOS. These color names can be used for Emacs “faces”, i.e. color-and-font settings of text.

LIN to the left in a (squished) email list, hl-mode to the right in regular text

Recently, this came in handy when Protesilaos Stavrou published his LIN package source code – it’s basically an extension of the built-in hl-mode, read: highlight-line-mode. The default highlights the current line. That’s kind of useful to find your insertion point more quickly. But it’s also used in selection interfaces to highlight the currently selected line: email modes like message-mode, notmuch, mu4e, and feed reader elfeed use this to show that the actions you can perform pertains to the currently focused or highlighted line. That’s where Prot’s LIN package comes into play: it helps distinguish between highlighting the line in text editing modes and highlighting the line in selection interfaces, so you can use different colors.

You could say that LIN adds a semantic layer on top of “highlight this line”: it can distinguish between “highlight this line of text” and “highlight the selection”.

Being on macOS, I wanted the system default selection color. Blue background with white foreground color.

I used this approach for hl-line-mode since May and wrote about this back then. I couldn’t use hl-line to highlight my line in text documents if I wanted to, though, because of the color choice. This new approach improves that thanks to LIN.

To use macOS’s named colors for selections as LIN’s background and foreground instead of specifying color values directly, tweak the two LIN faces:

(when (memq window-system '(mac ns))
  (set-face-attribute 'lin-hl nil
                      :background "selectedContentBackgroundColor")

  ;; To also override the foreground (see `lin-override-foreground'):
  (set-face-attribute 'lin-hl-override-fg nil
                      :foreground "alternateSelectedControlTextColor"
                      :background "selectedContentBackgroundColor"))

The cool part about named system colors on macOS is that they are “Dynamic System Colors”: in dark mode, they produce a different value than in light mode. That means you don’t need to pick dark and light colors individually. The same color names will work.

If you switch your OS or app’s appearance to dark mode, though, you need to effectively reload the colors. Unlike native macOS apps, Emacs won’t automatically use the correct color for the current appearance.

So you need to trigger an update of LIN’s faces to pick up the color values if you change the appearance from dark to light, or light to dark. macOS builds of Emacs have a hook for this, and you can perform a color update by adding a function to ns-system-appearance-change-functions:

(defun my-lin-macos-system-colors ()
  (when (memq window-system '(mac ns))
    (set-face-attribute 'lin-hl nil
                        :background "selectedContentBackgroundColor")

    ;; To also override the foreground (see `lin-override-foreground'):
    (set-face-attribute 'lin-hl-override-fg nil
                        :foreground "alternateSelectedControlTextColor"
                        :background "selectedContentBackgroundColor")))

(when (memq window-system '(mac ns))
  (add-hook 'ns-system-appearance-change-functions #'my-lin-macos-system-colors))

This information is now also part of LIN’s README and, if Prot’s past efforts to document his packages holds true, will also become part of the manual when you install this package.

Swift Pattern Matching Operator: Compiler Error when Specifying Enum Type and Case Name?

Today’s WTF moment with Swift is related to switch-case and the pattern matching operator, ~=.

Defining operator overloads for special cases can help to keep case statements readable. And I thought it’d be simple enough to quickly check the setup, but I was in for a surprise!

So let’s say you have an enum that is RawRepresentable and backed by some integer type. Take UInt16, for example, because we’re dealing with a C API today that stores some type attributes as unsigned short and we want a nice Swift API for that:

enum CType: UInt16 {
    case two = 2

Now you can compare CType.two.rawValue == 2, and you can replace the literal 2 with the C unsigned short reference, which is exposed to Swift as UInt16.

But the .rawValue suffix gets old quickly when you have many cases in CType and want to switch over the C struct’s type directly.

So you reach for the pattern matching operator to make this shorter:

func ~= (pattern: CType, value: UInt16) -> Bool {
    return pattern.rawValue == value

With this operator overload, you can now write CType.two ~= 2, and perform pattern matching in if-case and guard-case and switch-case statements.

The weird part if how Swift allows you to do that.

So my line of thought was to write case CType.two: to be verbose and help the Swift compiler find the correct operator overload.

switch UInt16(2) {
case 1: // ...
case CType.two: print("Matched!")
default: // ...

The thing is: this doesn’t even compile!

error: enum case ‘two’ is not a member of type ‘UInt16’

Well, yes, that’s right – the case is a member of CType, as I am plainly telling in the code.

I tried other variants to help the compiler figure out what this pattern I want to match against is. It turned out that variables work just fine with Xcode 13.1 RC, as do static variables on the UInt16 type:

let cTypeVar = CType.two

extension UInt16 {
    static var cTypeStatic: CType { .two }

switch UInt16(2) {
case 2: fallthrough                   // ✓  Compiles fine
case cTypeVar: fallthrough            // ✓  Compiles fine
case UInt16.cTypeStatic: fallthrough  // ✓  Compiles fine
case CType.two: fallthrough           // ✗  Does not compile
default: break

Basically everything works except the enum wrapper for known values I have. Even a static variable that returns the same type!

Some time later on Slack, Ian S. told me to try just case .two:

switch UInt16(2) {
case 2: fallthrough                   // ✓  Compiles fine
case cTypeVar: fallthrough            // ✓  Compiles fine
case UInt16.cTypeStatic: fallthrough  // ✓  Compiles fine
case /*CType*/.two: fallthrough       // ✓  Compiles fine?!?
default: break

For all intents and purposes, this appears to be a bug in Swift – the pattern matching operator overload works when I don’t specify the type name, as you can see in the last code example, but the compiler produces an error when the .two case is written with its fully qualified type? That doesn’t make sense; if anything, I could understand if the opposite were the case: if the compiler cannot figure out what .two refers to, but has a much easier time compiling the pattern matching case statement when I type out case CType.two:...

Have reported this as FB9724060.

Replacement for NSAppearance.performAs­Current­Drawing­Appearance on macOS 10.14 and 10.15 to Fetch the Correct NSColor.cgColor

Today, I had trouble getting NSColor to work with colors from Asset catalogues when asking for its .cgColor.

Since NSColor is appearance-aware, i.e. it switches light and dark mode appropriately when used directly in your views, I wondered why asking for .cgColor always returned the initial value. Say we start the app in light mode, then this is always going to be the light mode color, never dark mode. Yes, not even if you initialize the color anew using NSColor(named: ...).cgColor.

I knew that storing the CGColor won’t dynamically update the result, but not even the computed .cgColor property? – That implies it’s not NSColor’s job to be appearance-aware. It’s someone else’s job.

That didn’t let me leave work in peace, so I spent my evening fiddling with this for an hour or two with a test app. I found that NSAppearance.performAs­CurrentDrawing­Appearance { ... } does the job. But that’s only for macOS 11+, so it won’t do.

Searching the web for performAsCurrentDrawingAppearance produces very little results. Nobody else wanting this on macOS 10.14 and 10.15? The Mozilla bug tracker has a ticket that contains a fix that sets NSAppearance.current before performing some drawing stuff – that was the only hint I found in that web search that made things click. You can and should set NSAppearance.current just like you work with fill colors and NSGraphicsContext stuff. It’s some global state, but it’s also supposed to be changed by you all the time.

In hindsight, Daniel Jalkut told as much in his 2018 article on Dark Mode:

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. [Bold emphasis mine.]

I just didn’t connect the dots properly.

And with these hints, I wrote a block-based helper that would replace performAs&shy;CurrentDrawing&shy;Appearance for me:

extension NSAppearanceCustomization {
    public func performWithEffectiveAppearanceAsDrawingAppearance<T>(
            _ block: () -> T) -> T {
        // Similar to `NSAppearance.performAsCurrentDrawingAppearance`, but
        // works below macOS 11 and assigns to `result` properly
        // (capturing `result` inside a block doesn't work the way we need).
        let result: T
        let old = NSAppearance.current
        NSAppearance.current = self.effectiveAppearance
        result = block()
        NSAppearance.current = old
        return result

extension NSColor {
    /// Uses the `NSApplication.effectiveAppearance`.
    /// If you need per-view accurate appearance, prefer this instead:
    ///     let cgColor = aView.performWithEffectiveAppearanceAsDrawingAppearance { aColor.cgColor }
    var effectiveCGColor: CGColor { NSApp.performWithEffectiveAppearanceAsDrawingAppearance { self.cgColor } }

This will now get the effective CGColor of any NSColor, based on the NSApp.effectiveAppearance.

For finer-grained control, create a context by using aView.performWith­EffectiveAppearance­AsDrawingAppearance { ... } instead. That allows each view to opt out of automatic appearance changes and set its context appropriately.

So today I learned that NSColor from Asset catalogues do not magically auto-resolve the required appearance; NSColor relies on NSAppearance.current to be set properly. That’s done by views (in draw(_:), for example, but not in viewDidChangeEffectiveAppearance() – I checked).

NSTextView Performance May Degrade for Large Plain Text Documents When usesFontPanel Is Active

I was profiling performance bottlenecks in the The Archive and noticed that no matter how much highlighting functionality I removed/commented-out, the apparent slowness was all due to … Touch Bar API?!

1.89 s   98.0%	0 s	   -[NSTextView(NSSharing) setSelectedRanges:affinity:stillSelecting:]
1.00 s   51.8%	0 s	    -[NSTextView updateFontPanel]
1.00 s   51.8%	0 s	     -[NSTextView(NSTextView_TouchBar_API) updateTextTouchBarItems]
1.00 s   51.8%	0 s	      -[NSTextTouchBarItemController setSelectedAttributesWithEnumrator:]

I have been “pruning” the Touch Bar related calls from the profiling stack to focus on what I though would be the real bottlenecks. But, as often, it turns out this was stupid and the instruments did point out the true problem. Something indeed was causing trouble here, it turned out.

Luckily, all my internal libraries/frameworks have little example apps included, and the innermost Markdown highlighting library is no different. The same file that was causing problems in The Archive was working fine there all the time, and that was my main motivator to comment-out stuff I added in modules that used the highlighter and focus on that. But in the end all that was left was a difference in the setup of Text Kit components, and there I found the real culprit. It is this setting that was activated for The Archive but not for the much faster sample app:

textView.usesFontPanel = true

That tells NSTextView to respond to font changes in the system standard font panel (often bound to ⌘T or some such in text editors).

Turning this off made the time-intensive Touch Bar API calls go away immediately. The call stack would’ve told me as much if only I hadn’t dismissed the information it was providing.

I’m not quite sure why this makes both clicking around in the text (i.e. changing the selected range) and typing so slow, yet.

I do know that some rich text editors (including TextEdit) show rich text control buttons in the Touch Bar, like buttons to control bold/italic/underline; and these would need to update as you type or move the insertion point. But these buttons aren’t even visible in a plain text NSTextView. The only text view related touch bar button is the Emoji picker. So I’m not sure why there’s this slow-down when the Touch Bar wouldn’t even need to update. All I can share at the moment is that disabling usesFontPanel outright eliminated this performance problem.

By the way, this problem was reported by a user running macOS High Sierra, so it’s not just a Big Sur bug. He also has no Touch Bar, so it might be possible that the performance bottleneck surfaces even if my dev machine was a Touch Bar-less Mac Mini or something. Not sure, though. Imagine how annoying it’d be to find this performance problem if I didn’t have a MacBook Pro with a Touch Bar, and you’d need a Touch Bar to make the problem surface. That’d be no fun at all.

Why did I use font panel support in a plain text Markdown app at all? To let users modify the app’s default font settings from the font panel, if they so desire, without going to the app preferences. Seems we can’t have nice things, though, so that might have to go away in the next update.

I’ll keep an eye open and investigate. But this is such a stupid thing that I wanted to share it as quickly as possible.

So if your syntax highlighting is slow for large documents, and your Time Profiling instrument points out something related to the Touch Bar API that somewhere in the call stack mentiones “font panel”, try usesFontPanel = false.

Retry Imperative Conditions with RxSwift Using a Delay

In The Archive, people relying on character composition to enter their text noticed that the auto-saving routing got in the way and aborted the composable editing mode.

This affects e.g. Chinese or Japanese character input on macOS, but also when you hit a composable accent like ´ after which the text editor waits for another character to put underneath the accent.

This composing editing mode is handled in NSTextInputClient via what they call “marked text”. The accent is not actually part of the NSTextView.string until you finish the composition. Same with e.g. Chinese input: you hit a bunch of keys and see composition interface on screen, but the actual string isn’t changed until you commit the change.

Since the underlying string isn’t changed, when users activate this composition mode, none of the key presses during that mode fire textDidChange or similar notifications.

When The Archive knows the user is idle, the app reacts to external file changes by displaying them right away. The idle check was bound to text changing notifications until now, so when you were using the keyboard layout for e.g. Chinese Pinyin Simplified and typed a bunch of keys, the composition mode would stay active for quite a while, not registering as “idle”.

This posed a problem in The Archive when this composition mode was active.

To check if that mode is active, a text view’s hasMarkedText().

The idle signal I am relying on lives in RxSwift land and worked like this: 1 second after the last user input or selection change, change the internal state to .idle.

To account for character composition to take longer, I had to prevent this .idle event from firing while hasMarkedText() returned true. While hasMarkedText() returns true, no other interaction with the text view is registered, so I couldn’t just ignore the .idle event – there wouldn’t be another one coming later – but had to delay it.

Why not just ignore the .idle event? After all, when users finish character composition, the text view content is changed as usual and 1 second later another .idle event would fire. But when users abort the composition, no such change event occurs. If I just drop the .idle event, then the user aborts character composition, the text view would just not begin to idle at all anymore.

Digging around in RxSwift extensions, Marin’s post about retry pointed me to RxSwiftExt’s implementation of retry from which I stole the implementation of a delay:

The use of Observable.just(()).delaySubscription(...) took some time to get used to. The immediate signal of () is just the hook to apply the actual delay. Delaying the subscription means whatever is actually happening when subscribing or flat-mapping to this observable sequence is delayed. We’re not subscribing to this sequence directly, so it affects the content of the .flatMapLatest block.

First, here’s how I use it:

let idlingAfterUserEdit = anyUserInteraction
    // Debouncing will wait for the duration to pass after
    // the latest edit, so we effectively have 1s idle delay.
    .debounce(.seconds(1), scheduler: MainScheduler.instance)
    // Delay event production while composition is active
    .flatMapLatest { _ in
        retry(until: { $0.hasMarkedText() == false },
              on: textView,
              delay: .seconds(2))
    // Handle retry timeout: just begin to idle then.
    .map { _ in .idle }

Now here’s the retryUntil implementation:

/// Produces a success event `()` either right away if `test` passes,
/// or after N tries (up to `maxTries`) with `delay` between each try.
/// - Returns: Observable sequence producing a `()` signal once the
///   condition is met, or an error when it times out.
func retry<T>(
    until test: @escaping (T) -> Bool,
    on object: T,
    delay: RxTimeInterval,
    scheduler: SchedulerType = MainScheduler.instance,
    maxTries: UInt = .max)
-> Observable<Void> {
    return retry(
        until: test,
        on: object,
        delay: delay,
        scheduler: scheduler,
        maxTries: maxTries,
        currentTry: 0)

private func retry<T>(
    until test: @escaping (T) -> Bool,
    on object: T,
    delay: RxTimeInterval,
    scheduler: SchedulerType = MainScheduler.instance,
    maxTries: UInt,
    currentTry: UInt)
-> Observable<Void> {
    if currentTry > maxTries {
        return .error("retryUntil: \(currentTry) over limit")
    if test(object) {
        return .just(())
    return .just(())
        .delaySubscription(delay, scheduler: scheduler)
        .flatMapLatest {
            retry(until: test,
                  on: object,
                  delay: delay,
                  scheduler: scheduler,
                  maxTries: maxTries,
                  currentTry: currentTry + 1)

I didn’t find value in taking the extra time to make this truly generalizable over any Observable<Element> sequence; I don’t actually care about the contents of the event that I want to delay since I’m mapping them to .idle anyway without even looking. But if you add that to your code base, please do share.

As always with RxSwift, I’m not feeling overly confident in the approach or in my ability to understand what’s going on, even if I extract well(?)-named helpers like this that don’t do much.

To inspect the call stack and see if letting this run UInt.max times would result in a stack overflow, I did what every good caveman does:

if currentTry > 10 { fatalError() }

Nope, looks good; the delay schedules the block itself on the target queue like DispatchQueue.main.asyncAfter(...) would.

The good news so far is that delaying the .idle event until after the comoposition mode has ended does wonders. Auto-saving of the contents still happens in the background, but the editor doesn’t abort composition and display external updates.

All of this, by the way, would’ve been so much simpler if the marked text API had some kind of notification or delegate callbacks. I pondered adding this myself, but the edge cases are too messy – e.g. you can’t rely on unmarkText() being called when the user aborts composition by clicking outside the app and bringing another window into focus.

FastSpring Introduces Multi-Discount Coupon Codes

Recently, FastSpring announced what they call “Multi-Discount Coupons”. These are coupon codes that:

  • can be used multiple times (e.g. CYBERMONDAY, to be used by any customer, as opposed to one-time use coupons)
  • can apply different discounts for multiple products.

This is different from regular coupon codes that would only apply to one product.

To implement a coupon-based discount for a combination of products, the best bet so far was to create a (temporary) product bundle and apply a re-usable coupon to that.

I’m glad to see FastSpring is still expanding the features of the new backend. It’s catching up to the decades old backend, and this is, I believe, even exceeding that one’s features.

In the old backend, you were able to do many more discount-based offerings that were commonplace. When the new backend was introduced, I lamented that the established patterns of offering discounts weren’t possible. We exclusively had one-time use coupons. So you couldn’t run a sale with a CYBERMONDAY coupon that anyone could use an infinite number of times (you had to generate unique codes like CYMON01A45). And you couldn’t give leads from a sponsored blog posting a discount as an incentive to purchase. (I am not implying that this is a good or bad idea. Just that it wasn’t possible, while being a very common practice; “Use coupon CTIETZE2021 for 20% off!”)

The best thing you had was to apply referral-based discounts, or, worse, create a discounted, hidden copy of your product and link folks to that. Meh.

So things are improving for sellers, that’s good.

How to Fix Mach-O Header Code 0x72613c21 When You Try to Export Your App in Xcode

I was preparing a test build to check if linking against a new library worked fine in production. Trying to distribute the app using my Developer ID (but this would also have happened in a step before uploading to the App Store), I got this:

Found an unexpected Mach-O header code: 0x72613c21


Like probably most developers, I have absolutely no clue about many crucial steps in the app making process. Especially stuff like code signing and most build settings elude me – I pick up pieces over the years, but not with the same speed with which I’m getting better at writing apps. So I didn’t understand what’s going on at all here. The search results I got weren’t that useful, so this post is meant to fill the vacuum for the next person running into this.

Didn't hurt to also make a video demo if you don't know where to click

Code 0x72613c212 Indicates You Are Embedding a Static Library

As far as I understand the message, embedding a static library in your app binary can produce code 0x72613c21. There may be other causes. But in short, this was the issue for me.

My app bundle is embedding a .framework bundle which in turn embeds a 3rd party library. The first embedding step is fine. The second isn’t, because the 3rd party library is a static library. I didn’t pay attention to this because I had no clue this is a problem in the first place.

An Apple Technical Note, TN2435, has an explanation for this. It’s a good resource, but it wasn’t among the first suggestions of my initial search; it did pop up when I asked the search engine why not embed static libraries xcode, though:

The following error indicates your app embedded a static library.

Found an unexpected Mach-O header code: 0x72613c21

This is caused by placing a static library in a bundle structure that looks like a framework; this packaging is sometimes referred to by third party framework developers as a static framework. Since the binary in these situations is a static library, apps cannot embed it in the app bundle.

How to Check If a Binary Is a Static Library

If you’re not sure if you have a static or dynamic library, TN2435 even includes instructions to check:

Terminal command to determine if a binary is a static library

file <PathToAppFramework>/<FrameworkName>.framework/<FrameworkName>

Example output for a static library

Mach-O universal binary with 2 architectures

<PathToLibrary> (for architecture armv7): current ar archive random library

<PathToLibrary> (for architecture arm64): current ar archive random library

I checked, and sure enough, this is the truncated output. Looks similar:

.../Versions/a/libMultiMarkdown: Mach-O universal binary with 2 architectures:
    [arm64:current ar archive random library] [x86_64:current ar archive random library]
.../Versions/A/libMultiMarkdown (for architecture arm64):
    current ar archive random library
.../Versions/A/libMultiMarkdown (for architecture x86_64):
    current ar archive random library

Compare this to a regular framework built in Swift:

.../Versions/A/JSONAttachment: Mach-O universal binary with 2 architectures:
    [x86_64:Mach-O 64-bit dynamically linked shared library x86_64]
    [arm64:Mach-O 64-bit dynamically linked shared library arm64]
.../Versions/A/JSONAttachment (for architecture x86_64):
    Mach-O 64-bit dynamically linked shared library x86_64
.../Versions/A/JSONAttachment (for architecture arm64):
    Mach-O 64-bit dynamically linked shared library arm64

There, you see “dynamically linked”, so conversely, the other isn’t.

This TN2435 I have just found when I wanted to look for more references for my own notes. I didn’t know about this yesterday when I ran into the problem. Most search results I found suggested using otool -h and otool -f instead. But I could not make sense of the results at all. And for the library in question, the static lib appeared to be the concatenated result of many .o files, so the output was crazy long, too.

Instead of even bothering with otool for library inspection, just check the binary product with file instead.

Fix: Do Not Embed Static Libraries

The solution, also mentioned in TN2435, is a simple change:

Once you’ve confirmed the library is static, go to the Build Phases for the app target in Xcode. Remove this library from any build phase named “Copy Files” or “Embed Frameworks.” The library should remain in the “Link Binary with Libraries” section.

By default, when you add a library to a framework or app target, Xcode chooses “Embed & Sign”. Was
the case for me as well here.

Change the library setting to 'Do Not Embed'

Change that to “Do Not Embed”, and you’re golden.

The result is that the framework is removed from the 'Embed Frameworks' build phase, but not the 'Link Binary With Libraries' phase; you can also perform this step manually, esp. if your Xcode version doesn't show the convenient 'Do Not Embed' dropdown

See also:

→ Blog Archive