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