// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "ConversationViewLayout.h" #import "Session-Swift.h" #import "UIView+OWS.h" NS_ASSUME_NONNULL_BEGIN @interface ConversationViewLayout () @property (nonatomic) CGFloat lastViewWidth; @property (nonatomic) CGSize contentSize; @property (nonatomic, readonly) NSMutableDictionary *itemAttributesMap; // This dirty flag may be redundant with logic in UICollectionViewLayout, // but it can't hurt and it ensures that we can safely & cheaply call // prepareLayout from view logic to ensure that we always have a¸valid // layout without incurring any of the (great) expense of performing an // unnecessary layout pass. @property (nonatomic) BOOL hasLayout; @property (nonatomic) BOOL hasEverHadLayout; @end #pragma mark - @implementation ConversationViewLayout - (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle { if (self = [super init]) { _itemAttributesMap = [NSMutableDictionary new]; _conversationStyle = conversationStyle; } return self; } - (void)setHasLayout:(BOOL)hasLayout { _hasLayout = hasLayout; if (hasLayout) { self.hasEverHadLayout = YES; } } - (void)invalidateLayout { [super invalidateLayout]; [self clearState]; } - (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context { [super invalidateLayoutWithContext:context]; [self clearState]; } - (void)clearState { self.contentSize = CGSizeZero; [self.itemAttributesMap removeAllObjects]; self.hasLayout = NO; self.lastViewWidth = 0.f; } - (void)prepareLayout { [super prepareLayout]; id delegate = self.delegate; if (!delegate) { OWSFailDebug(@"Missing delegate"); [self clearState]; return; } if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) { OWSFailDebug(@"Collection view has invalid size: %@", NSStringFromCGRect(self.collectionView.bounds)); [self clearState]; return; } if (self.hasLayout) { return; } self.hasLayout = YES; [self prepareLayoutOfItems]; } - (void)prepareLayoutOfItems { const CGFloat viewWidth = self.conversationStyle.viewWidth; NSArray> *layoutItems = self.delegate.layoutItems; CGFloat y = self.conversationStyle.contentMarginTop + self.delegate.layoutHeaderHeight; CGFloat contentBottom = y; NSInteger row = 0; id _Nullable previousLayoutItem = nil; for (id layoutItem in layoutItems) { if (previousLayoutItem) { y += [layoutItem vSpacingWithPreviousLayoutItem:previousLayoutItem]; } CGSize layoutSize = CGSizeCeil([layoutItem cellSize]); // Ensure cell fits within view. OWSAssertDebug(layoutSize.width <= viewWidth); layoutSize.width = MIN(viewWidth, layoutSize.width); // All cells are "full width" and are responsible for aligning their own content. CGRect itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height); NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; itemAttributes.frame = itemFrame; self.itemAttributesMap[@(row)] = itemAttributes; contentBottom = itemFrame.origin.y + itemFrame.size.height; y = contentBottom; row++; previousLayoutItem = layoutItem; } contentBottom += self.conversationStyle.contentMarginBottom; self.contentSize = CGSizeMake(viewWidth, contentBottom); self.lastViewWidth = viewWidth; } - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { NSMutableArray *result = [NSMutableArray new]; for (UICollectionViewLayoutAttributes *itemAttributes in self.itemAttributesMap.allValues) { if (CGRectIntersectsRect(rect, itemAttributes.frame)) { [result addObject:itemAttributes]; } } return result; } - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { return self.itemAttributesMap[@(indexPath.row)]; } - (CGSize)collectionViewContentSize { return self.contentSize; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return self.lastViewWidth != newBounds.size.width; } @end NS_ASSUME_NONNULL_END