Question

Using a UICollectionViewController and my own UICollectionViewLayout subclass, I've put together a view that shows a Gannt style time chart. Using that framework to do the bands has been really easy.

What's not as clear as to me is how to design the backing view that gives time context. I sketched up an example in Inkscape:

Sample Time Series

The black rectangular outline is an example of where the user might currently be scrolled to.

So, I get how to do the pink/orange/yellow bands. What I'm less clear how to achieve is the background striping and the time labels.

One option that I've started on is to make a custom UIView subclass and set it as the backgroundView property of my collectionView. Doing the drawRect: to draw the vertical stripes would be easy.

The harder part is getting the time labels to show up always at the top/bottom of the current scroll area, rather than at the edges of the backgroundView. Is there a way to figure out what the current visible area of my background view is, rather than the frame/bounds which will be "full" view?

Or should I be somehow using the decoration features of the UICollectionView and UICollectionViewLayout. Didn't seem like a good fit for what I saw of the API there, but this is my first UICollectionView, so maybe I'm wrong?

UPDATE

I have learned 2 things since the original post, which leave me even more confused how to accomplish this than before:

  1. When Apple's docs say:

The view (if any) in this property is positioned underneath all of the other content and sized automatically to fill the entire bounds of the collection view.

They basically mean the screen of the device, not the actual extent of the view. I found with logging, that no matter the scrolling, the bounds\frame and drawRect: argument were always the size of the screen.

  1. I then thought maybe I could use the layoutAttributesForElementsInRect: argument. My thought was that I would fit UICollectionViewLayoutAttributes to the incoming argument. Unfortunately, this rectangle seems to often be larger than the viewable area. I assume they do some caching beyond the edges of the visible area.
Was it helpful?

Solution

Here's what I ended up doing. I actually ended up (ab?)using a decoration view. Here's how I did it, I was pretty pleased with the result actually:

I added a TimeBarsView class, as a subclass of UICollectionReusableView:

@interface TimeBarsView : UICollectionReusableView
@property (assign, nonatomic) NSInteger offset; // horizontal scroll offset
@end

And I use the the offset property to draw the background view as if it were scrolled to that position in its drawRect: method. The piece that glues that together is:

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    [super applyLayoutAttributes: layoutAttributes];
    self.offset = layoutAttributes.indexPath.item;
}

The trick here is that I'm using the the IndexPath of the background path to communicate its scroll position.

In my UIControllerViewLayout subclass, I implement the following methods:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

Called from initWithCoder and init:

- (void)registerTimeBars {
    [self registerClass:[TimeBarsView class] forDecorationViewOfKind:@"TimeBars"];
}

The usual layoutAttributesForElementsInRect: method with a preamble like this:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray *inRect = [NSMutableArray array];
    [inRect addObject:
        [self
            layoutAttributesForDecorationViewOfKind:@"TimeBars" // link to the TimeBars
            atIndexPath: [NSIndexPath
                indexPathForItem:self.collectionView.contentOffset.x // use the current scroll x value as the indexPath
                inSection:0]]];
    ... // other layouts for the normal item views like usual
    return inRect;
}

And finally

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
    CGPoint offset = self.collectionView.contentOffset;
    CGSize size = self.collectionView.bounds.size;
    // align current frame so it matches the current scroll box
    layoutAttributes.frame = CGRectMake(offset.x, offset.y, size.width, size.height);
    layoutAttributes.zIndex = -1; // make sure it's below other views
    return layoutAttributes;
}

OTHER TIPS

Should I be somehow using the decoration features…?

No, these are equivalent to headers/footers in a UITableView. They won't work for the background.

Honestly, I think you're overcomplicating it. I would just make two UICollectionViews - one for the yellow/orange/pink bars, and one underneath for the time bars. Disable user interaction on the bottom collection view. In the scrollViewDidScroll: delegate callback, set the content offset of the bottom collection view to match the content offset of the top collection view. Then the bottom one will scroll with the top one, and they will appear to work as one.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top