Lufsa: Sticky Note Images
(Approx. 8 min read)
After previous projects with SwiftUI, I became curious as to what the limits of the framework are, and how different the development experience is between macOS and iOS apps. To push those limits, I aimed to recreate a somewhat weirder application: xteddy.
Why xteddy?
xteddy is a X11 application written by Professor Stefan Gustavson which originally did exactly one thing: draw a non-rectangular, floating window to the screen containing just the bundled xteddy image, and implement some basic handlers to kill and open it appropriately. However, later versions released in 1997 onwards allow you to load any arbitrary .xbm (X Bitmap), and in the Debian Linux port, .png files. This effectively made it a simple sticky note application but for images.

When I switched from using a Thinkpad running Linux to a Macbook, I missed this app quite a bit as I had some useful scripts that took advantage of xteddy’s functionality. Unix’s X11 and macOS’s Quartz windowing system have very different designs and no backwards compatibility for X11 libraries, so a simple recompile wouldn’t do.
The pain point that convinced me to do the rewrite was when I kept wanting to have floating windows with certain reference images (band information for remote sensing data), but putting them into the Sticky Notes app that comes with macOS was not really working for me (hard to use). So I opted to give a rewrite a go!
Fighting with SwiftUI
I initially tried to see if something like this was even possible within SwiftUI. It’s been in development for a long time, presumably it should be nearly feature-complete for this sort of thing at this point? I thought it would be as simple as just writing:
1struct LufsaApp: App {
2 var body: some Scene {
3 WindowGroup(for: ImageDocument.self) { $doc in
4 if let doc {
5 ImageStickyView(document: doc)
6 }
7 }
8 .windowLevel(.floating)
9 .windowBackgroundDragBehavior(.enabled)
10 .commandsRemoved()
11 .commands {
12 CommandMenu("Images") {
13 Button("Open Image...", systemImage: "photo") {
14 manager.openImage(using: openWindow)
15 }.keyboardShortcut(KeyboardShortcut("o"))
16 }
17 }
18 }
19}
This wasn’t sufficient. The main issue was that focus on the windows themselves were deallocating randomly after creation. Ominous, and definitely not foreshadowing of greater technical problems to come with this approach. This required a bit more work:
1@MainActor
2class WindowManager: Observable {
3 var documents: [ImageDocument] = []
4 // methods went here
5}
Again, some newfound problems with windows. Changing state on them required developing a complex set of methods to be implemented on the actual View directly.
1.onChange(of: document.properties.isBorderless) { _, isBorderless in
2 if isBorderless {
3 window?.styleMask = [.borderless, .fullSizeContentView]
4 window?.titlebarAppearsTransparent = true
5 window?.titleVisibility = .hidden
6 window?.backgroundColor = .clear
7 window?.isOpaque = false
8 } else {
9 window?.styleMask = [.titled, .closable, .miniaturizable, .resizable]
10 window?.titlebarAppearsTransparent = false
11 window?.titleVisibility = .visible
12 window?.backgroundColor = .windowBackgroundColor
13 window?.isOpaque = true
14 }
15}
Since SwiftUI still doesn’t allow us to select what window we’re working on in a way that works reliably (.appearsActive exists, but fails on some style masks), we have to handle that:
1struct WindowAccessor: NSViewRepresentable {
2 @Binding var window: NSWindow?
3 func makeNSView(context: Context) -> NSView {
4 let view = NSView()
5 DispatchQueue.main.async {
6 self.window = view.window
7 }
8 return view
9 }
10 func updateNSView(_ nsView: NSView, context: Context) {}
11}
It was around this point that I started to give up on any reasonable hope of getting this done quickly, and realized I’d need to rethink my approach, as I’d been forced to reinvent:
- Window management and window memory allocation
- Window tracking and state management per-window (since SwiftUI doesn’t really know what a “window” is; it thinks in terms of pure
ViewsandScenes) - The
Imageview itself (I.background()ed it with anNSViewRepresentableto change click behavior because SwiftUI is picky about what clicks are “gestures” and thus qualify for.onTapGesture()
Even with all these hacks, behavior was still breaking because the SwiftUI runtime kept changing behavior like the View overlays and the very NSWindow properties I was trying to write to, and I simply couldn’t beat SwiftUI at its own game!
Clearly, I needed a framework that made it quick and easy to operate on what my entire project was actually about, which is raw pixels and direct window draws. After consulting the documentation once again, I found it in the code I’d already been repeatedly subclassing aggressively anyways: AppKit.
AppKit is Still Great (On macOS)
It’s clear that for any macOS app that needs very close system ties or nonstandard behavior that’s allowed within macOS, you have no choice but to drop to AppKit. Fortunately, it’s mostly great in Swift as long as you are aware of with the footguns of this entire API, which is that you’re actually secretly in imperative-ish Objective-C the whole time and nobody’s telling you because the FFI is great here, so be very careful with some things you’d normally do without caution in SwiftUI. For instance:
1@objc func openFile(_ url: URL) {
2 let image = NSImage(byReferencing: url)
3 let canvas = ImageCanvas(image: image)
4 let delegate = AspectScaleDelegate(baseSize: image.size)
5
6 let window = LufsaWindow(
7 contentRect: NSRect(x: 0, y: 0, width: Int(image.size.width) as Int, height: Int(image.size.height)),
8 styleMask: [.titled, .closable, .resizable, .miniaturizable],
9 backing: .buffered,
10 defer: false
11 )
12
13 // time passes...
14 window.delegate = delegate
15 // uh oh, my delegate just got deallocated
16 self.windows.append(window)
17}
What is wrong with this code? It surely stumped me for some time. The reason this will fail is that the delegate is a weak reference. Normally, Swift would handle copying memory around and juggling the reference with automatic reference counting, so even though the NSWindow will escape this scope it won’t deallocate objects it’s referencing. However, NSWindow is actually an Objective-C object, so its runtime will deallocate the delegate inside of the NSWindow during garbage collection. To inform it that you really do want the delegate to stay:
1// this is Dark Arts to create a strong reference.
2// *ptr to heap of main actor, which holds it.
3objc_setAssociatedObject(window, &Self.delegateKey, delegate, .OBJC_ASSOCIATION_RETAIN)
4// and our delegate doesn't deallocate!
This is really good for overriding class behavior, but it’s definitely confusing at first, not very Swift-like to read, and similar twists came up repeatedly as I worked with AppKit. Despite being in Swift, I ended up thinking and reading more about Objective-C’s behavior more than I’d ever have expected to in 2026.
Another annoying side effect of this choice is the strange mysterious behavior that remains undocumented in the macOS windowing system:
1if window.styleMask.contains(.titled) {
2 savedStyleMasks[window] = window.styleMask
3 // technically, we could allow resizes here,
4 // but they're buggy so I'm not going to allow it.
5 window.styleMask = .borderless
6 window.backgroundColor = .clear
7 // borderless windows don't get focus by default
8 window.makeKeyAndOrderFront(nil)
9}
Why exactly macOS is so weirdly restrictive about resizing borderless windows is unclear. It also crashes Finder to spawn non-rectangular windows, which is a genuine regression in Quartz. There’s a good chance I’m probably doing it wrong, or that a framework that wraps Quartz like SDL figured out how to do it, but if it is possible it’s definitely not easy to do.
Finished Product
I decided ultimately to name this thing Lufsa after the name of the bear in the original program. Just like the classic one, Lufsa supports a debordered mode, where window chrome isn’t rendered. Due to limitations in macOS this is secretly rectangular, but it’s an invisible rectangle cropped to content, so it should be very close to non-rectangular in practice. A slight regression over the 30 years that have passed, but otherwise modern windowing APIs are just as capable as X11 was without X11’s massive architectural problems.

Windows also scale relative to their aspect ratio, so even the above 100x100px catJump.gif looks great when it’s 20 times its size. Lufsa also supports far more filetypes (anything Previews supports since it’s the same API) and even animated image files too!
It’s gone a bit beyond the goals of perfectly emulating xteddy, but I bundled all the Debian Linux port images with the application bundle. Under File -> Open, you can find and open any of them if you’d like. Make sure to turn on Pixel-Perfect for the canonical ’90s experience.
For my purposes the project is basically complete. If you want to give it a go yourself, you can try it by clicking here and then downloading the latest release. Because of AppKit, it’s super backwards compatible (any macOS install since High Sierra should be compatible, so it’ll run fine on any Mac from the last decade).
If you happen to be a new Swift developer1: on macOS, AppKit is still fantastic and shouldn’t be shunned just because it’s not SwiftUI (and thus, not shiny & new). If anything, when your application is more “windows and pixels” rather than “views and layouts” consider reaching for it first until Apple finally finds a way to SwiftUI-ify window management in a clean, cross-platform way. One can hope that with visionOS and iPadOS encouraging more window-based inputs, we might see that day come sooner rather than later…
You might have also been sent here because I linked to this blog post as part of the “Free University” student Winter Study course on macOS/iOS development at Williams. If that is you: welcome, and hopefully this article cleared up what I was talking about! ↩︎