Releases: AppKitSupportProgram/AppKitPlus-Release
Release list
0.6.0
New
Hover and cursor interactions for any view. Both attach with addInteraction(_:).
NSCursorInteraction(cursor:)shows a cursor while the pointer is over the view. It takes part in
AppKit's own cursor updates, so a subview that manages its own cursor — a text view's I-beam, a split
view's divider — keeps it over its own area. Like every cursor AppKit manages, it changes only while the
view's window is key.NSHoverAppearanceInteractioncalls a handler with the view and whether it is hovered, each time that
changes. It handles what a plainNSTrackingArealeaves to you: a pointer already inside when the view
is attached or shown, a view hidden or removed while hovered, a table cell removed and re-added during
reuse, and — withactiveScopeset to.inKeyWindowor.inActiveApp— the window losing key status
or the app deactivating. A non-zeroanimationDurationruns the handler inside an animation; changes
made directly to the view's layer are not animated, so animatealphaValueinstead.
The UIGeometry.h helpers AppKit lacks, under UIKit's names and with UIKit's behaviour: string round
trips for CGAffineTransform, CGVector, NSEdgeInsets and NSDirectionalEdgeInsets (plus UIKit's
spellings for points, sizes and rects), NSValue boxing, keyed NSCoder coding, and
NSEdgeInsetsInsetRect. Swift gets UIKit's spellings — rect.inset(by:), NSCoder.string(for:),
NSCoder.cgAffineTransform(for:), NSValue(cgAffineTransform:), coder.encode(_:forKey:) — and .zero,
Equatable and Codable on both edge-inset types.
NSAction can be subclassed in Swift. Its designated initialiser init(identifier:handler:) is now
public (-initWithIdentifier:handler: in Objective-C). A subclass that declares an initialiser of its
own also implements required init?(coder:).
Fixes
NSBezierPath's UIKit-style shapes changed once MapKit was loaded. MapKit, PencilKit and AnnotationKit
define some of the same NSBezierPath selectors, and opening a context menu loads all three (observed on
macOS 26 and 27). From then on rounded rectangles lost UIKit's continuous corners, quadratic curves were
stored as cubics and arcs could differ slightly — from Swift as well. The members no longer live on
NSBezierPath's method list, so no framework can take them over; see Breaking. Present since 0.5.0.
Configured buttons carried state between updates. After its first click a button kept the accent tint
for good, and a button that had been disabled stayed grey once enabled again. A key equivalent or
destructive flag you set on the button yourself was cleared at its first state change; a role now takes
back only what it wrote. An update that changes nothing no longer invalidates the button's intrinsic
size; it used to do so six times, twelve per click. Present in every earlier release.
imageColorTransformer broke symbol sizing. The symbol was redrawn into a bitmap, so
preferredSymbolConfigurationForImage stopped sizing it — an image-only button with a 40 pt star came out
41 points wide instead of 69. A symbol image now stays a symbol and takes the colour as its palette
colour; the same goes for alternateImageColorTransformer.
Secure archives of actions and menus came back nil. Archiving with requiresSecureCoding failed for
an NSContentUnavailableConfiguration whose button properties held a primaryAction or a menu, and for
every NSViewAccessoryPopUpMenu. NSAction now adopts NSSecureCoding and archives as UIAction does:
the identifier is kept and the handler is not, so a decoded action does nothing when performed;
NSDocumentLaunchAction also keeps its title, subtitle, image and tint. AppKit's NSMenu does not adopt
NSSecureCoding, so a secure archive leaves menus out and a pop-up menu accessory decoded from one has an
empty menu. Archives made without secure coding keep the menu.
Breaking
- Objective-C reaches the
UIBezierPathmembers throughcompatibility. TheNSBezierPath (PathConstruction)and(Drawing)categories and their headers are gone;NSRectCornerand the new
property are in<AppKitPlus/NSBezierPath+Compatibility.h>, which the umbrella header imports. Write
[path.compatibility addLineToPoint:point]for[path addLineToPoint:point], and
[NSBezierPath.compatibility bezierPathWithRoundedRect:rect cornerRadius:radius]for the class
factories. Swift source is unchanged —NSBezierPath(roundedRect:cornerRadius:),path.addLine(to:). NSActionis the Objective-C class in Swift, not a struct. Pass identifiers as
NSAction.Identifier("open"becomes.init("open")), callremoveAction(identifiedBy:)for
removeAction(forIdentifier:), and name the handler typeNSActionHandlerforNSAction.Handler.
NSAction { _ in },addAction(_:),removeAction(_:)andprimaryActionread as before.
NSDocumentLaunchAction's inheritedidentifierandhandlerare now typedNSAction.Identifier?and
NSActionHandler.- Delete your own
EquatableorCodableconformance forNSEdgeInsetsorNSDirectionalEdgeInsets.
The framework now declares both, and the compiler reports a second one as redundant.
Compatibility
Added: NSCursorInteraction, NSHoverAppearanceInteraction (with NSHoverAppearanceHandler and
NSHoverAppearanceActiveScope), the NSGeometryAdditions.h functions and its NSValue and NSCoder
categories, NSBezierPath.compatibility (NSBezierPathCompatibility, NSBezierPathCompatibilityFactory)
and -[NSAction initWithIdentifier:handler:]. Code built against 0.5.0 that uses NSAction from Swift, or
the UIBezierPath members from either language, must be recompiled: the struct's symbols and the 13
category methods are gone from the binary. Nothing else public was removed. The minimum deployment target
stays macOS 12.0.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.6.0")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 27.0 Build version 27A266a |
| SHA-256 | 6c1d0b5db3307b9ef0142d8e454b8e03b5ec886aa2d7d1ce001a464bedabfdcc |
0.5.0
New
UIBezierPath's construction API on NSBezierPath. NSBezierPath's look-alike methods compute
something else — its arc takes degrees and reverses clockwise, its rounded rect has circular corners —
so drawing code ported from UIKit drew the wrong shape with no compile error. The twelve missing members
are added and produce the elements UIBezierPath produces: the rounded-rect factories (UIKit's
continuous corners, NSRectCorner), the arc factory and addArcWithCenter:… (radians, UIKit's
clockwise), addLineToPoint:, addCurveToPoint:…, addQuadCurveToPoint:controlPoint:, appendPath:,
applyTransform:, usesEvenOddFillRule and fill/strokeWithBlendMode:alpha:.
Settable layout margins on NSLayerBackedView. AppKit's layoutMarginsGuide is read-only.
NSLayerBackedView gains UIView's layoutMargins, directionalLayoutMargins,
preservesSuperviewLayoutMargins, insetsLayoutMarginsFromSafeArea and layoutMarginsDidChange, and
they drive AppKit's own guide, so constraints already made against layoutMarginsGuide follow them.
Resolution follows UIKit, except that the default is what AppKit computes rather than UIKit's 8 pt: a
view that never sets margins lays out exactly as before.
Effects of your own in NSEffectView. NSEffectHandler is now public, in the new
AppKitPlus.NSEffectSubclass submodule (import AppKitPlus.NSEffectSubclass, or
#import <AppKitPlus/NSEffectSubclass.h>; a plain import AppKitPlus does not see it). Subclass
NSEffect and NSEffectHandler, name the effect class and the backing view class, implement
apply(_:to:), and call NSEffectHandler.register(_:) in applicationWillFinishLaunching(_:). The
effect then renders wherever an effect is accepted: NSEffectView, NSBackgroundConfiguration.visualEffect
and NSModernDrawerConfiguration.effect. The most specific effect class wins, and within one class the
latest registration, which is how you replace the built-in handler for NSVisualEffect or NSGlassEffect.
Accessories that use AppKit's own interactions.
NSViewAccessoryActionMenu(Swift:.actionMenu()) is an ellipsis button that opens exactly the menu
a right-click on its row opens, through the table's own context-menu path —clickedRow, the highlight
ring and the cleanup are AppKit's.displayed: .whenHoveredgives the usual hover-revealed button.NSViewAccessoryDetailcan show a popover anchored to its button: set
popoverContentViewControllerProvider(Swift:.detail(popover:)). The provider receives an
NSViewAccessoryPopoverPresentation(behavior,animates,contentSize,close()); a popover you
leave unsized takes its content's fitting size.- Every clickable accessory is offered to VoiceOver as a custom action of its row.
- A hover-only accessory stays visible while a menu or popover opened from it is open.
Fixes
Swift content configurations never saw their host's state. No host ever called updated(for:) on a
configuration written in Swift — your own struct, or NSHostingConfiguration — so it never reacted to
selection, hover or drop targeting. Hosts now pass a snapshot of their state; a content-unavailable host
passes NSContentUnavailableConfigurationState, search text included, where UIKit passes a cell state.
And NSViewController.contentUnavailableConfiguration no longer stores nil for a Swift configuration
other than NSContentUnavailableConfiguration.
Editing a copy of a Swift configuration could edit the original. var custom = original; custom.textProperties.color = .red also changed original for NSContentUnavailableConfiguration's
property groups, and editing NSContentUnavailableView.configuration or a transaction's
initialSnapshot / finalSnapshot wrote into the view or the transaction. Every Swift value type that
wraps an Objective-C object now copies it before the first write.
Content-configuration protocols demanded the main actor. Calling updated(for:), reading a
configuration state, or building an NSHostingConfiguration from nonisolated code failed to compile,
even in the Swift 5 language mode. Isolation now matches UIKit: only makeContentView() and
NSContentView are @MainActor.
Main-actor types could not conform to fifteen protocols without @preconcurrency. They are now
@MainActor in Swift, as their UIKit counterparts are: NSActionBindable, NSAppearanceProxyContainer,
NSCalendarViewDelegate, the three NSCalendarSelection…Delegates, NSFocusEnvironment, NSFocusItem,
NSFocusItemContainer, NSFocusItemScrollableContainer, NSFocusDebuggerOutput,
NSModernDrawerDelegate, NSLayoutSupport, NSTraitChangeRegistration and NSMutableTraits.
NSTraitCollection { … } and changedTraits(from:) were ambiguous in Swift. Both spellings that
compile against UITraitCollection now compile against NSTraitCollection without a type annotation.
NSOutlineViewDiffableDataSource unregistered your drag types. Assigning reorderingHandlers
without canReorderItemHandler — turning reordering off, or in Swift setting didReorder before
canReorderItem — cleared every type registered on the outline view, so drops from elsewhere, files for
instance, silently stopped arriving. Present since the data source was introduced.
A reorder aimed at the top of an inset table dropped at the end. With floating group rows (the
default), dragging into the strip between a section header and its first row proposed a drop below the
last row. NSOutlineViewDiffableDataSource and NSTableViewDiffableReorderableDataSource now drop where
the pointer is. Every macOS version was affected.
Tables in a navigation-controller page could not take focus. After every push or pop the page's plain
container view became first responder. On macOS 27 a click on a table in the page then left its selection
in the inactive grey, and on every system a right-click on a row's text opened no menu.
preferredFirstResponder now answers the page's view only when it accepts first responder, and nil
otherwise, which leaves the window first responder.
Breaking
- Renderer subclassing hooks need their submodule. A plain
import AppKitPlusno longer sees
NSGraphicsRendererSubclass.h(+rendererContextClassand the rest). Add
import AppKitPlus.NSGraphicsRendererSubclassor#import <AppKitPlus/NSGraphicsRendererSubclass.h>. <AppKitPlus/AppKitPlusRuntimeClassName.h>is gone. Its macros are in
<AppKitPlus/AppKitPlusDefines.h>; importing the umbrella header is unaffected.NSTraitCollection(traits:)is unavailable in Swift. WriteNSTraitCollection { $0.… = … }
(init(mutations:)). Objective-C'straitCollectionWithTraits:/initWithTraits:are unchanged.- A
@MainActorcontent configuration needs a main-actor conformance. A type conforming to
NSContentConfigurationis no longer inferred main-actor isolated. If the type or itsupdated(for:)
needs the main actor, declareextension MyConfiguration: @MainActor NSContentConfiguration {};
otherwise Swift 5 warns and Swift 6 errors, exactly as withUIContentConfiguration.
NSHostingConfigurationis no longer main-actor isolated either. - Conforming to one of the fifteen protocols above in a type's own declaration makes that type
@MainActor, as conforming to the UIKit counterpart does. Nonisolated code calling into such a type
may needawaitin Swift 6. NSModernDrawerConfiguration.shadowisNSShadowProperties?in Swift (it was
__NSShadowProperties?). Reading returns a copy: edit it, then assign it back.NSDocumentLaunchAction's Swift initialisers hand the handler theNSDocumentLaunchAction(it was
__NSAction).updateInteractiveTransition:inContext:left the public headers ofNSTransitionControllerand
NSParallaxTransitionController, as did the latter's_addShadowToView:withAlpha:and
_setupDimmingViewInContext:withAlpha:. They took private context types; only
NSNavigationControllercalled them.
Behaviour change
A nil backgroundColor in NSBackgroundConfiguration now fills with the tint color
(controlAccentColor, with backgroundColorTransformer applied), as documented and as UIKit does; it
used to draw nothing. Set NSColor.clearColor for no fill. The list factories — unselected
listCellConfiguration and listAccompaniedSidebarCellConfiguration, listHeaderConfiguration,
listFooterConfiguration — now answer clearColor instead of nil, and still draw nothing.
Compatibility
Added: NSEffectHandler, NSViewAccessoryActionMenu, NSViewAccessoryPopoverPresentation,
NSViewAccessoryDetail.popoverContentViewControllerProvider, NSRectCorner, the NSBezierPath (PathConstruction) and (Drawing) categories, the layout-margin members of NSLayerBackedView, and the
AppKitPlusDefines.h header. Everything under Breaking is source-level: no public class, method or type
layout was removed from the binary, and the methods that left the transition-controller headers still
exist at runtime. The framework no longer exports 43 internal C symbols that no public header declares.
The minimum deployment target stays macOS 12.0.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.5.0")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 1dd80ea5a9742b2c52104ed3553ae3511c956d6a14edc0b14b13224bc5d8f5b5 |
0.4.4
New
NSTableViewDiffableReorderableDataSource — AppKit's NSTableViewDiffableDataSource with the data
source methods it never implemented. The stock class answers only numberOfRowsInTableView:; the six
drag-and-drop methods and tableView:sortDescriptorsDidChange: are simply absent, so a diffable table
could not be reordered without subclassing it yourself and translating rows back into snapshot edits.
This subclass does that translation:
- Drag-and-drop reordering through
reorderingHandlers, shaped like
NSOutlineViewDiffableDataSource's:canReorderItemHandlerturns reordering on and decides per item,
willReorderHandler/didReorderHandlerreceive anNSDiffableDataSourceTransactioncarrying the
snapshots before and after and one section transaction per section whose order changed. Several
selected rows move together in row order, rows can move into another section, dropping onto a row
inserts above it, and dropping above a section header lands at the end of the section above (or at the
start of the first section). The commit is a real row move, not a reload: row views, the selection and
anything else that lives on a row travel with it.animatesReorderingChanges(defaultYES) or the
per-reordershouldAnimateReorderingHandlerswitch the animation off without falling back to
reloadData. sortDescriptorsDidChangeHandler, called when the user clicks a column header;oldDescriptors
is empty on the first change.applySnapshotUsingReloadData:(with and without a completion), the explicit name for what
applySnapshot:animatingDifferences:NOalready does.
Only drags started by the data source's own table view are treated as reorders; every other drag answers
NSDragOperationNone, so a subclass can call super first and then accept its own pasteboard types.
Enabling reordering registers one private pasteboard type on the table view and leaves yours alone;
disabling it removes only that one. In Swift, NSTableViewDiffableReorderableDataSource<Section, Item>
has every member of AppKit's NSTableViewDiffableDataSource<Section, Item> plus the additions, so
adopting it is a type-name change; identifiers must be Hashable & Sendable. The Example app gains a
"Reorderable Table" page.
Compatibility
Added: NSTableViewDiffableReorderableDataSource, NSTableViewDiffableDataSourceReorderingHandlers.
Nothing public was removed or renamed and no type layout moved — existing binaries keep working without
recompiling; recompile only to use the new class. The minimum deployment target stays macOS 12.0.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.4.4")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 27.0 Build version 27A266a |
| SHA-256 | 8b4399ecacb974276d76b0e6e77c419887f3bdffc338da39346effbc48f49d10 |
0.4.3
New
NSDeferredMenuItem — a menu item whose items arrive after the menu is on screen. Port of
UIDeferredMenuElement. Put one in any NSMenu (main menu, submenu, pop-up button, context menu):
it shows as a disabled "Loading…" placeholder, asks its provider on the main thread when the containing
menu opens, and inserts the items the provider hands back at the placeholder's position. Three entry
points, all matching UIKit: itemWithProvider: caches the result and asks once,
itemWithUncachedProvider: asks on every opening, and itemUsingFocusWithIdentifier:shouldCacheItems:
carries no provider and instead asks the responder chain for one through
providerForDeferredMenuItem: (provider(for:) in Swift), starting at the view the menu was opened
from — the pop-up button, or the view whose context menu it is — so the view controller that owns the
button is on the chain. No isa change, no swizzle, no delegate takeover: the trigger is AppKit's own
per-menu open / close notification, linked weak.
NSScrollBehavior — where momentum scrolling comes to rest. AppKit has no notion of this; UIKit has
isPagingEnabled and scrollViewWillEndDragging:withVelocity:targetContentOffset:. NSScrollBehavior
is the calculation behind both as an immutable value: normal leaves the landing point alone, paging
snaps to a multiple of pageOffset away from pagingOrigin, detents snaps to one of a list of
NSScrollDetent. NSScrollBehaviorInteraction attaches one to a plain NSScrollView and keeps
whatever delegate the scroll view already had. The algorithm is ported instruction by instruction from
the private framework behind Photos, and three of its rules read the opposite of their names, so read
the header before relying on them: paging rounds in the direction of travel rather than to the nearest
boundary; with allowsFlickAcrossMultiplePages off a flick advances exactly one page and a gesture
starting on a boundary goes nowhere; a detents behavior picks relative to the current offset, so one
flick moves one detent however hard it was thrown. Two deliberate departures from the source, both so a
layout mistake cannot trap the host: a closed last detent with nowhere to step to snaps nothing, and
unsorted detents assert in debug and are sorted in release.
Fixes
Content-configuration hosts stopped refreshing after their first configure. Both table engines
took key-value observing on a row view before installing the dynamic subclass that carries the layout
override, and an isa has one owner: the subclass was declined, setNeedsUpdateConfiguration marked the
row for layout, and nothing ever ran the update pass. Selection, emphasis and drop-target changes stopped
re-resolving the configuration, and layout margins stopped reaching the content view. Every table using
the row view or cell hosts was affected. Install first, observe second; no code change on your side.
A glass view inside a navigation controller was tinted on the first push. The framework carried a
tree-inherited tintColor on every NSView (internal since 0.4.0) that answered its first read with
controlTextColor and wrote that colour into every subview. NSNavigationController performed that
read when it built its first back item, and on macOS 26 the write landed on NSGlassEffectView.tintColor
— the glass's own tint — so a page backed by a glass view went light for the length of the first push.
The category is gone. The only thing that rode on it, the bar button, keeps its own tint now.
Behaviour change
NSBarButtonItem.tintColor set after the item's button exists now reaches the button. It used to
apply only at creation and silently do nothing afterwards. The tint goes to the button the item
generated and nowhere else: a custom view you supplied is left alone even when it has a tintColor
of its own.
Performance
NSDiffableDataSourceSectionSnapshot's Objective-C bridge now carries the same
@_semantics("convertToObjectiveC") / @_effects(readonly) annotations as the other seven bridged
overlays, so an optimized downstream build folds an ObjC → Swift → ObjC round trip instead of paying
for a copy. NSOutlineViewDiffableDataSource makes four such trips per apply. Debug builds are
unaffected; the attributes are in the shipped .swiftinterface.
Compatibility
Added: NSDeferredMenuItem, NSDeferredMenuItemProvider, NSResponder (DeferredMenuItem),
NSScrollBehavior, NSScrollDetent, NSScrollBehaviorInteraction. Nothing public was removed or
renamed and no type layout moved — existing binaries keep working without recompiling; recompile only to
use the new classes. Private implementation classes were renamed in source (_NSP… → _NS…); their
runtime names are unchanged and none of them was ever exported.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.4.3")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 27.0 Build version 27A266a |
| SHA-256 | 24c2cb3044a7182521e371f5e19f5073042d5d2d26a526900dd222b4b119b4df |
0.4.2
Performance
Two more NSOutlineViewDiffableDataSource.applySnapshot costs are gone, both in the step that
translates a diff into NSOutlineView calls.
Reorders moved almost every row. The batch engine walked the target order and moved whatever was
not at its index. One row moved from far away therefore cost a move for every row it pushed along, and
each move searched the row model linearly: swapping a tenth of a thousand roots issued 989 moves for
184 rows that had actually changed place. The engine now keeps the longest run of rows already in
relative order, moves or inserts every other row exactly once, and derives every index arithmetically
instead of searching. Moves are still moves — row views, selection and expansion state travel with the
row as before — and a moved row lands at the index the snapshot gives it whichever direction it travels.
Expanding many parents settled the view once per parent. Expansion-state sync called
expandItem: / collapseItem: one at a time, and NSOutlineView settles its visible row views after
each call. They now share one beginUpdates / endUpdates, opened with the first call that needs it.
Measured against 0.4.1 on the same machine at the same time (release build, 1000 roots with five
collapsed children each, window on screen):
| 0.4.1 | 0.4.2 | UICollectionViewDiffableDataSource |
|
|---|---|---|---|
| swap 10% of the roots | 12.2 ms | 4.7 ms | 3.7 ms |
| reverse all roots | 19.0 ms | 12.9 ms | 12.4 ms |
| expand 100 roots | 15.1 ms | 6.4 ms | 7.5 ms |
| append 100 roots | 2.1 ms | 2.1 ms | 2.0 ms |
Appends, deletes, sectioned snapshots and fully expanded deep trees are unchanged.
One caveat for anyone benchmarking: after a batched expansion the view holds exactly the on-screen row
views, whereas one-by-one expansion used to leave about a dozen extra ones behind. A collapse measured
immediately after an expand therefore creates those views itself and reads one or two milliseconds
slower than before; from a view the user expanded, collapsing costs what it did.
Compatibility
Everything here is internal. No public API was added, removed or renamed, no type layout moved, and
the export table is unchanged — existing binaries keep working without recompiling.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.4.2")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 995651eaa2a61c18dc5682f6952e9adfb7df82a35490d8b67cb29e915f161d1e |
0.4.1
Performance
NSOutlineViewDiffableDataSource.applySnapshot was priced by the shape of the tree rather than by the
size of the change. Two separate costs are gone.
The part that grew with the number of parents. Every apply recomputed the section snapshot's root
array from scratch — once per inserted row, because NSOutlineView asks child:ofItem: for each one.
Collecting the visible rows then called parentOfChildItem: per row, which scans backwards to node 0
for a root item, giving O(roots × parents). Expansion-state sync hashed every identifier in the
snapshot. Root arrays and visible items are now computed once per state (the state is immutable, so
the cache cannot go stale), the visible-row walk derives parents from a level stack instead of asking
the tree, and expansion sync addresses the snapshotter by index. A tree with 761 parents now costs
what the same data costs with one parent.
The part that did not depend on the change at all. An empty diff still cost about 6 ms. Four
things happened on every apply regardless: a fresh top-level snapshot was rebuilt, _itemToSection
was torn down and refilled (in sectionless mode the answer is always the same sentinel), the batch
engine tested every visible row for parent addressability including rows with no children, and the
sentinel section was fed to expandItem:, which sends AppKit through its lazy-item lookup. The
top-level snapshot is now materialised on read, only parents that actually have child rows are
settled, and neither the section map nor expandItem: is touched in sectionless mode.
Measured against 0.4.0 with an external A/B harness — 1269 roots, 761 parents, 3466 items, children
collapsed, three runs each, alternating:
| 0.4.0 | 0.4.1 | |
|---|---|---|
| apply, all items | 14.48 ms | 2.44 ms |
| apply, filtered subset | 7.36 ms | 2.54 ms |
| apply, empty diff | 8.04 ms | 2.26 ms |
Normalised against each framework's own hand-written reloadData baseline on the same machine at the
same time, this port scores 0.19 where UICollectionViewDiffableDataSource scores 0.62; at 0.4.0 it
was 1.01. Absolute numbers are from a loaded machine and are conservative.
NSBrowserDiffableDataSource builds its parent index the same way and got the same treatment.
Behaviour change
After a top-level apply, snapshot() no longer carries the reload requests that apply already
performed. Previously the data source kept the caller's snapshot object as its current snapshot,
and any reconfigureItemsWithIdentifiers: / reloadItemsWithIdentifiers: recorded on it stayed
there. Taking snapshot(), editing it and applying it back — the ordinary way to use this API — would
therefore redo the previous apply's reloads on top of your own.
If you were relying on that to re-run a reload, record it again on the new snapshot. Nothing else
about snapshot() changed: it still reports each section as that section's visible items.
Compatibility
Everything here is internal. No public API was added, removed or renamed, no type layout moved, and
the export table is unchanged — existing binaries keep working without recompiling.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.4.1")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 9f0f4c926ab9e7e3e1b3636f84415860f017f91cc1b605dc52d8aa14a0af3dc8 |
0.4.0
New
View-based browser columns, and a diffable data source for NSBrowser
AppKit has shipped diffable data sources for NSTableView and NSCollectionView since 10.15, and
this framework added the NSOutlineView one; NSBrowser was the last list class without one. It was
also the only one still drawing rows with NSCell, which ruled out Auto Layout, hosted SwiftUI and
this framework's own content configurations inside a browser row.
-
NSViewBasedBrowser— anNSBrowserwhose columns lay out rows as views. Everything else about
NSBrowseris unchanged: the same item data source methods, the same column and selection
behaviour, the same public API. Rows are asked for from the delegate asNSViews, through the new
NSViewBasedBrowserDelegate(browser:viewForItem:atRow:inColumn:, plus optional row-view and
disclosure-indicator methods). This is the arrangement Finder's column view uses. -
NSBrowserDiffableDataSource<ItemIdentifierType>— describes the whole browsable tree with a
singleNSDiffableDataSourceSectionSnapshot. A browser is a plain tree, so there is no section
layer. It installs itself as the browser's delegate and forwards everything it does not answer to a
delegate of your own, so column titles, sizing and drag-and-drop keep working. A Swift overlay gives
it the usual generic identifier, so aHashablestruct works as it does everywhere else.Passing a stock
NSBrowseris supported: the tree is still published, rows are simply drawn by
cells and the cell provider is never called.
Snapshots are applied with -[NSBrowser reloadColumn:] rather than row-level animation. That is a
deliberate choice, not a stub: AppKit's own item-based column reload restores the selection by item
identity and closes the columns after one whose selection can no longer be restored, both of which
row-level updates would have to reimplement. animatingDifferences is accepted for symmetry and
currently has no effect, so adopting animation later will not change any signature.
Two contracts are easy to get wrong and are documented on the API itself:
- A column header view controller must carry its height in the view's frame by the time
loadViewreturns. AppKit builds the column's clip view from that frame and never consults the Auto
Layout fitting size, so a view made withNSView()plus a height constraint renders as no header. rowHeightand the item-based lookups (itemAtRow:inColumn:,parentForItemsInColumn:,
isLeafItem:,selectionIndexPath) only work once the data source is installed — that is what puts
the browser into item mode. SettingrowHeighton a freshly constructed browser raises.
On any OS where AppKit stops vending the private column-controller class this builds on, columns fall
back to cells rather than failing to launch; NSViewBasedBrowser.isViewBasedColumnSupported reports
which you are getting.
Fixes
applySnapshot:animatingDifferences:NOstill animated moves. The inserts and removals were passed
NSTableViewAnimationEffectNone, butmoveItemAtIndex:inParent:toIndex:inParent:takes no
animation argument and animates implicitly under whateverNSAnimationContextis current — a probe
measured a row view's presentation layer travelling for about 0.3 s in an apply that was supposed to
be instant. Both apply paths now wrap their moves in a zero-duration animation group, the AppKit
counterpart of what UIKit does for its own non-animated apply. Affects both outline data sources.
Compatibility
Everything above is additive. No API was removed, renamed or changed, and no type layout moved, so
existing binaries keep working.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.4.0")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 3db462403d5567a416842247f1d61cd2bb9392648a41b4aaef099853899ddba4 |
0.3.2
Fixes
- Applying a snapshot that deletes rows and reorders the survivors in the same batch raised
NSOutlineViewException: … is outside the valid children index bounds, and reordering several
rows at once could land them in the wrong order even when no index went out of range. Both data
sources and bothapplyvariants were affected;applySnapshotUsingReloadData:was not.
NSOutlineViewapplies every call insidebeginUpdates/endUpdatesto its row model at once,
so an index must be right at the moment of the call, while every release since 0.1.5 issued the
moves after the removals with indexes taken from the snapshot the batch started from (0.1.x had
worked around the same crash for drag reordering by reloading; that workaround was dropped in the
May engine and nothing guarded it). The row updates are now
computed against the rows the view actually has at each step of the batch. Regression tests cover
the flat, nested, sectioned and few-hundred-row shapes.
No API changed and nothing is deprecated. Section changes that keep the visible order (fixed in
0.3.1) keep working; the update engine no longer needs AppKit's private identifier differ at all.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.3.2")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 44011e2b812ed2edb8b046936bf3e8d4a96a7f523e3462cc25b04fba2a17ecfa |
0.3.1
Fixes
rowViewProviderwas never consulted byNSOutlineView, on either data source and in
both languages. AppKit caches a delegate's capabilities when the delegate is set, which the
data source does in its initializer — before a provider can exist — and the delegate
interceptor only claimedoutlineView:rowViewForItem:while a provider was installed.
Custom row views now appear. Section rows never reach the item-typed provider: they go to
your own delegate if it implementsoutlineView:rowViewForItem:, otherwise AppKit builds
its regularNSTableRowView.- Reading
differenceonNSDiffableDataSourceTransactionor
NSDiffableDataSourceSectionTransactionfrom Swift trapped with
"Failed to cast difference". The typed difference is now produced by bridging through
NSOrderedCollectionDifference. - Applying a top-level snapshot that moved an item to another section — or under another
parent — without changing the order of the visible rows showed the item twice: once in its
new place and once, stale, in its old one. Both the section-snapshot rebaser and the row
update engine worked from a flat diff that cannot see such a move; both now follow the
item's container. A parent moved together with its expanded children across sections no
longer raises "Duplicate item identifiers", and the moved parent stays expanded, so the
destination shows exactly what the top-level snapshot lists there.
No API changed and nothing is deprecated. Each fix ships with a regression test that failed
first, and the diffable data sources — the Objective-C facades, the Swift overlays, the
implementation, the rebaser and the tree snapshotter — are now covered member by member.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.3.1")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 779b34e858b4e098019ba378a7f541a2c8a63c2f3d81b4c895ad55130ddbdaeb |
0.3.0
Breaking changes
NSViewController transition state is removed
The NSViewController (TransitionState) category is gone with no replacement:
transitionStateTrackingEnabled, isBeingPresented and isBeingDismissed. These are
category methods, so a binary built against 0.2.x does not fail to link — it raises
doesNotRecognizeSelector: the first time one is called. Source referencing them fails to
compile.
Both probes were permanently wedged. An appearance AppKit starts and then abandons set the
BeingPresented flag, and only -viewDidAppear cleared it, so every real dismiss afterwards
answered NO; separately, window.orderOut(nil), minimising and restoring each reported a
dismiss and a present for a controller that went nowhere. That was the module's entire public
contract. The two other flags the README advertised —
isMovingToParentViewController / isMovingFromParentViewController — were never implemented.
Migration. If you need real transition phases, use View Controller Transitioning
(NSViewControllerTransitionCoordinator and friends). If you only wanted to tell a temporary
disappearance from a permanent one, test self.view.window == nil or
self.parentViewController == nil directly — more reliable than what this module inferred.
NSOutlineViewDiffableDataSource: the top-level snapshot now follows the section snapshots
No signature changed, but three behaviours did. The top-level snapshot's item list for a section
is now always equal to that section snapshot's visibleItems, matching
UICollectionViewDiffableDataSource. snapshot() therefore returns items, not just section
identifiers, so the ordinary "take the snapshot, edit it, apply it" idiom works — it previously
did nothing or threw.
- Applying a freshly built top-level snapshot that lists sections but no items now empties
those sections, as it does in UIKit. It used to leave their contents alone. A snapshot
obtained fromsnapshot()carries its items, so round-tripping is unaffected. applySnapshot:toSection:appends a section that is not yet in the top-level snapshot
instead of asserting.- On a sectioned data source with no
sectionHeaderViewProvider, section rows are rendered by
a built-in default header and are group rows. Previously the section identifier was passed to
yourcellProvider, which Objective-C callers may have relied on to draw section rows.
Fixes
- A user expanding a row by hand was not recorded in the section snapshot unless a lazy-loading
handler was installed, so the next apply collapsed it again. - Mixing top-level and per-section applies could show duplicated or misplaced rows once a section
contained nested items: view updates were dispatched by flat top-level index, which does not
match the rowsNSOutlineViewdraws. They are now computed from the visible items. - Root-level items could not be reordered in sectionless mode.
applySnapshotUsingReloadData:ignored items on a sectioned data source.- The post-animation repair in sectionless mode was a no-op.
- A sectioned data source with no
sectionHeaderViewProvidertrapped in the Swift layer. - The Swift reordering handlers reported only the first item of a multi-item drag.
Warning
AppKitPlus is in testing. No API or ABI stability is promised.
Any release may remove classes, change type layouts, or change protocol requirements, with no
deprecation period. Pin an exact version and read these notes before upgrading.
Installation
.package(url: "https://github.com/AppKitSupportProgram/AppKitPlus-Release", from: "0.3.0")Artifact
| Platform | macOS 12.0+ |
| Architectures | arm64, arm64e, x86_64 |
| Built with | Xcode 26.6 Build version 17F113 |
| SHA-256 | 4b000aa402502b1509a28ca5134bfb39776efc978ff3c936b350f61669810a64 |