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

NSCollectionLayoutBoundarySupplementaryItem background blur covering the entire layout section
 
 
Q