Implementing Section Borders with JJCollectionViewRoundFlowLayout

You configure section borders by returning a JJCollectionViewRoundConfigModel with borderWidth and borderColor values from the delegate method collectionView:layout:configModelForSectionAtIndex:, which the layout applies as native CALayer borders on the section's decoration view.

The JJCollectionViewRoundFlowLayout library (available at kingjiajie/jjcollectionviewroundflowlayout) extends UICollectionViewFlowLayout to draw rounded backgrounds with optional borders around entire sections. Unlike subclassing cells, this approach uses decoration views—separate from the cell hierarchy—to render visual containers, ensuring borders appear behind content and respond to layout changes automatically.

How the Border System Works

Borders in JJCollectionViewRoundFlowLayout are rendered as native CALayer properties on decoration views that sit behind each collection view section. The layout does not hardcode border values; instead, it queries your delegate for a configuration model per section.

The configuration model (JJCollectionViewRoundConfigModel) defines border appearance through two key properties defined in JJCollectionViewRoundConfigModel.h (lines 14‑19):

  • borderWidth: The width of the border line in points (CGFloat)
  • borderColor: The color of the border (UIColor)

When borderWidth is greater than zero, the layout creates a decoration view attribute that stores these values. The actual rendering occurs in JJCollectionReusableView (the decoration view class), which applies the values to its backing layer during applyLayoutAttributes:.

Configuring Borders via the Delegate Protocol

To enable borders, your view controller must conform to JJCollectionViewDelegateRoundFlowLayout and implement the required configuration method. The layout calls this method during prepareLayout to retrieve section-specific settings.

The delegate method signature is:

- (JJCollectionViewRoundConfigModel *)collectionView:(UICollectionView *)collectionView
                                              layout:(JJCollectionViewLayout *)collectionViewLayout
                       configModelForSectionAtIndex:(NSInteger)section;

Inside JJCollectionViewRoundFlowLayout.m (lines 495‑497), the layout checks for this implementation:

if ([delegate respondsToSelector:@selector(collectionView:layout:configModelForSectionAtIndex:)]) {
    attr.myConfigModel = [delegate collectionView:self.collectionView
                                            layout:self
                    configModelForSectionAtIndex:section];
}

If you return a model with default values (zero width), no border appears. To render a visible border, explicitly set the width and color properties:

#pragma mark - JJCollectionViewDelegateRoundFlowLayout

- (JJCollectionViewRoundConfigModel *)collectionView:(UICollectionView *)collectionView
                                              layout:(JJCollectionViewLayout *)layout
                       configModelForSectionAtIndex:(NSInteger)section {
    JJCollectionViewRoundConfigModel *model = [[JJCollectionViewRoundConfigModel alloc] init];
    
    // Background configuration
    model.backgroundColor = [UIColor systemBackgroundColor];
    model.cornerRadius = 12.0;
    
    // Border configuration
    model.borderWidth = 1.5;
    model.borderColor = [UIColor separatorColor];
    
    return model;
}

How Borders Are Rendered on Screen

The border is not drawn via Core Graphics in drawRect:; instead, it uses the hardware-accelerated CALayer border properties for optimal scrolling performance.

When the layout finishes calculations in prepareLayout, it creates JJCollectionViewRoundLayoutAttributes containing the myConfigModel. During layoutAttributesForElementsInRect:, these attributes are returned to the collection view, which instantiates or updates the corresponding JJCollectionReusableView decoration views.

In JJCollectionViewRoundFlowLayout.m (lines 106‑114), the layout attributes are applied to the view's layer:

// Inside JJCollectionReusableView (decoration view) implementation
view.layer.borderWidth = model.borderWidth;

if (@available(iOS 13.0, *)) {
    // Respect dark mode and trait collections automatically
    view.layer.borderColor = [model.borderColor resolvedColorWithTraitCollection:self.traitCollection].CGColor;
} else {
    view.layer.borderColor = model.borderColor.CGColor;
}

This implementation ensures that:

  • Dynamic colors (like UIColor.separatorColor) automatically adapt to Light/Dark mode on iOS 13+
  • Performance remains high because the system composites the border in the render server, not the CPU
  • Animation works seamlessly with collection view updates

Customizing Border Position with Edge Insets

By default, the border aligns with the section's edge insets calculated from the flow layout's sectionInset. To create a border that is inset relative to the background (or extends beyond it), implement the optional delegate method collectionView:layout:borderEdgeInsertsForSectionAtIndex:.

This method returns UIEdgeInsets that shrink or expand the decoration view's frame independently of the content inset:

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
                        layout:(JJCollectionViewLayout *)layout
borderEdgeInsertsForSectionAtIndex:(NSInteger)section {
    // Inset the border 8pt inside the section background
    return UIEdgeInsetsMake(8, 8, 8, 8);
}

Behind the scenes, JJCollectionViewRoundFlowLayout.m stores this value in the layout attributes (lines 390‑398) and applies it when calculating the final decoration view frame (lines 800‑808):

// Store the custom inset in the attribute
attr.borderEdgeInsets = userCustomSectionInset;

// Apply inset to the decoration view frame
sectionFrame.origin.x += userCustomSectionInset.left;
sectionFrame.origin.y += userCustomSectionInset.top;
sectionFrame.size.width -= (userCustomSectionInset.left + userCustomSectionInset.right);
sectionFrame.size.height -= (userCustomSectionInset.top + userCustomSectionInset.bottom);

Positive values shrink the border toward the center; negative values extend it outward beyond the background bounds.

Complete Implementation Example

Here is a complete view controller implementation that configures a collection view with rounded sections, background colors, and 1-point separators between sections:

#import "JJCollectionViewRoundFlowLayout.h"

@interface ViewController () <UICollectionViewDataSource, JJCollectionViewDelegateRoundFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    JJCollectionViewRoundFlowLayout *layout = [[JJCollectionViewRoundFlowLayout alloc] init];
    layout.delegate = self; // Required for border configuration
    
    self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds
                                           collectionViewLayout:layout];
    self.collectionView.dataSource = self;
    [self.view addSubview:self.collectionView];
}

#pragma mark - JJCollectionViewDelegateRoundFlowLayout

- (JJCollectionViewRoundConfigModel *)collectionView:(UICollectionView *)collectionView
                                              layout:(UICollectionViewLayout *)layout
                       configModelForSectionAtIndex:(NSInteger)section {
    JJCollectionViewRoundConfigModel *model = [[JJCollectionViewRoundConfigModel alloc] init];
    
    // Visual styling
    model.backgroundColor = [UIColor secondarySystemBackgroundColor];
    model.cornerRadius = 8.0;
    
    // Border styling
    model.borderWidth = 1.0;
    model.borderColor = [UIColor systemGray3Color];
    
    return model;
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
                        layout:(UICollectionViewLayout *)layout
borderEdgeInsertsForSectionAtIndex:(NSInteger)section {
    // Keep border 4pt inside the background edge
    return UIEdgeInsetsMake(4, 4, 4, 4);
}

#pragma mark - UICollectionViewDataSource

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 3;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return 10;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
                  cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell"
                                                                           forIndexPath:indexPath];
    cell.contentView.backgroundColor = [UIColor systemOrangeColor];
    return cell;
}

@end

Summary

  • Delegate Protocol: Conform to JJCollectionViewDelegateRoundFlowLayout and implement collectionView:layout:configModelForSectionAtIndex: to return configuration models.
  • Border Properties: Set borderWidth and borderColor on the JJCollectionViewRoundConfigModel instance to enable native CALayer borders.
  • iOS Version Support: The implementation automatically uses resolvedColorWithTraitCollection: on iOS 13+ to support dynamic colors, falling back to standard CGColor on earlier versions.
  • Positioning Control: Use collectionView:layout:borderEdgeInsertsForSectionAtIndex: to inset or outset the border relative to the section background, as calculated in JJCollectionViewRoundFlowLayout.m (lines 800‑808).
  • Performance: Borders render using hardware-accelerated layer properties, not CPU-bound drawing, maintaining smooth scrolling performance.

Frequently Asked Questions

How do I remove the border from a specific section while keeping it on others?

Return a JJCollectionViewRoundConfigModel with borderWidth set to 0.0 (or leave it at the default zero) for sections that should not display a border. The layout checks model.borderWidth before applying layer properties, so zero width effectively disables the border for that section while preserving the rounded background.

Does the border color automatically support Dark Mode?

Yes. The implementation in JJCollectionViewRoundFlowLayout.m uses resolvedColorWithTraitCollection: when running on iOS 13 or later, which resolves dynamic colors (like UIColor.separatorColor or UIColor.systemGrayColor) against the current trait collection before converting to CGColor. On iOS 12 and earlier, it falls back to the standard CGColor conversion, so static colors work as expected.

Why are my borders appearing clipped or outside the visible bounds?

This typically occurs when the borderEdgeInsets are negative (extending outward) but the collection view's bounds or contentInset do not provide sufficient space. Ensure your collection view has clipsToBounds disabled if you intend borders to extend visibly beyond cell boundaries, or verify that your borderEdgeInsertsForSectionAtIndex: implementation returns positive insets if you want the border contained within the section background.

Can I animate the border color or width when the collection view updates?

Yes. Because the border is rendered via standard CALayer properties, you can animate changes by calling reloadSections: or invalidating the layout with animation. For custom transitions, modify the model values and trigger invalidateLayout—the decoration view will pick up the new borderWidth and borderColor values and animate if wrapped in a UIView animation block, though precise control requires accessing the decoration view directly via the layout's supplementary view APIs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →