NSCollectionLayoutBoundarySupplementaryItem background blur covering the entire layout section

My app has the following UI layout:

  • NSSplitViewController as the windows contentViewController
    • NSPageController in the content (right) split item
      • NSTabViewController as the root items of the NSPageController
        • NSViewController with a collection view in the first tab of that NSTabViewController

The collection view is using a NSCollectionViewCompositionalLayout in which the sections are set up to have a header using NSCollectionLayoutBoundarySupplementaryItem with pinToVisibleBounds=true and alignment=top

With macOS 26, the pinned supplementary item automatically gets a blurred/semi-transparent background that seamlessly integrates with the toolbar. When the window's title bar has a NSTitlebarAccessoryViewController added, the said semi-transparent background gets a bottom hard edge and a hairline to provide more visual separation from the main content.

During runtime, my NSPageController transitions from the NSTabViewController to another view controller. When transitioning back, the semi-transparent blur bleeds into the entire section. This happens no matter if there's a NSTitlebarAccessoryViewController added or not. It doesn't happen 100% of the cases, it seems to depend on section size, header visibility and/or scroll position. But it happens more often than not.

Most of the time, a second or so after the back transition - shortly after pageControllerDidEndLiveTransition: of the NSPageControllerDelegate is called - the view updates and the supplementary views are back to normal. Sometimes, the issue also appears not when transitioning using NSPageController, but simply by scrolling through the collection view.

Anyone has an idea what is happening here? Below are two screenshots of both the "ok" and "not ok" state

I'm on macOS 26.0.1 and I'm using XCode 26.0.1

Have you inspected the value of clipsToBounds on views in your view hierarchy? Particularly supplementary views.

If the view has clipToBounds set to NO and its -drawRect: uses the dirtyRect it can draw outside its bounds.

My supplementary view is a simple NSView with a centered NSTextField - I don't do custom drawing in drawRect: and setting clipsToBounds to true didn't help. The blur and shade is coming from the NSCollectionView.

One this that seems to mitigate the artifact a bit is to call invalidateLayout on the collectionViewLayout with a 100ms delay in the pageController:prepareViewController:withObject: delegate method of NSPageController - but it's a hacky workaround

I suggested looking at clipsToBounds b/c I recently ran into a situation that looks very similar to your second screenshot when modifying a project that uses NSCollectionView.

In my case the issue occurred when the layout used a footer view. The footer view implemented drawRect: and was filling the dirty rect. The project was written (not originally by me) but long before clipsToBounds was exposed as public API. Setting clipsToBounds to YES on the footer view stopped the issue from occurring.

It was not my initial thought to check clipsToBounds because all the system blurring mixed in with the semi transparent fill color the footer view used made me think the issue was caused by something else. When I changed the footer view background color to something that stood out more like NSColor.purpleColor it made it more obvious. I first tried all sorts of other tricks. So I figured this story may be worth sharing.

If you are able to narrow your issue down to something else it would be great if you shared here for knowledge building.

the semi-transparent blur bleeds into the entire section.

I just ran into this in one of my apps after scrolling a collection view a bit. Luckily I was attached to the debugger and was able to debug the view hierarchy. I discovered it to be an instance of a private class called NSScrollPocket which gets added to the NSScrollView.

I set it hidden with: po (void)[0x977e0b480 setHidden:YES];

The blur over the section went away. Then I called setHidden:NO and it came back.

Usually the scroll pocket appears to be near the top where the pinned section header is but I guess sometimes they have one over the entire section rectangle.

So maybe we can workaround with an NSScrollView subclass like:

-(BOOL)_isPotentialSubviewOuttaPocket:(NSView*)subview
{
    Class scrollPocketClass = NSClassFromString(@"NSScrollPocket");
    return (scrollPocketClass != nil
            && [subview isKindOfClass:scrollPocketClass]);
}

-(void)didAddSubview:(NSView*)subview
{
 [super didAddSubview:subview];
 if ([self _isPotentialSubviewOuttaPocket:subview])
    {
     // outta pocket
      subview.hidden = YES;
    }
}

But if AppKit sets the hidden property on the scroll pocket at various times maybe we need to just prevent NSScrollPocket from entering the view hierarchy..meh

or perhaps we can use a sledgehammer to make sure instances of NSScrollPocket remain hidden. might have to put it on the grill and sizzle, I mean swizzle that thing

But still when transitioning back with my NSPageController to the collection view the pinned header is hidden until the transition completes but I was able to mitigate that issue by calling invalidateLayout() briefly after the navigation starts.

I noticed a similar issue - headers are sometimes misplaced when they are shown for the first time (headers can be toggled in my app). I'm actually using the old flow layout still so it may be a different issue but the headers snap in place after scrolling a bit. In my case calls to invalidateLayout etc. didn't reliably help. Fixing stuff after performSelector: afterDelay worked but is ugly and I want to avoid.

For me I had better success just calling -performBatchUpdates:completionHandler: with a nil updates block. My original plan was to try to fix the frames in the completionHandler which ought to be called after layout is actually ready but simply calling performBatchUpdates:completionHandler: and doing nothing seems to kick it in gear. I guess I'll have to test more to be sure though.

I also use the -performBatchUpdates:completionHandler: with the nil updates block to do things like restoring scroll position which you can only do when the layout is ready.

For me I had better success just calling -performBatchUpdates:completionHandler: with a nil updates block.

Maybe scratch that. I'm able to restore scroll position properly after a previous reloadData call in the completionHandler: the but sometimes header views are still misplaced. After inspecting index paths for visible items and index paths for header view etc. it seems that these index path collections are completely out of whack and are nowhere near the visible collection view region.

Appears there is some kind of bug in NSCollectionView where visible index paths and visible header views are not updated to match the visible region. Seems to occur when the collection view is updated/scrolled ~viewDidAppear so I'm probably experiencing the same bug when it comes to the header views not be properly shown.

Also was surprised to discover that there are some situations where NSCollectionView won't call the completion block with -performBatchUpdates:completionHandler: so whatever code you put in there could potentially get dropped (shouldn't it always call the completion block and pass NO for the finished flag?). I might just umm stay away from that...

Fixing the header view frames like this seems to work.

NSSet <NSIndexPath*> *theIndexPaths = [collectionView indexPathsForVisibleSupplementaryElementsOfKind:NSCollectionElementKindSectionHeader];
		for (NSIndexPath *aIndexPath in theIndexPaths)
			{
				NSView *headerView = [collectionView supplementaryViewForElementKind:NSCollectionElementKindSectionHeader atIndexPath:aIndexPath];
			
				NSView *headerViewSuperview = headerView.superview; 
				NSCollectionViewLayoutAttributes *headerLayoutAttributes = [collectionView layoutAttributesForSupplementaryElementOfKind:NSCollectionElementKindSectionHeader atIndexPath:aIndexPath];
				if (headerView != nil
					&& headerViewSuperview != nil
					&& headerLayoutAttributes != nil)
					{
						NSRect headerViewRect = headerView.frame;
						NSRect targetRect = [headerViewSuperview convertRect:headerLayoutAttributes.frame fromView:collectionView];
						if (!NSEqualRects(targetRect, headerViewRect))
							{
								headerView.frame = targetRect;
							}
						else
							{
								// frame okay
							}
					}
				else
					{
						os_log_fault(OS_LOG_DEFAULT, "Header view in visible region not properly set up.");
					}
			}

I'll probably wrap it in a category method

There's certainly some issues with the sticky headers. I was seeing crashes in my analytics and now also locally that appear to be related with the headers (_updatePinnedSectionSupplementaryItemsForCurrentVisibleBounds is in the stack trace):

ObjCRuntime.ObjCException: Objective-C exception thrown.  Name: NSInternalInconsistencyException Reason: Frame {{inf, inf}, {0, 0}} does not intersect {{0, 0}, {654, 4779}}
Native stack trace:
	0   CoreFoundation                      0x00000001888b18dc __exceptionPreprocess + 176
	1   libobjc.A.dylib                     0x000000018838a418 objc_exception_throw + 88
	2   Foundation                          0x000000018a9f53cc -[NSMutableDictionary(NSMutableDictionary) initWithContentsOfFile:] + 0
	3   AppKit                              0x000000018dc6d91c _NSPinnedFrameForFrameWithContainerFrameVisibleFrame + 1288
	4   AppKit                              0x000000018dc734dc _NSPinnedNonOverlappingFramesForContentFrameVisibleFrame + 284
	5   AppKit                              0x000000018d55655c -[_NSCollectionLayoutAuxiliaryItemSolver _solveForPinning:visibleRect:] + 1604
	6   AppKit                              0x000000018d62c2ac -[_NSCollectionCompositionalLayoutSolver updatePinnedSectionSupplementaryItemsForVisibleBounds:] + 432
	7   AppKit                              0x000000018d9d9d10 -[NSCollectionViewCompositionalLayout _updatePinnedSectionSupplementaryItemsForCurrentVisibleBounds] + 112
	8   AppKit                              0x000000018d9d73a4 -[NSCollectionViewCompositionalLayout invalidateLayoutWithContext:] + 504
	9   AppKit                              0x000000018d7880e0 -[NSCollectionViewLayout invalidateLayout] + 68
	10  AppKit                              0x000000018d78bd90 -[NSCollectionViewLayout _invalidateLayoutUsingContext:] + 60
	11  AppKit                              0x000000018d8f622c -[_NSCollectionViewCore setBounds:] + 572
	12  AppKit                              0x000000018daa0114 -[NSCollectionView _clipViewFrameChanged:] + 396
	13  CoreFoundation                      0x000000018885b484 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 148
	14  CoreFoundation                      0x00000001888bff34 ___CFXRegistrationPost_block_invoke + 92
	15  CoreFoundation                      0x00000001888bfe78 _CFXRegistrationPost + 436
	16  CoreFoundation                      0x0000000188839f9c _CFXNotificationPost + 740
	17  AppKit                              0x000000018cc8635c -[NSView _postFrameChangeNotification] + 240
	18  AppKit                              0x000000018d92628c -[NSView setFrameSize:] + 1472
	19  AppKit                              0x000000018d27f6f4 -[NSClipView setFrameSize:] + 176
	20  AppKit                              0x000000018d9265c8 -[NSView setFrame:] + 300
	21  AppKit                              0x000000018da86034 -[NSScrollView _setContentViewFrame:] + 248
	22  AppKit                              0x000000018da86314 -[NSScrollView _applyContentAreaLayout:] + 444
	23  AppKit                              0x000000018cca79e4 -[NSScrollView tile] + 480
	24  AppKit                              0x000000018cca77d8 -[NSScrollView _tileWithoutRecursing] + 52
	25  AppKit                              0x000000018ccdb868 -[NSScrollView _update] + 24
	26  AppKit                              0x000000018d926080 -[NSView setFrameSize:] + 948
	27  AppKit                              0x000000018da88574 -[NSScrollView setFrameSize:] + 200
	28  AppKit                              0x000000018d9265c8 -[NSView setFrame:] + 300
	29  AppKit                              0x000000018d925714 -[NSView resizeWithOldSuperviewSize:] + 488
	30  AppKit                              0x000000018d925304 -[NSView resizeSubviewsWithOldSize:] + 360
	31  AppKit                              0x000000018d926080 -[NSView setFrameSize:] + 948
	32  AppKit                              0x000000018d757014 -[NSTabView setFrameSize:] + 88
	33  AppKit                              0x000000018d9265c8 -[NSView setFrame:] + 300
	34  AppKit                              0x000000018d925714 -[NSView resizeWithOldSuperviewSize:] + 488
	35  AppKit                              0x000000018d925304 -[NSView resizeSubviewsWithOldSize:] + 360
	36  AppKit                              0x000000018d926080 -[NSView setFrameSize:] + 948
	37  AppKit                              0x000000018d9265c8 -[NSView setFrame:] + 300
	38  AppKit                              0x000000018dcdee60 -[NSPageController _setupTransitionHierarchyWithSourceView:frame:destinationView:frame:forDirection:destinationValid:] + 1256
	39  AppKit                              0x000000018dcdb2b4 -[NSPageController _animateView:frame:toView:frame:direction:] + 248
	40  AppKit                              0x000000018dcdbd38 -[NSPageController _navigateToIndex:animated:] + 944
	41  AppKit                              0x000000018da606f0 +[NSAnimationContext runAnimationGroup:] + 56
	42  AppKit                              0x000000018da607a4 +[NSAnimationContext runAnimationGroup:completionHandler:] + 100
	43  AppKit                              0x000000018dcdbe70 -[NSPageController navigateBack:] + 260
NSCollectionLayoutBoundarySupplementaryItem background blur covering the entire layout section
 
 
Q