The ones that cost dearly. Almost every one has a symptom that points somewhere other than the cause.
Xojo wipes an inherited button's image, forever
Measured on 17 September 2026 on a DesktopButton placed in a window. setImage: is received — right after the call, [button image] returns the image and imagePosition the value set. On the NEXT click, before touching anything: image gone, imagePosition back to 0. Nothing had touched it in between.
Neither the cell nor the bezel is to blame: a plain NSButtonCell, a Push bezel, both of which draw an icon perfectly outside Xojo. XOJButton overrides no drawing method — only events. But the framework itself calls setImage:, setImagePosition: and setButtonType:, the last of which resets image and bezel, and it does so AFTER Opening and on every pass.
What does not work: Timer.CallLater(0) to set the image later, setNeedsDisplay: to force the drawing. There is no hook before an inherited control is drawn.
What works: HOSTING rather than inheriting — NativeIconButtonControl. The button is then created by the library, which Xojo does not know and never reconfigures.
An icon above the title falls outside the frame, with the Push bezel
Found by rendering, on macOS 27. With ImagePosition = Above (or Below) and the Push bezel — the ordinary button's — AppKit draws the icon inside the frame and the title OUTSIDE, below the pill. Tried at 32, 40, 46, 52 and 60 points tall: the bezel keeps its own fixed height and the text stays outside, at every height.
The cause is not missing room: the Push bezel has a height of its own, which the control's frame does not command. fittingSize gives no warning — it returns 24 points tall for Above just as for a button with no image.
What works: a bezel that agrees to grow — FlexiblePush (2) or SmallSquare (10), both verified. Icon and title then fit inside the frame, stacked. With Leading, Trailing, Left or Right, the Push bezel is perfectly fine.
The dragged row vanishes: AppKit's image is not enough
Found frame by frame on macOS 27, in two recordings. The image: for a view-based table, the header says AppKit builds it from NSTableCellView's draggingImageComponents — its text field and image. Plain NSView cells provide almost nothing: a white title, then nothing at all. The selection: in Gap style, the grabbed row is hidden for the whole drag; what is being moved is visible nowhere. And over the middle of a row, AppKit proposes DropOn; refusing it leaves no destination.
What works, and mirrors Numbers: Regular style, so the original row stays in place; photograph the rows as early as pasteboardWriterForRow: with cacheDisplayInRect: on the table; place a ghost view in the table — those photos stacked in an opaque rounded card with an accent-colour border, the drop shadow carried by an outer view that clips nothing (a plain translucent copy blended into macOS 27's white background) — that validateDrop: moves on every motion from draggingLocation, and that draggingSession:endedAtPoint: removes; empty AppKit's image with setDraggingFrame:contents:; retarget DropOn to DropAbove with setDropRow:dropOperation:.
On macOS 27, a white field on a white window
The field has not lost its border: it has lost its contrast. Measured on a 27.0 system in light appearance, windowBackgroundColor is now 255/255/255 — exactly the white of textBackgroundColor, the field's interior. Only a line of about 12 % grey remains, which the eye no longer sees on a scaled-down screenshot. Square and Rounded are then drawn identically, and every field is affected: DesktopTextField as much as NativeTextField, it is the same NSTextField.
Two intuitive remedies fail, measured too: setting a backgroundColor on a bezeled field is ignored, and dropping the bezel for a plain line gives a hard stroke, bright white in dark mode. What works is the System Settings layout: put the fields in a group box, whose grey makes the white stand out — and an NSBox is drawn on macOS 15 too, with no version test. Mind the empty title: it still reserves 12 points at the top of the box. Increase contrast, in the accessibility settings, strengthens borders; that is the user's choice, not the application's.
On macOS 27, menu icons disappear without an error
The code has not changed, the symbol is set through setImage:, and the menu opens without its icon. NSMenuItem's header says it plainly: from macOS 27 on, AppKit “determines the visibility of menu item images, and will typically hide images”. The new preferredImageVisibility property is Automatic on creation — measured on a 27.0 system.
The remedy is one setting, Visible, behind a selector test. NativeMenu sets it by default: a symbol passed as an argument is an explicit request, and handing it back to the system is still possible with ImageVisibility = Automatic. The pitfall only concerns items with an image: a palette menu's swatches have none.
Testing for a CLASS method: two idioms, two different arguments
respondsToSelector: is a MESSAGE sent to an object: it searches the class of that object. For a class method you therefore send it to the class itself. Send it to the metaclass and it searches the meta-metaclass, and never finds anything.
The runtime function class_respondsToSelector(cls, sel) is the other idiom: it looks at the instance methods of the class handed to it, so for a class method you must pass the metaclass. Both are correct; mixing them is not.
The symptom is silent and misleading: a feature reports itself unavailable on a machine that supports it, with no error anywhere. Observed on the macOS 15 cursors, reported absent on a 15.7.9 system.
Never derive one pane's geometry from another's
Under NSSplitViewController, two full-height panes have the same height — in steady state. AppKit does not resize them at the same instant, and a full-screen toggle is enough to show it: laying the inspector's content out from the height reported by the detail pane's resize event gives a wrong frame.
The symptom misleads twice over. Too tall on the way in: since AppKit's origin is at the BOTTOM, the content leaves through the top of the pane and the panel looks empty — which reads as a drawing bug, not as a frame that is too large. Too short on the way back: the content ends up stuck at the bottom. And nothing shows in steady state, only during the transition.
Each pane must be read from its own bounds. The value received in the event serves only as a fallback for the very first pass, before the controller has laid the pane out — at construction, its bounds are still those of its factory.
The customisation palette goes through the same delegate
toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar: serves two purposes: populating the bar, and building the customisation sheet. Returning the same NSToolbarItem in both cases confuses the item displayed in the sheet with the one in the bar: dragging it to the bar does nothing, without the slightest error. The header states it — each toolbar receives its own distinct copies and each time this method is called a new instance, and the palette is built through that same method.
Since NSToolbarItem is NSCopying, return an autoreleased copy when willBeInserted is NO, and the real instance when it is YES: live property changes on an item already placed then carry on working.
Two orderings decide the rest. autosavesConfiguration must be YES BEFORE setToolbar: — that is the moment AppKit reads the saved configuration back; set afterwards, it only writes, and the bar returns to the default set at every open. And displayMode and the style must be set before attaching, as starting values: set afterwards, they overwrite the mode the user has just chosen in the sheet every time.
The autosave key is NSToolbar Configuration <identifier> in the application's own preferences — read from a real one, not guessed. No API resets it: it has to be deleted and the default identifiers re-inserted.
NSOutlineView compares the pointers you hand it
The data source returns one “item” per node, and NSOutlineView keeps those objects and compares them to know what is expanded and what is selected. Returning a new object on every call — even an equivalent one — loses the expanded state and the selection on every refresh.
So the keys must be stable and retained: one NSString per node, tied to its number — never to its row, which changes as soon as something is inserted or moved —, cached in a Dictionary, with the reverse table to find the node again from the pointer. A key stays retained until the tree is cleared, even for a removed node: AppKit may still hold it during the removal animation. And setOutlineTableColumn: designates the column that carries the triangles: without it, AppKit puts them where it likes and the indentation does not follow the text.
A node identifier must not be a row number
Returning the row index as identifier is tempting: as long as you only append, nothing moves and everything works. But removing or inserting in the middle shifts every row above — the identifiers the caller kept, the parent table, and the keys handed to NSOutlineView. Expansion and selection then jump from one node to another, a child hangs off the wrong parent, and nothing raises.
The cure is a type of its own, not a more stable integer: a never-reused number kept as an Integer would have compiled everywhere and matched the row until the first removal. NativeOutlineNode makes the compiler refuse every place that confused the two. And since an index AppKit does not know raises an Objective-C exception, animation is only attempted on a parent that is shown and expanded; elsewhere, the tree is reloaded.
A folder is not necessarily a folder
FolderItem.IsFolder returns True for an application, an .mpkg, a Photos library, an Xcode project, an RTFD document. macOS calls that a package and it alone knows: you have to ask NSURLIsPackageKey, NSURLIsApplicationKey and NSURLContentTypeKey. A modern .pkg, for its part, is not a folder at all — it is a flat file.
Two pitfalls in the detail. The order of the questions: an alias to an application is an alias first, an application is a package before it is a folder. And you must resolve the symbolic links — /Applications/Safari.app is one, and would otherwise pass for a plain alias. Finally, read the keys rather than copying them out: NSURLIsApplicationKey is in fact _NSURLIsApplicationKey, with a leading underscore that nothing announces.
RealityKit is out of reach from Xojo
RealityKit has replaced SceneKit since macOS 26, but it exposes no view class to Objective-C: neither ARView, nor RealityView, nor Entity, nor ModelEntity. Verified at runtime — the framework's only ObjC classes are Swift internals with mangled names (_TtC10RealityKit…). Its public headers cover nothing but Metal shaders.
Since Xojo speaks only Objective-C, RealityKit cannot be reached. SceneKit therefore remains the only workable route to 3D in Xojo, deprecated though it is — and that will have to be known the day Apple withdraws it: the fallback will be the QuickLook thumbnail, static.
The size asked of QLThumbnailImageCreate is a BOX, and 3D wants 4:3
The size parameter is not a scaling instruction but a box the thumbnail has to fit into. The 3D generator returns 4:3: any box narrower than that ratio makes it fail without saying anything. Measured on an STL — 840 × 340 returns Nil, 400 × 267 too, whereas 512 × 512 returns 512 × 384 and 400 × 300 returns 400 × 300.
So we ask for a square, which contains any natural ratio, and let the view scale it. Beyond 1024 the generator caps itself. The trap is a nasty one because the failure is silent and looks like “this format is not handled”: you fall back to the icon believing QuickLook cannot manage it.
Closing a QLPreviewView twice ends the process in silence
close is final — the view accepts nothing afterwards — but nothing stops you calling it a second time, and that makes the application vanish with no message and no trace. The case arrives quickly as soon as two paths close: one on the page change, the other before rebuilding.
So an internal flag is needed, and it must be checked in close as well as by the caller. Available() must return False after closing, failing which a careful caller will believe itself allowed to close again.
LIVE preview or THUMBNAIL: 3D makes the choice compulsory
A QLPreviewView installs an NSRemoteView served by SceneKitQLPreviewExtension — another process, which moreover outlives its client and accumulates. On a 3D document, that destroys the process's drag and drop for good: nothing can be dragged into the application any more, and the Finder even refuses to lift a file. Neither close, nor destroying the view, nor changing page does anything about it — only quitting the application. Images, PDFs, text and audio are unharmed.
The thumbnail is the way out: QLThumbnailImageCreate goes through SceneKitQLThumbnailExtension, the one the Finder uses constantly. It returns an image — no out-of-process view in the window, no process left behind — with the full 3D rendering. Deprecated since 10.15 but still present and synchronous (43 ms on an STL), where the modern API demands a block and a callback off the main thread. It returns Nil on folders, applications and unknown formats: you then fall back to the Finder icon, and the breakage of the day it disappears is already provided for.
A class outside AppKit does not exist until you load its framework
QLPreviewView belongs to QuickLookUI, under the Quartz umbrella — which Xojo never links. Verified: a binary that does not link Quartz does not find the class, objc_getClass returns Nil. So an explicit dlopen is needed before any ClassRef.
And on the full path: dlopen("Quartz") fails, as does dlopen("AppKit") for that matter — the short names Xojo accepts in a Lib are not the ones dlopen resolves. A pleasant surprise, on the other hand: NSURL already conforms to the QLPreviewItem protocol, there is no runtime class to build.
A Xojo keyword as an enumeration member, and the error blames the uses
Soft is a keyword — the one from Soft Declare — and Xojo ignores case: Soft=1 in a #tag Enum is a “Syntax error”. The symptom is misleading to the point of being disabling: Xojo reports every line that writes MyEnum.Soft and declares the enumeration block sound, as long as one use remains to blame.
The error only lands on the declaration once every reference has been removed. The rule to draw from it: when a construct rigorously identical to code that compiles is refused, it is an identifier — and you must go and read the declaration it names, not the line the compiler points at.
addRow: raises if the root row is not compound
Setting a simple predicate on an NSPredicateEditor reduces it to a single row, with no compound root. The next addRow: does not stay inert: it raises an NSRangeException in -[NSRuleEditor _insertNewRowAtIndex:ofType:withParentRow:] — and an Objective-C exception ends the Xojo process.
The remedy costs nothing: wrap the predicate in an AND with a single sub-condition. Measured — the string returned by predicateFormat is identical, but the editor gets its compound row back. rowTypeForRow(0) lets you detect the dangerous state before acting; an empty editor, for its part, accepts addRow: without trouble.
An NSTableHeaderView with a zero frame is installed, and invisible
Hiding a table header means setHeaderView:nil — the NSScrollView then folds away the area it was reserving for it. Bringing it back by building a fresh header works too, provided you give it a height: with a zero frame it really is installed, the scroll view restores its area… at a height of zero.
So nothing is visible, with not the slightest error to say so. Better to keep the original header — retained, since setHeaderView:nil releases it — than to guess a height that depends on the system and the table style.
The menu checkmark image is SHARED
paletteMenuWithColors:titles:selectionHandler: leaves the swatches with the ordinary state image: NSMenuCheckmark, 18 × 17 points. Drawn over a swatch of about 13 points, it overflows and ends up clipped — you see a white arc instead of a checkmark.
And resizing it in place is out of the question: two distinct palettes return the same object, and so does an ordinary menu item. A fresh checkmark has to be built per item — an SF symbol at the wanted size, in template mode so that it takes its colour from the background.
AppKit has no vertical alignment - you place the frame
An NSTextField centres its text in the frame it is given, and that is all: no property says “at the top” or “at the bottom”. The vertical alignment of a cell is therefore obtained by placing the frame of the control inside the cell, not by setting anything.
Two consequences. Since the origin of a view that is not flipped is at the bottom left, “at the top” means the largest y — the opposite of intuition. And the autoresizing mask must be limited to the width: letting the height follow would undo the placement just calculated.
A style is not a control: where to put the macOS 26 edge effect
NSScrollEdgeEffectStyle has neither frame nor view: it is a style object, and the SDK accepts it on two properties only — preferredScrollEdgeEffectStyle of NSTitlebarAccessoryViewController and of NSSplitViewItemAccessoryViewController. Making it a class of the library would have been padding.
So it lives on NativeWindowChrome, which owns the window. SetScrollEdgeEffect returns the number of accessories actually styled: zero is not an error but the observation that there is none to style — the window has a title bar accessory only when the RTF format bar is open.
The table's double-click and a cell's editing aim at the same gesture
The double-click action of an NSTableView and the start of editing of an NSTextField housed in a cell both answer the second click. On an editable column they fight over the mouse, and nothing in the API says so.
DoubleClickAction = False disarms the action — a nil SEL passed to setDoubleAction:, and AppKit sends nothing more. Same logic for the look: BezeledEditableCells chooses between a field that only reveals itself on typing (the Finder rename) and a field that announces itself as editable straight away. Neither is objectively right, hence the option rather than an imposed choice.
The tag of an NSControl, to find the cell that acts
When a checkbox or a menu housed in a table fires its action, the callback receives nothing but the control — no row, no column. The tag, a free NSInteger on every NSControl, carries the flat index of the cell there, the same as the storage: row x columns + column.
You get back to both through an integer division and a modulo. An encoding like row x 1000 + column would work too, but would impose an arbitrary limit; reusing the storage index imposes none and leaves only one piece of arithmetic to check.
An editable field warns you by TWO paths
The action of an NSTextField fires on Return; controlTextDidEndEditing: fires on the loss of focus. Both are needed — one very often clicks elsewhere without validating — but they then both arrive for a single change.
The answer is not to choose: it is to do nothing when the value has not really changed. The guard makes both paths idempotent and the event fires only once.
An Objective-C exception cannot be caught from Xojo
predicateWithFormat: raises an NSInvalidArgumentException on a malformed string — verified. A Xojo Try...Catch does not intercept it: the exception passes straight through and ends the process. This is not an error you handle, it is a crash.
Consequence for the shape of the API: SetPredicateFormat must receive only strings produced by PredicateFormat, never user input. The method says so in its own comment rather than leaving it to be discovered.
An NSSet has no order - “the first item” does not exist
collectionView:didSelectItemsAtIndexPaths: returns an NSSet, not an array. Taking “the first” index from it makes no sense: anyObject is the only honest access when you want just one.
The same caution applies to selectionIndexPaths. As soon as multiple selection is allowed, exposing a single “selected index” is a simplification that has to be owned out loud.
An NSCollectionViewItem does not need a nib
Verified in Objective-C before relying on it: setView: on a fresh NSCollectionViewItem works, and loadView is never called. It is the trick NativeWindowChrome already uses for its NSViewController, and it avoids the whole detour through an interface file.
A corollary not to forget: an item whose view is set by hand draws no selection. You have to paint it yourself from didSelect / didDeselect, writing the colour at the moment of the click so that it is resolved in the current appearance.
The IDE silently restores an anchoring the chrome forbids
Under NSSplitViewController, the rule is: the size through the Xojo API — the only gesture that lays out the children of a DesktopPagePanel again —, the position through AppKit. For the second to hold, the top-level views of the detail must have LockRight and LockBottom at False. With LockTop and LockBottom at True, Xojo takes the view to be stretched from the top to the bottom of the window and puts it back at Top = 0 — under the toolbar — after the AppKit placement.
The trap is that no line of code changes: stretching a control to the bottom edge of the container in the designer is enough for the IDE to lock the anchoring, without a word. The symptom is unmistakable — the page title overlaps the window title, the toolbar labels are crossed by the content. It can be checked in the .xojo_window, on the header of the top-level block, before any nested Begin. Putting LockBottom back to False does not lose the height: the DetailResized event recalculates it on every pass.
Xojo is case-insensitive: an identifier that looks like a keyword is the keyword
isa is the IsA operator. sub, iF, dO, tO likewise. And it is not restricted to keywords: a parameter named color masks the Color type, so that Color.RGB(0,0,0) becomes a member lookup on that parameter.
A syntax error on the FIRST statement of a method stops the parsing of the whole class: its members then become “non-existent” as seen from other classes, and its constructor is reported with a wrong number of arguments. Five errors for a single bad name.
The retain is the whole subject
NSToolbar.delegate, NSToolbarItem.target, NSControl.target, NSTableView.dataSource, NSPopover.delegate: all of them weak, zeroing references. ObjCClass.CreateInstance therefore returns Retain(init(alloc(cls))).
Without that retain the instance dies at once, the weak reference empties, and no callback ever fires. The symptom reads as “the runtime refuses weak references to a dynamically created class”. It does not refuse them; the object had simply been deallocated.
The size through the Xojo API, the position through AppKit
Writing DetailPanel.Width = w through the Xojo property is the only gesture that lays out the children of the DesktopPagePanel again. Then correct the position with PlaceView, in AppKit coordinates. Never read the Xojo geometry under a split view controller — it stays out of date.
And remove LockRight / LockBottom from the top-level views: that lock is what tells Xojo “this control is mine, I resize it”. Otherwise Xojo lays out as well, against the width of the WINDOW, and you see two layout passes.
reloadData deselects
Four ways to refresh a list, and taking the wrong one manufactures bugs. Calling it from the selection-changed notification empties the selection just before the event reads the index — the symptom reads as “the click does nothing”.
And a plain setNeedsDisplay: does NOT make an overridden getter such as isEmphasized be read again: only a real reload does, hence ReloadPreservingSelection.
NSToolbarItemGroup: the order of the wiring is everything
AppKit routes a group's click to the subitem, never to the group. But writing target/action on a subitem that is ALREADY assembled ends the process at the first real layout pass, with no exception and no Closing event.
The sequence that works: setSubitems: with an empty array → wire each subitem → setSubitems: with the full list.
ObjCBlock has been supplied by Xojo since 2019r2
New ObjCBlock(theDelegate As Object) then .Handle. The Delegate takes the declared arguments of the block, without the block pointer at the front; built on an instance method, it carries the object with it, so the Cocoa registry is not needed.
For an asynchronous API, the block, its delegate AND the owning object must be held in PROPERTIES until the callback — never in local variables.
sizeToFit exists only on NSControl
Calling it on a plain NSView raises an unknown selector. NativeSlider.Handle returns a composite host view as soon as there are icons: NativeControlHost.Center therefore makes the call conditional on Cocoa.Responds.
NSTextAlignment does not have the same values on every architecture
The SDK switches on TARGET_ABI_USES_IOS_VALUES: on Apple Silicon, Centre = 1 and Right = 2; on Intel, the other way round. Hence the #If TargetARM in the badges and in NativeTextField.SetAlignment.
SDK 26 renamed the whole of NSBezelStyle
Rounded → Push, RegularSquare → FlexiblePush, TexturedRounded → Toolbar, RoundRect → AccessoryBarAction, Recessed → AccessoryBar, Inline → Badge. The old names are now no more than deprecated aliases.
And setButtonType: RESETS the bezel and the image to values proper to the type: to be called BEFORE setting BezelStyle, never after.
Three APIs where the value is not yet where you look for it
comboBoxSelectionDidChange: — stringValue is not up to date yet, AppKit copies the chosen item over AFTER the callback: read objectValueOfSelectedItem. clickedPathItem — valid ONLY while the action is being sent. NSAlert.suppressionButton — to be read AFTER RunModal.
NSAlert: the order of addition, not the order on screen
The first button added is the default button, hence the rightmost one, the one Return triggers. With a destructive action the keyboard equivalent must therefore be moved — an irreversible action must never fire by reflex.
And NSAlert has no notion of “destructive”: it is its buttons that have one, buttons being an array of NSButton.
The RTF bar does not belong to the editor, but to the window
usesInspectorBar installs an NSTitlebarAccessoryViewController on the window — an __NSInspectorBarView view, 28 points, under the toolbar and across the full width — and shrinks the content view by as much. So it cannot be confined to a frame: it is at window level, exactly as in TextEdit.
Any layout that positions its own views must be redone after the call, otherwise the bar covers the content instead of pushing it. And contrary to what one readily assumes, neither the scroll view nor the order of the calls changes anything — verified in Objective-C without a scroll view, set before setDocumentView:, then outside a window: the accessory is installed in all three cases. For a bar inside the frame another route is needed: the component's own bar, or the ruler accessory below.
The ruler accessory crashes if the client is not set first
It is the only native way to house a bar inside the text area: the accessory becomes a subview of the NSRulerView, hence of the scroll view, without touching the window. But setAccessoryView: raises an exception if the ruler's clientView has not been set beforehand — “you must set the client view of the ruler before you can have an accessory view”. This is not a call without effect, it is a crash.
And the graduated ruler comes with it: 60 points measured for 28 of accessory. For a bar on its own in a frame, the one in NativeRichTextEditor remains preferable.
A formatting command goes to the first responder
changeFont: and changeColor: do not aim at the control you think: the Font panel and the Colours panel send them to the first responder. Without a prior makeFirstResponder, the command goes nowhere, with no error.
Auto Layout and frame placement: what sorts itself out, and what does not
NSGridView and NSStackView work in Auto Layout, whereas the whole library places by frame. Verified in Objective-C: built with initWithFrame:, both keep translatesAutoresizingMaskIntoConstraints at YES — so they place by frame like the rest. And addRowWithViews:, like addArrangedSubview:, sets that flag to NO on the views handed to them: the caller has nothing to adjust.
Two reservations. The class factory stackViewWithViews:, for its part, returns a stack at NO — it would ignore its frame; hence initWithFrame: in the constructor. And fittingSize is expressed in alignment rectangles: the frame of an NSButton overflows its own by a few points, so a grid of buttons fitted to the pixel looks clipped — Refit takes an extra for that.
Two runtime classes with the same name: the selector disappears without a word
ObjCClass reuses a class that is already registered — this is deliberate, and it is what prevents proliferation from one window to the next. But if two different classes ask for the same name, the second inherits from the first and its AddMethod is a silent no-op: its target never answers its selector.
The symptom points nowhere. A menu opens, an item is chosen, popUpMenuPositioningItem: returns YES — an item WAS indeed chosen — and nothing happens: the action went into the void for want of a responder. Since then, AddMethod checks on a reused class that the selector really is there, and raises otherwise. It happened once, between NativeMenuToolbarItem and NativeMenu, both on “VDSMenuTarget”.
The palette menu cannot be pulled down on its own
A menu with a Palette presentationStyle must be the submenu of an item belonging to an ordinary menu. The header is clear: it can be neither opened by popUpMenuPositioningItem:, nor attached directly to a pop-up menu or a toolbar item. AddColorPalette therefore imposes that shape — you give it the title of the carrying item, not a menu of its own.
Good news on the other hand: its selection handler is optional. You pass Nil and read selectedItems afterwards, which spares you having to build an Objective-C block. And selectionMode acts only inside a GROUP — the items between two separators.
A Boolean event that is not implemented returns False
That detail dictates the name of a library's events. An Event WillDrag() As Boolean would have forbidden every drag until the caller wrote it — the opposite of what one expects from a default.
Hence CancelDrag, inverted: returning True cancels, and writing nothing lets it through. NativeDropView, by contrast, keeps Dropped in the direct sense, because a drop has to be handled to mean anything.
A programmatic selection warns the delegate
selectRowIndexes:byExtendingSelection: fires tableViewSelectionDidChange: exactly like a click — NativeSidebar.SelectPage is therefore enough to make the page follow, without pushing it a second time.
With one measured reservation: AppKit does not notify an unchanged selection. Aiming at the row that is already selected produces no event — which is correct, nothing moved, but a caller that relied on the event to synchronise itself needs to know.
The origin of a view that is not flipped is at the bottom left
The frame of an NSDraggingItem is expressed in the coordinates of the view that opens the session. Leaving it at (0,0) does not put the thumbnail “at the start”: it sticks it in the bottom left corner, far from the cursor.
For it to follow the mouse: convertPoint:fromView: with Nil — which means “from the window's coordinates” — on the point returned by locationInWindow, then an offset of half a thumbnail to centre it. Checkable without a mouse: a view at (100,50), a cursor at (180,110) in the window, and you must read (80,60).
A CGPoint by value reads as two Doubles
The drag source selectors — hitTest:, draggingSession:endedAtPoint:operation: — declare a CGPoint passed by value. Two consecutive Doubles occupy exactly the same registers as the structure: the Xojo delegate can therefore receive them separately, without declaring a structure.
Verified in Objective-C rather than assumed: a method installed with the encoding "@@:{CGPoint=dd}" but implemented as (id, SEL, double, double) does receive the right values.
What is dropped is not necessarily a file
A FolderItem coming from a drop knows whether it is a folder — IsFolder. Writing “file” for everything is a convenience, not a limit of drag and drop.
With one nuance: a package — .app, .pkg — is a directory, so IsFolder counts it among the folders. To treat it as a document, which is what the Finder does, you have to look at its extension. The icon returned by NSWorkspace.iconForFile:, for its part, already shows the difference.
Two ways to read an exported constant, and they are not interchangeable
dlsym returns the address of the symbol. For an NSString constant — toolbar item identifier, pasteboard type — the symbol contains a pointer to the object: it has to be dereferenced, slot.Ptr(0). For a symbol declared as an array, such as _NSConcreteGlobalBlock, the address is the object: dereferencing would give anything at all.
Hence Cocoa.ExportedString for the first case, and the direct reading for the second. Both run in the library, three lines apart.
What the system does not retain for you
An NSStatusItem disappears as soon as the variable holding it goes out of scope: the menu bar does not retain it. And Remove is not optional — without it, the extra stays in the bar until the end of the process, even once the Xojo object is destroyed.
Same family as asynchronous blocks and action targets: as soon as AppKit holds only a weak reference, or none, a PROPERTY is what is needed, never a local.
An NSOutlineView IS an NSTableView
The chain reads at runtime: NSOutlineView → NSTableView → NSControl → NSView. An outline is therefore not a control next to the table, it is a table that indents its first column. Writing two independent classes costs the cell code twice, and the second always ends up the poorer: the outline had neither checkbox, nor menu, nor editable cell, nor colour, nor alignment, nor header — nothing AppKit forbade, only what the duplication had left out.
Making it inherit removed ten twin implementations and left only ten overrides: the data source selectors, the column that carries the disclosure triangles, clearing, the refusal to sort, the visible row ↔ storage row translation, row adding turned into node adding, and three relays that also raise the node event. The same kinship holds elsewhere in the library: NativeSidebar and NativeOutlineSidebar descend from NativeSidebarBase, and four toolbar items from NativeToolbarItem.
Xojo dispatches virtually from Super.Constructor
A subclass override is indeed reached from the base class's constructor: the object carries its real class from allocation onwards — the Java model, not C++. That is what allows a single constructor to build what is common and let the subclass supply its differences. One condition: the override must touch only its parameters, the subclass's properties not being set at that instant.
And it must be observed, not assumed. A silent fallback to the base implementation raises nothing: you get a control that simply draws nothing, a symptom too quiet for anyone to trace back to its cause. A Cocoa.Responds(mDelegate, "<selector added by the override>") just after construction, followed by a System.DebugLog on failure, turns the assumption into an observation for the price of one call.
A fallback is not “do nothing”
Everything dating from macOS 26 is set behind a Cocoa.Responds — the SELECTOR queried, never the version number. But the next question matters just as much: what do you do when it is absent? For a border shape or a tint prominence, doing nothing is right — the control keeps its look, and the application is simply less refined. For placeholderStrings, doing nothing would leave the field with no placeholder at all: that is no longer a degradation, it is a regression. So the FIRST of the placeholders is set there.
The rule reads in one sentence: the fallback must give the best available behaviour, not the absence of behaviour. A selector test that guards against the crash while breaking the feature has merely moved the defect.
A property of NSView belongs to every class
prefersCompactControlSizeMetrics (macOS 26) is declared on NSView, not on a particular control: it therefore holds for the twenty-one classes the library wraps. Copying it into each would be the same line twenty times over to correct the day it changes. It is set ONCE, as a shared method of NativeControlHost, which takes the Handle of any control.
The same reasoning for the enumerations: NSControlBorderShape serves NSButton and NSSegmentedControl, NSTintProminence serves NSButton and NSSlider. Both live in NativeControlHost and the classes refer to them — the same reason an outline inherits from a table rather than copying it.
Read the SDK, and probe rather than assume
The thresholds of NSLevelIndicator are documented in one direction only — “values above the warning threshold”. There is no documented way to invert them: any other direction is painted by hand through fillColor.
And when a setting “changes nothing” on screen: push an absurd value and rebuild ONCE. If it moves, the code path is alive and the adjustments were too subtle; if not, the binary that is running is not the code you edited.