Find Non-64-Bit Apps Using the Free Go64 Tool

From the St. Claire Software website:

Quickly scan your disk for applications, see which ones are 32-bit, and find upgrade information with the click of a button.

Go64 makes it easy to plan for the future.

And yes, it’s FREE!

Find out which apps will stop working after Mojave and why. Does deep scans of apps, e.g. checking embedded frameworks and helper app bundles.

I used to use their Default Folder X about 10 years ago, and still use Jettison. I trust the dev.

The Archive v1.4.0 Released, Introducing Tabs

This is a pretty huge update. It introduces native macOS multi-tabbing to The Archive – which means you can now open multiple tabs or windows and have different search contexts active. We call these contexts “Workspaces”. On macOS 10.12 and newer, you’ll get native tabbing of windows; for macOS 10.11 and earlier you’ll get multiple windows only.

With the addition of browser-like navigation in v1.3 and now tabs in v1.4, The Archive now sports a pretty unique set of features already for a plain text note manager!

I was surprised about the low memory consumption of my first naive attempt at both the history and multiple workspaces. When I added the history, I eventually ditched premature attempts to save RAM and started with the most simple method: when the current workspace contents are pushed to the history, truly everything is put there. The search results, which you may think are just the note titles, and their file contents. I experimented with this in an archive with 40,000 notes. While the memory consumption did increase linearly, the actual difference from a fresh start to 100 items on the navigation stack (which is the maximum) was minuscule. A couple of MB. I expected a larger explosion, to be honest.

Similarly, the multiple workspaces don’t share content. Each workspace has its own content. The current file index, which is a live representation of the file system contents, that indeed is shared, but not what you see on screen. So if you have 100 tabs with 100 navigation history items each, you’ll have 10,000 workspace snapshots in memory. While that will probably show as an increase in memory consumption, it’s not as much as you’d think. Plain text is really nice to your RAM, it turns out.

So be my guest and check out the latest version of The Archive! It’s the funnest update so far :)

NiftyMeny – Automatically Recreate a macOS App Menu Bar in HTML

Did you ever want to re-create the macOS Main Menu in HTML to show stuff related to your app? It’s hard for me to accept that anyone would ever build this, but Brett Terpstra did.

Check out the live HTML demo. This is so weird! It’s like you’re running a Mac desktop in your browser.

If you write documentation for your app that includes menus, and you want to automate the process, taking into account the ever-changing nature of an app’s menu during development, niftymenu might be just your thing! The menu can be grabbed from any app, and it comes with, well, nifty features for taking screenshots.

Empty NSWindow Title Invisible in Window List

I noticed this when experimenting with multiple tabs. When you set NSWindow.title to an empty String "", the window/tab will be hidden from the window list in the submenu below the “Window” main menu’s item. I expected a menu item with an empty label, but instead you get no item at all.

Paid Up Front: Two Perspectives to Make this Business Model Work to Your Advantage

I found an interesting connection between two articles about paid up front apps, and how this paywall can work to your advantage in two ways by separating the ap user’s population into two groups, prospects and paying custoemrs:

  1. Reduce the support volume, because far less people will be using your app compared to freemium.
  2. Learn from prospects (and increase conversion) by supplying a free-to-download variant to them.

I never thought about the effect of paywalls on support email volume until Jordan Morgan launched his app Spend Stack the other day and now published an interesting argument pro paid up front pricing. Paid up front will limit your user base a lot compared to free-to-download/freemium, that’s true, but you’ll have a far lower volume of support emails, and you will only get emails from paying customers. Jordan receives 10–15 emails/day at 500 downloads/day right after launch. If you think freemium will increase downloads tenfold to 5000/day, he would also have to deal with hundreds of emails!

Of course paying customers will be more likely to interact with the developer than people casually browsing the App Store to download any free app. So Jordan might not get 100–150 emails/day, but still he will get more. Success results in more inquiries which suck away at your hours. Will you respond to less emails in return? Is it okay for you to talk less to paying customers because non-paying users also end up in the same inbox?

There’s value in numbers, though: if you want to tailor an app according to user feedback, you will want more and not less emails. There’s at least one way I know to make this work. Separate paying from non-paying users, gather feedback, and treat your paying customers with care by installing a paywall as a filter, but then target the folks outside your paywall, too.

A piece by Joe Allen I wrote more about in 2016 gives away the details: In short, Joe Allen launches free versions of his app concept to gather lots of user feedback. People will tell you the killer features that need to be added to the app eventually. You could even split the initial concept into multiple laser-focused apps and sell them individually, at “pro pricing”. Market the resulting app to the folks that showed interest, got in touch, or subscribed to your mailing list. Then you could also end up with a great launch day1, with pre-orders and tons of people who you know will be interested in the result. This approach required preparation, and you will want to make getting in touch with you as easy as possible to (1) gather feedback, and (2) get user contact details to send them the app launch notices.

When you frame an increase of user feedback for free downloads not as cost but as an opportunity, you can split the population in two with a paywall and target them separately . In short, you can make use of the increased volume of support email if your strategy incorporates building a product based on user feedback – it’s the same cause-and-effect chain Jordan describes, but focuses on the upsides of getting in touch of users that have not paid for your app (yet). This is not a given. There’s plenty of reason not to listen to what users say they want. But that’s not the point of this post today.

To summarize, the paywall of paid up front pricing works to your advantage in multiple ways:

  1. The paywall will leave you in touch with customers – not just users of a free version of your app. You want to take care of these fine folks. The paywall thus reduces your support volume (and the volume of 1-star reviews of people who don’t seem to get what your app is about; you know the kind of review I’m talking about). Jordan has a point: you want to treat customers with care, so being able to focus on them is important.

  2. Outside the paywall, there’s far more people. They haven’t paid for your app (yet). Treat them like potential customers: Try not to end up framing the anonymous masses as a bunch of evil and/or dumb folk who want to ruin your business. I understand why some devs get frustrated, and there sure are trolls in the wild. It’s true that a lot of the world’s overall population is not your target audience. It’s both in your and your user’s interest to know if your app is a good fit. The paywall is your filter, and it’s your job to help people figure out if they want to pass the filter or not. You can work with the folks outside your paywall, generate interest, and find out what they really want from an app like yours. To get there, I think it’s clever to release a free variant of your app like Joe does.

In the end, your paid customers will receive the premium support they deserve, and you can tackle feedback by user of the free versions at a different pace (if you feel inclined to respond to all of them at all, that is). You will be able to learn from both paying customers and non-paying but potentially interested users.


  1. I don’t want to emphasize the importance of a good launch here. It’s probably an important factor to get off quickly, but it’s not the only thing that matters. Are you in the indie app business for the long haul, or is this just a one-off project? 

Variable Name Conventions in Tests

Found a question about how to “best” write the tests for the Fizz Buzz kata. The author provides his approach and questions as a GitHub Gist. One interesting takeaway is his use of the name of his constants, e.g. anyNumberDivisibleBy3 = 3. It expresses very well what kind of placeholder this number is supposed to be.

I added this to my arsenal of naming conventions when writing tests.

The last thing I discovered in 2014 coming from J. B. Rainsberger’s What your tests don’t need to know will hurt you that stuck: the “irrelevant” prefix, as in:

let irrelevantDate: Date = Date(timeSinceReferenceDate: 123)

This prefix tells me which part of a function call or object setup I should gloss over, and which are the important variables for a test case. Love it, 10/10, ★★★★★, can recommend.

J. B. Rainsberger also commented on the Gist and suggests to change test case names to be less technical, and convey more of the intent: should return FizzBuzz when it would otherwise want to return both Fizz and Buzz tells the reader that the algorithm should concatenate the result, and this is a bit easier to grok than should return FizzBuzz when the number is divisible by 3 and 5, which sounds just like another arbitrary rule. The latter still fine, but it lacks the beauty and brevity of the former. You don’t have to understand what “3 and 5” means, you don’t have to have the context of the other tests. It’s self-sufficient.

Do you have naming conventions like anyNumberDivisibleByX and irrelevantY you can recommend? Please share in the comments or on Twitter (@ctietze).

Implement NSWindow Tabbing with Multiple NSWindowControllers

After the fiasco of sharing a single NSWindowController among multiple NSWindow instances, one per tab, let’s look at a proper implementation.

For the working example, see the code on GitHub.

This approach adheres to a 1:1 relationship between window controller and window. That’s the way AppKit’s components are meant to be used, apparently. While we didn’t run into a ton of problems in the last post, you can safely assume that creating windows with window controllers performs at least some memory management tricks for us: If you look at the docs, you can see that NSWindow.windowController and NSWindowController.window are both not marked as weak or unsafe_unowned. This would create a retain cycle. Yet in practice it doesn’t. I noticed this first when reading the doc comments about windows releasing themselves when closed:

The default for NSWindow is true; the default for NSPanel is false. Release when closed, however, is ignored for windows owned by window controllers. Another strategy for releasing an NSWindow object is to have its delegate autorelease it on receiving a windowShouldClose(_:) message. (Apple Docs for NSWindow.isReleasedWhenClosed, emphasis mine.)

Whatever is actually going on, there is something going on behind the scenes.

To create a window and its window controller from scratch, my first suggestion is to extract your window from the Main.storyboard. Then you don’t have to rely on storyboard scene names to recreate windows, which I don’t like that much. It keep creation of the window pretty simple, too. Or you do things programmatically, of course. Whatever course you take, it should be as easy as calling e.g. createWindowController() to get a new instance.

func createWindowController() -> WindowController {
    let windowStoryboard = NSStoryboard(name: "WindowController", bundle: nil)
    return windowStoryboard.instantiateInitialController() as! WindowController
}

Managing window controller objects in memory

The initial window controller from your Main.storyboard file was retained for you through the conventions of @NSApplicationMain and loading the app from a storyboard. By extracting the window controller from there, the initial window will not have any special memory management anymore. It will be treated like any other instance you are going to create during the lifetime of your app: you have to retain a strong reference somewhere.

You could put the necessary code in AppDelegate, but I often find my apps outgrowing this very quickly. I try to keep the application delegate as small as possible. Managing all window instances should be some other object’s job. You can call this WindowManager or similar. For the sake of demonstration, I’ll call it TabService, because it deals with tab creation first and foremost.

TabService will have a collection of NSWindowController references. As long as TabService is around, it’ll make sure your windows will stay on screen and be wired to a window controller.

Closing tabs is the same as closing the tab’s window. If a tab, and by extension its window are closed, we want to deallocate the window controller, too. So it makes sense to listen to windowDidClose notifications. That’s why I bundle any window, its window controller, and a NSNotificationCenter subscription token in a little struct:

struct ManagedWindow {
    /// Keep the controller around to store a strong reference to it
    let windowController: NSWindowController

    /// Keep the window around to identify instances of this type
    let window: NSWindow

    /// React to window closing, auto-unsubscribing on dealloc
    let closingSubscription: NotificationToken
}

Keep in mind that NSNotificationCenter will not unsubscribe block-based notification tokens when you deallocate them automatically. I use Ole Begemann’s NotificationToken wrapper for this. You could just as well use selector–target-based subscriptions which do unsubscribe when the target is deallocated since macOS 10.11.

Create new tabs from the “+” button in the tab bar

Given an initial window on screen, we need a reference from the existing WindowController to TabService to tell it to create and store the objects for a new tab. Recall that the method for the “+” (add tab) button in macOS tab bars invoke newWindowForTab(_:) on the window controller; the implementation I end up liking the most is this:

protocol TabDelegate: class {
    func createTab(newWindowController: WindowController,
                   inWindow window: NSWindow,
                   ordered orderingMode: NSWindow.OrderingMode)
}

class WindowController: NSWindowController {
    weak var tabDelegate: TabDelegate?

    override func newWindowForTab(_ sender: Any?) {
        guard let window = self.window else { preconditionFailure("Expected window to be loaded") }
        guard let tabDelegate = self.tabDelegate else { return }

        let newWindowController = self.storyboard!.instantiateInitialController() as! WindowController

        tabDelegate.createTab(newWindowController: newWindowController,
                              inWindow: window,
                              ordered: .above)

        inspectWindowHierarchy()
    }
}

TabService will conform to TabDelegate, and I made the connection weak to adhere to the convention of declaring delegates. This avoids retain cycles. In this case, closing the window would break the cycle anyway, but I try not to get clever and break AppKit expectations without good reason, because from the tabDelegate declaration, you cannot infer when a cycle would be broken. Makes it harder to understand the code.

Here’s an implementation of TabDelegate that creates a ManagedWindow and registers for the window closing notification:

class TabService {
    fileprivate(set) var managedWindows: [ManagedWindow] = []
}

extension TabService: TabDelegate {
    func createTab(newWindowController: WindowController,
                   inWindow window: NSWindow,
                   ordered orderingMode: NSWindow.OrderingMode) {
        guard let newWindow = addManagedWindow(windowController: newWindowController)?.window 
            else { preconditionFailure() }

        window.addTabbedWindow(newWindow, ordered: orderingMode)
        newWindow.makeKeyAndOrderFront(nil)
    }

    private func addManagedWindow(windowController: WindowController) -> ManagedWindow? {
        guard let window = windowController.window else { return nil }

        // Subscribe to window closing notifications
        let subscription = NotificationCenter.default
            .observe(
                name: NSWindow.willCloseNotification, 
                object: window) { [unowned self] notification in
                    guard let window = notification.object as? NSWindow else { return }
                    self.removeManagedWindow(forWindow: window)
        }
        let management = ManagedWindow(
            windowController: windowController,
            window: window,
            closingSubscription: subscription)
        managedWindows.append(management)

        // Hook us up as the delegate for the window controller.
        // You might want to do this in another place in your app,
        // e.g. where the window controller is created,
        // to avoid side effects here.
        windowController.tabDelegate = self

        return management
    }

    /// `windowWillClose` callback.
    private func removeManagedWindow(forWindow window: NSWindow) {
        managedWindows.removeAll(where: { $0.window === window })
    }
}

Creating the first window in the app

Finally, to display the initial window and keep a strong reference to TabService, the AppDelegate will take care of the launch:

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var tabService: TabService!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        replaceTabServiceWithInitialWindow()
    }

    private func replaceTabServiceWithInitialWindow() {
        let windowController = WindowController.create()
        windowController.showWindow(self)
        tabService = TabService(initialWindowController: windowController)
    }

}

We can now easily add a “New Tab” main menu item and wire it to ⌘T for more convenient tab creation. I just use the newWindowForTab(_:) selector directly for the menu item.

Allow tab/window creation when all windows are closed

If the last window is closed, though, there’s no responder to the newWindowForTab(_:) action because all window controllers are gone! In than case, the AppDelegate can chime in.

extension AppDelegate {
    /// Fallback for the menu bar action when all windows are closed or
    /// an unrelated window is open, e.g. the About panel or a Preferences window.
    @IBAction func newWindowForTab(_ sender: Any?) {
        if let existingWindow = tabService.mainWindow {
            tabService.createTab(newWindowController: WindowController.create(),
                                 inWindow: existingWindow,
                                 ordered: .above)
        } else {
            replaceTabServiceWithInitialWindow()
        }
    }
}

extension TabService {
    /// Returns the main window of the managed window stack.
    /// Falls back the first element if no window is main. Note that this would
    /// likely be an internal inconsistency we gracefully handle here.
    var mainWindow: NSWindow? {
        let mainManagedWindow = managedWindows
            .first { $0.window.isMainWindow }

        // In case we run into the inconsistency, let it crash in debug mode so we
        // can fix our window management setup to prevent this from happening.
        assert(mainManagedWindow != nil || managedWindows.isEmpty)

        return (mainManagedWindow ?? managedWindows.first)
            .map { $0.window }
    }
}

Thanks to the responder chain, the window controller will answer first, if possible, and the AppDelegate is a fallback when no other responder is available.

I took care of not just replacing the tabService instance blindly when AppDelegate.newWindowForTab(_:) is reached. It sounds reasonable to assume that the window controller will respond in almost all cases, and that this fallback will only be used when all tabs are closed. But that’s not the truth. The fallback will be called whenever there’s no other suitable responder in the responder chain, which happens at least when

  • all tabs are closed, or
  • an unrelated window is key window.

You don’t want to throw away all your user’s tabs when she accidentally hits ⌘T to create a new tab from your app’s Preferences window!

In general, your AppDelegate may want to try to re-route incoming actions to interested objects when it’s responding to global NSApp.sendAction(_:to:from:) messages.

That, or you don’t implement @IBAction and NSResponder callbacks in your AppDelegate to explicitly avoid a global handler that isn’t aware of main menu item’s validation. It makes perfect sense to disable “New Tab” menu items when the tabbable window is not main and key window.

Conclusion

That’s all there is to it. It now just works. No weird behavior, no quick fixes necessary. This is likely the way we should be setting this up.

Again, check out the GitHub repo for the demo code and have fun making your app tabbable!

The World’s Most Comprehensive Guide to Implementing Programmatic Creation of Tabs in a Single NSWindowController That You Shouldn’t Adhere To

This is a follow-up to “Programmatically Add Tabs to NSWindows without NSDocument” from January this year. There, I was creating NSWindow instances from storyboards and re-used a single window controller to manage them. The point of that post was to solve the problem that newly created tabs will themselves not respond to the “+” button – or any menu action, for that matter, because they fell out of the responder chain at first.

This is a continuation of that idea. Please note that these are all experimental observations.

I do not recommend switching out a NSWindowController’s main window reference when switching tabs for the following reasons:

  1. As Dave DeLong pointed out, it feels wrong: you wouldn’t switch out a NSViewController.view but instead create a new view/controller combo. Same should go for windows.
  2. Here, the windows are empty; fully functional window controls will likely break in practice.
  3. You run into all kinds of weird behavor, as documented below.

Even though I do not recomment this approach, I understand people are looking for this and I can recall why I tried to do it this way myself. After all, the default tab creation selector is newWindowForTab(_:). It can be implemented anywhere in the responder chain between the window (which displays the tab bar) and the AppDelegate (which is probably the last responder you have control over). I presume that most apps don’t have many NSResponder objects between AppDelegate and NSWindowControllers, so implementing the callback in your window controllers is an idea that comes natural.

Keep in mind that implementing the callback in the NSWindowController does not mean you need to implement everything related to tab creation there! You can inform a delegate that manages multiple controllers about the event. I’ll write about a better approach in an upcoming post.

Experimental observations

Still interested in the experiment?

For the scope of this exploration into tabs, we will work with this implementation of newWindowForTab(_:), which is called when you hit the “+” button in a window’s tab bar. I put it into a window controller.

  • It shares a single window controller among all tabs.
  • It treats NSWindowController.window as the “root window” and adds tabs to it. We’ll talk about that label in a minute.
  • It activates the newly added tab immediately.
override func newWindowForTab(_ sender: Any?) {

    guard let rootWindow = self.window else {
        preconditionFailure("Expected window to be loaded")
    }

    let newWindowController = self.storyboard!.instantiateInitialController() as! WindowController
    let newWindow = newWindowController.window!

    // Add as a new tab to the right of the original window
    mainKeyWindow.addTabbedWindow(newWindow, ordered: .above)
    newWindow.delegate = self // Set delegate first to notify about key window change
    newWindow.makeKeyAndOrderFront(self)

    // `newWindowController` is not referenced and will be deallocated at the end of
    // this method, so use 1 shared controller to keep the window in the responder chain.
    newWindow.windowController = self
}

Windows without a tab bar will not always have a tab group

To maybe nobody’s surprise, this is the effect of NSWindow.TabbingMode on a sole NSWindow:

  • .disallowed: both the window’s tabbedWindows array and the tabGroup are nil.
  • .preferred: the tabbedWindows array contains 1 element, the window itself, and the tabGroup is non-nil.
  • .automatic: the window’s tabbedWindows array is nil if the tab bar is hidden, but tabGroup is non-nil. If you show the tab bar, it’s the same as starting with .preferred.

When a window is tab-able, it has a NSWindowTabGroup. When it shows tabs, its tabbedWindows array will show the visible tabs.

But even if tabbedWindows is nil, the tabGroup.windows will contain 1 element, the window itself!

So we can think of tabbedWindows as the visible tabs, or a representation of the view state, while tabGroup contains the “tab model”.

The concrete type of AppKit’s default tabGroup instance if NSWindowStackController, by the way, which appears to be a private type.

When your root window shows with the tabbing mode set to .automatic, tabbedWindows will still be nil during windowDidLoad. Only later is the array not-nil.

This is how I find out which window is contained in the collection, by the way: by setting a breakpoint and inspecting the memory addresses in the debugger output.

(lldb) po self.window!
<NSWindow: 0x600003e0c400>

(lldb) po self.window!.tabbedWindows
▿ Optional<Array<NSWindow>>
  ▿ some : 1 elements
    - 0 : <NSWindow: 0x600003e0c400>

The selected tab’s window will be key and main window when you set it programmatically

Let’s call the window controllers window property the “root window”, because we start with that one and insert tabs into it.

When you open tabs next to the root window, i.e. to the right of it, you will notice that the root window will remain key and main window.

Root window <NSWindow: 0x600003e04300> Window #1 has tabs:
-  <NSWindow: 0x600003e04300> Window #1 isKey = true , isMain = true
-  <NSWindow: 0x600003e05800> Window #5 isKey = false , isMain = false
-  <NSWindow: 0x600003e05000> Window #4 isKey = false , isMain = false
-  <NSWindow: 0x600003e08600> Window #3 isKey = false , isMain = false
-  <NSWindow: 0x600003e20200> Window #2 isKey = false , isMain = false

When you send makeKeyAndOrderFront(nil) to the new tab so it becomes active after showing, see what happens if you log the window hierarchy afterwards:

Root window <NSWindow: 0x600003e0c100> Window #1 has tabs:
-  <NSWindow: 0x600003e0c100> Window #1 isKey = false , isMain = false
-  <NSWindow: 0x600003e04700> Window #5 isKey = true , isMain = true
-  <NSWindow: 0x600003e02e00> Window #4 isKey = false , isMain = false
-  <NSWindow: 0x600003e06c00> Window #3 isKey = false , isMain = false
-  <NSWindow: 0x600003e02d00> Window #2 isKey = false , isMain = false

Note that only sending the new tab’s window the makeKey() message will not activate the tab. You need makeKeyAndOrderFront!

Manually selecting a tab will make it key, but not main window – at first

Also note that manually selecting a tab will make it key window, but it will not make it the main window of the app. Here’s the log output from windowDidBecomeKey(_:) after I selected a tab manually:

Root window <NSWindow: 0x600003e08500> Window #1 has tabs:
-  <NSWindow: 0x600003e08500> Window #1 isKey = false , isMain = true
-  <NSWindow: 0x600003e0eb00> Window #5 isKey = false , isMain = false
-  <NSWindow: 0x600003e0ea00> Window #4 isKey = true , isMain = false
-  <NSWindow: 0x600003e00b00> Window #3 isKey = false , isMain = false
-  <NSWindow: 0x600003e0e900> Window #2 isKey = false , isMain = false

The main window state will change, too, eventually, but only after the windowDidBecomeKey(_:) callback has exited. If you’re curious, add a key-value subscription and see for yourself:

NSApp!.observe(\.mainWindow, options: [.new, .old]) { [weak self] (app, change) in
    print("main window \(change.oldValue) -> \(change.newValue)")
}

This will show the change after windowDidBecomeKey(_:) exited.

When you Cmd-Tab in and out of the app again after a new tab was seleted, the windowDidBecomeKey(_:) callback is invoked again. Then, when tabbing back into the app, you’ll the that the selected tab is both main and key window. So don’t be fooled by the initial output like I was for quite some time.

Tabbed windows also have a list of all tabbed windows!

This surprised me, but is somewhat consistent with the active tab becoming key and main window (eventually).

When you add a tabbed window, all windows, including the root window, will have the same tabbedWindows content:

Root window <NSWindow: 0x600003e20000> Window #2 has tabs:
-  <NSWindow: 0x600003e08600> Window #1 isKey = false , isMain = true , and has tabs:
    -  <NSWindow: 0x600003e08600> Window #1 isKey = false , isMain = true
    -  <NSWindow: 0x600003e20000> Window #2 isKey = true , isMain = false
-  <NSWindow: 0x600003e20000> Window #2 isKey = true , isMain = false , and has tabs:
    -  <NSWindow: 0x600003e08600> Window #1 isKey = false , isMain = true
    -  <NSWindow: 0x600003e20000> Window #2 isKey = true , isMain = false

Because I sometimes get asked how I figure this stuff out, I put the following print statements into newWindowForTab(_:) after adding the tab:

func inspectWindowHierarchy() {
    let rootWindow = windowController.window!

    guard let tabbedWindows = rootWindow.tabbedWindows else {
        print("Root window", rootWindow, "does not have tabs!")
        return
    }

    print("Root window", rootWindow, "has tabs:")
    rootWindow.tabbedWindows?.forEach { window in
        print("- ", window, "isKey =", window.isKeyWindow, ", isMain =", window.isMainWindow, ", and has tabs:")
        window.tabbedWindows?.forEach { subWindow In
            print("    - ", subWindow, "isKey =", subWindow.isKeyWindow, ", isMain =", subWindow.isMainWindow)
        }
    }
}

The original window will not have a special status

When you make another tab active via makeKeyAndOrderFront, it becomes key and main window. That’s just how a non-tabbed window you activated and brought to front would behave.

But I found this confusing at first, especially before I found out all the rest above, and I still am not sure what this really means. Who is responsible for drawing the frame and tab bar?

Are the windows switched out from underneath each other to make it appear that the frame on screen stays the same? (This would explain the odd behavior of the frame resetting, see below.)

Originally I expected the root window to give up key status, but not main status. I expected it to remain “special” in some way, but it turns out I couldn’t find any specialty, as you will see.

Closing the root window will break the naive addition of tabs

Say you have 10 tabs opened and close the tab for the root window. Then tabs 1...9 will be visible. But once you try to add a new tab by calling rootWindow.addTabbedWindow(...) again, you’ll find out that a new window frame will open that includes the rootWindow’s tab and the new one. So closing the root window apparently affects how addTabbedWindow behaves.

What happens when you open new tabs and close the root window’s tab

Before jumping to conclusions, let’s have a look at what happens to the tabbesWindows arrays.

To inspect this, make the window controller the new windows’ delegate if you haven’t already:

newWindow.delegate = self

Note: Setting newWindow.windowController = self does not, in fact, change the delegate. You have to do this in an extra step, just like your Xib or Storyboard comes with a delegate wiring from the get-go.

Then I print the window hierarchy again once the key window changes, which happens automatically after I close the root window’s tab and another tab gets focused:

func windowDidBecomeKey(_ notification: Notification) {
    guard let window = notification.object as? NSWindow else { return }
    guard window != self.window else { return }
    inspectWindowHierarchy()
}

The output:

Root window <NSWindow: 0x600003e0c000> does not have tabs!

Bummer. So closing the root window effectively removes our access to all of the remaining tabs on screen. There still is a window frame drawn with the remaining tabs, but the window controller has no means to get there. Note that the root window’s tabbedWindows is nil, and its tabGroup.windows contains only a reference to itself.

Before you ask: I also had a look at the root window’s childWindows. The visible window isn’t listed there, either. The windows have no connection I could find between each other. As far as AppKit is concerned, the closed window is gone, detached from the remaining visible tabs, and their controller cannot get to them.

Switching tabs actually changes the visible window frame on screen

Next, start fresh and add 2 tabs to the application. I select the 3rd tab, stop execution in Xcode, open the View Debugger, and inspect the NSWindow that is part of the window controller scene. Its object address is not the address of the root window, it’s the address of the window corresponding to the current tab.

You may have noticed I said “visible window” or “window frame” a lot in this post. It’s because naming things gets confusing.

From a user’s perspective, switching tabs changes the visible contents of the window. It does not exchange the window, as far as a user perceives any change at all. The window, with its titlebar and its close, maximize, and minimize buttons, stays the same. But in the world of AppKit, that’s not the case. The window on screen is replaced with the window of the selected tab, and the tab bar just shows a different active selection.

The thing visible on screen I call the “window frame”, to reference the drawn artifact. It seemingly stays constant. But “the window”, as in NSWindow, is different under the hood.

How can you figure out which window is the one used to draw the frame onto your screen?

  • Find the active tab. As long as the rootWindow is visible, you can use rootWindow.tabbedWindows.first(where: { $0.isKeyWindow }) to get the key window, i.e. the active tab.
  • Find the active app’s window. Once the rootWindow is closed, and its tabbedWindows reference is nilled-out, your only chance is to use the application’s global window list: NSApp.windows.first(where: { $0.isKeyWindow }). (In applications with e.g. a preferences window or when you show the About panel, you’ll need to use a different filter predicate to exclude all the windows that are not the tab-based ones.)

Note than when you open the standard About panel, NSApp.windows.first(where: { $0.isKeyWindow }) will produce the About panel because it has key window status. But the About panel will not be the main window; it’s an auxiliary window only. Use NSApp.windows.first(where: { $0.isMainWindow }) to get to the window with the tabs in it.

Fix the window controller’s notion of “its window” to always know the active tab

I do not like that the root window is still referenced by the window controller although it’s been closed.

The notion of a “root window” was introduced by myself to distinguish between “the window visible from the start”, and “window for new tabs”. This made me believe the root window retained a special status, but nothing indicates this is the case.

It doesn’t make sense to cling to it. What I thought to be the root window really is nothing special at all. Its similar to all other windows in the tab group:

  • all windows contain a list of tabbedWindows, including the one we started with, and
  • closing any window removes it from tabbedWindows and also nils out its own list of tabs.

If the root window does not have any special status as far as AppKit is concerned, then the window controller shouldn’t treat it in a special way and stop holding on to it once it’s closed.

Let’s reflect this matter by updating the strong NSWindowController.window reference to point to the currently active tab.

Make sure the window controller still is the NSWindowDelegate of all windows for this to work:

class WindowController: NSWindowController, NSWindowDelegate {
    // ...

    func windowDidBecomeKey(_ notification: Notification) {
        guard let window = notification.object as? NSWindow else { return }
        // Replace the reference to the main key window
        guard window != self.window else { return }
        self.window = window
    }
}

Now you can close the original window’s tab and add tabs to the root window of your window controller. It will add tabs next to the currently active tab in all cases, and another frame with the previously closed window will not pop up again!

If you close most tabs that had key status, the window frame will reset when you add a new one

This is hard to pin down accurately, but it’s related to some status that tabs have when you activate them most of the time, but not always. One way to reproduce, moving the window frame before you add a new tab to see if the window frame moves at all:

  • Close cither the first root window’s tab, or
  • whichever tab gets key and main window status after the root tab closes, or
  • whichever tab gets key and main window status after the previous initial tab closes.

There’s a quirk if you call makeKeyAndOrderFront(nil) when showing a new tab. If you do, closing the tab will often result in the window frame snapping back; if you don’t, you can get away with closing tabs just fine.

Adding a new tab after closing the last important one results in the window snapping back to center.

To isolate the factors, I deactivated making the new tab key. Then have a look at the windows’s frame values after creating 2 tabs:

Root window <NSWindow: 0x600003e00800> Window #1 has tabs:
-  <NSWindow: 0x600003e00800> Window #1 isKey = true , isMain = true  at  (420.0, 343.0, 840.0, 513.0)
-  <NSWindow: 0x600003e02100> Window #3 isKey = false , isMain = false  at  (196.0, 240.0, 480.0, 292.0)
-  <NSWindow: 0x600003e0a300> Window #2 isKey = false , isMain = false  at  (196.0, 240.0, 480.0, 292.0)
  • Only the original window is visually centered on screen, which is the post-app-launch default,
  • only the original window has a fitting height.

(I experimented with different window sizes and positions. The new tab’s windows always start with (196.0, 240.0, 480.0, 292.0) before they become visible for the first time.)

What happens when you switch tabs?

Root window <NSWindow: 0x600003e00800> Window #1 has tabs:
-  <NSWindow: 0x600003e00800> Window #1 isKey = false , isMain = true  at  (420.0, 343.0, 840.0, 513.0)
-  <NSWindow: 0x600003e02100> Window #3 isKey = true , isMain = false  at  (420.0, 343.0, 840.0, 513.0)
-  <NSWindow: 0x600003e0a300> Window #2 isKey = false , isMain = false  at  (196.0, 240.0, 480.0, 292.0)

Okay, so when a window’s tab activates, its frame is adjusted. Move around, switch again, move again, switch again, and you end up with this:

Root window <NSWindow: 0x600003e05600> Window #2 has tabs:
-  <NSWindow: 0x600003e04400> Window #1 isKey = false , isMain = true  at  (718.0, 294.0, 840.0, 638.0)
-  <NSWindow: 0x600003e01b00> Window #3 isKey = false , isMain = false  at  (-27.0, 225.0, 840.0, 638.0)
-  <NSWindow: 0x600003e05600> Window #2 isKey = true , isMain = false  at  (718.0, 294.0, 840.0, 638.0)

This means that after switching from #1 to #2, #2 did update its frame to the current position of the original main window on screen. Let’s switch back to #1 and close the tab so #3 becomes active. Does it snap back now? No, the window frame on screen stays puts.

Interestingly, the frame origin and size after snapping back do not correspond to previous values. The window frame resizes when it snaps back for the first time, getting a bit taller. Note that in my demo app, the windows are empty and the contentView does not produce any meaningful intrinsicContentSize. The problem could look a bit different if you have a content view and/or an autosave name for the window frame.

Whatever the factors, a quick fix for this is to force-restore the frame position:

func windowDidBecomeKey(_ notification: Notification) {
    guard let window = notification.object as? NSWindow else { return }
    guard window != self.window else { return }

    // When changing whichever window is owned by the controller,
    // restore the previous key window's frame to prevent snapping
    // back to the center.
    let oldFrame = self.window?.frame
    self.window = window
    if let oldFrame = oldFrame {
        window.setFrame(oldFrame, display: true)
    }
}

Conclusion

As I said in the introduction, I do not recommend building on top of this. The behavior I witnessed is too weird already. I don’t know into which kinds of trouble I’ll run if I used this in a complex application like The Archive, where the user interface needs to stay responsive. I don’t want to mess up window autosaving information, including the widths of split view parts. Since the window frame snaps back until you force it not to, I suspect that something like this might happen all too easily.

T’was a fun experiment, though. I learned a lot about tab groups and the real meaning of key and main window status, and that tabbing just replaces the on-screen NSWindow instance. It’s always a bit enlightening to see how my perception as a user differs from the actual implementation. In this case, AppKit replaces the on-screen window completely and does not simply switch out content views.

I hope that having published this post will help humankind in the long run. My naive implementation of tabbing started with this approach, because NSWindowController is such a convenient place to respond to newWindowForTab(_:)! Don’t be like me, be smart and implement a better window controller management in your AppDelegate, if that works for you, or in a custom service object which we’ll explore in the next post.

How I Never Forget to Pack Stuff in My Backpack

I almost never forget to pack stuff for my regular trips to the gym, family visits, or shopping. Of course I sometimes do forget to pack something for a weekend trip, or I overpack books when I visit friends over the weekend and am afraid to run out of reading material during the train ride.

But for the daily leaves of my appartment, I do not forget to pack keys, phone, money, writing utensils, sketchbook, etc.

This wasn’t always the case. I attribute the immense improvement in reliability to my 2017 christmas read of Mari Kondo’s The Life-Changing Magic of Tidying Up.1 At first, I thought it was funny to give my backpack time to rest and relax. That’s how she frames emptying the bag completely. I still do think it’s funny, but the practice stuck. I tried a couple of her tips, no matter how they were explained or tied to a value system probably related to Shinto beliefs. And lo and behold, emptying the backpack completely helped to make sure I have everything packed when I leave.

I think this is because I would be so accustomed to having all necessities ready to go at any given point in time. If I ever took a piece out of the backpack and forgot to immediately re-insert it, I’d forget about it missing completely. Relying on an always-ready backpack made me more vulnerable to forgetting to pack e.g. my purse. It happened all the time, really. I almost never forgot my keys, though, because I always leave them next to the door for picking up whenever I leave. Packing my every-day carry backpack every time before I leave works in the same way.

Because my backpack is empty by default, I have to run through my mental checklist of things to pack just before I put on my shoes and leave. This actually makes me more resilient in practice: I have to fetch all the items I need everytime and I do notice when they are misplaced, actually making it less likely to misplace them in the future.

Here is another tip taken from Mari Kondo’s book: Optimize your storage for putting away, not for retrieval (see page 142 of the book). Chaos emerges from too high a threshold to put a thing where it belongs. Storing away should be the easiest part. When you need something, you will fetch it no matter the cost anyway. – But I do mind the cost of picking up, so I trained myself to put things into similar spots more reliably over time. I execute almost the exact same movements everytime I pack my bag now, it seems.

So as counter-intuitive as it may seem, emptying my backpack every time I get home helps me not to forget to bring anything when I leave.

In case you wonder: My beloved backpack is a Maxpedition Falcon III1, by the way. It’s a tactical backpack, and I love the form factor. I got the backpack last year as a thank-you from a good friend for being his best man when he wed his bride. It’s one of the very best gifts I ever received. I enjoy packing and unpacking it, it’s crazy. And I really dig that I can attach pouches using the Molle webbing. It’s a cool toy for grown-ups.


  1. This is an affiliate link. I get a small kickback from any purchase from amazon.com without any additional cost to you. 

Convert MS Word DOCX Files to Markdown with Images

I don’t know when was the last time I received a Microsoft Word .docx file. However long the streak may have been: it has been broken today.

The document contained links and embedded images. I was instantly taken aback by the prospect of all the manual labor of extracting the images and saving them to files, not even knowing how MS Word behaves nowadays.

But I have pandoc installed.

And it’s great. It even extracts images and saves them into a subfolder. I love it. Didn’t know about this feature until I scrolled through the output of pandoc -h.

This is the command I used:

$ pandoc -o output.md --extract-media=./ inputfile.docx

It puts all images into a media/ subfolder anyway, so I set extract-media to the current directory. Just lovely.


→ Blog Archive