FLEXScopeCarousel.m 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. //
  2. // FLEXScopeCarousel.m
  3. // FLEX
  4. //
  5. // Created by Tanner Bennett on 7/17/19.
  6. // Copyright © 2019 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXScopeCarousel.h"
  9. #import "FLEXCarouselCell.h"
  10. #import "FLEXColor.h"
  11. #import "UIView+Layout.h"
  12. const CGFloat kCarouselItemSpacing = 0;
  13. NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
  14. @interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
  15. @property (nonatomic, readonly) UICollectionView *collectionView;
  16. @property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
  17. @property (nonatomic, readonly) NSLayoutConstraint *heightConstraint;
  18. @property (nonatomic, readonly) id dynamicTypeObserver;
  19. @property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
  20. @property (nonatomic) BOOL constraintsInstalled;
  21. @end
  22. @implementation FLEXScopeCarousel
  23. - (id)initWithFrame:(CGRect)frame {
  24. self = [super initWithFrame:frame];
  25. if (self) {
  26. self.backgroundColor = [FLEXColor primaryBackgroundColor];
  27. self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  28. _dynamicTypeHandlers = [NSMutableArray new];
  29. // Collection view layout
  30. UICollectionViewFlowLayout *layout = ({
  31. UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
  32. layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  33. layout.sectionInset = UIEdgeInsetsZero;
  34. layout.minimumLineSpacing = kCarouselItemSpacing;
  35. layout.itemSize = UICollectionViewFlowLayoutAutomaticSize;
  36. layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize;
  37. layout;
  38. });
  39. // Collection view
  40. _collectionView = ({
  41. UICollectionView *cv = [[UICollectionView alloc]
  42. initWithFrame:CGRectZero
  43. collectionViewLayout:layout
  44. ];
  45. cv.showsHorizontalScrollIndicator = NO;
  46. cv.backgroundColor = UIColor.clearColor;
  47. cv.delegate = self;
  48. cv.dataSource = self;
  49. [cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
  50. [self addSubview:cv];
  51. cv;
  52. });
  53. // Sizing cell
  54. _sizingCell = [FLEXCarouselCell new];
  55. self.sizingCell.title = @"NSObject";
  56. // Dynamic type
  57. __weak __typeof(self) weakSelf = self;
  58. _dynamicTypeObserver = [NSNotificationCenter.defaultCenter
  59. addObserverForName:UIContentSizeCategoryDidChangeNotification
  60. object:nil queue:nil usingBlock:^(NSNotification *note) {
  61. [self.collectionView setNeedsLayout];
  62. [self setNeedsUpdateConstraints];
  63. // Notify observers
  64. __typeof(self) self = weakSelf;
  65. for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
  66. block(self);
  67. }
  68. }
  69. ];
  70. }
  71. return self;
  72. }
  73. - (void)dealloc {
  74. [NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver];
  75. }
  76. #pragma mark - Overrides
  77. - (void)drawRect:(CGRect)rect {
  78. [super drawRect:rect];
  79. CGFloat width = 1.f / UIScreen.mainScreen.scale;
  80. // Draw hairline
  81. CGContextRef context = UIGraphicsGetCurrentContext();
  82. CGContextSetStrokeColorWithColor(context, [FLEXColor hairlineColor].CGColor);
  83. CGContextSetLineWidth(context, width);
  84. CGContextMoveToPoint(context, 0, rect.size.height - width);
  85. CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
  86. CGContextStrokePath(context);
  87. }
  88. + (BOOL)requiresConstraintBasedLayout {
  89. return YES;
  90. }
  91. - (void)updateConstraints {
  92. if (!self.constraintsInstalled) {
  93. self.translatesAutoresizingMaskIntoConstraints = NO;
  94. self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
  95. [self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor].active = YES;
  96. [self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor].active = YES;
  97. [self.topAnchor constraintEqualToAnchor:self.superview.topAnchor].active = YES;
  98. [self.collectionView pinEdgesToSuperview];
  99. _heightConstraint = [self.heightAnchor constraintEqualToConstant:100];
  100. self.heightConstraint.active = YES;
  101. self.constraintsInstalled = YES;
  102. }
  103. self.heightConstraint.constant = [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  104. [super updateConstraints];
  105. }
  106. #pragma mark - Public
  107. - (void)setItems:(NSArray<NSString *> *)items {
  108. NSParameterAssert(items.count);
  109. _items = items.copy;
  110. // Refresh list, select first item initially
  111. [self.collectionView reloadData];
  112. self.selectedIndex = 0;
  113. }
  114. - (void)setSelectedIndex:(NSInteger)idx {
  115. NSParameterAssert(idx < self.items.count);
  116. _selectedIndex = idx;
  117. NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
  118. [self.collectionView selectItemAtIndexPath:path
  119. animated:YES
  120. scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
  121. [self collectionView:self.collectionView didSelectItemAtIndexPath:path];
  122. }
  123. - (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
  124. [self.dynamicTypeHandlers addObject:handler];
  125. }
  126. #pragma mark - UICollectionView
  127. - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  128. self.sizingCell.title = self.items[indexPath.item];
  129. return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  130. }
  131. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  132. return self.items.count;
  133. }
  134. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
  135. cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  136. FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
  137. forIndexPath:indexPath];
  138. cell.title = self.items[indexPath.row];
  139. return cell;
  140. }
  141. - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
  142. _selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
  143. if (self.selectedIndexChangedAction) {
  144. self.selectedIndexChangedAction(indexPath.row);
  145. }
  146. // TODO: dynamically choose a scroll position. Very wide items should
  147. // get "Left" while smaller items should not scroll at all, unless
  148. // they are only partially on the screen, in which case they
  149. // should get "HorizontallyCentered" to bring them onto the screen.
  150. // For now, everything goes to the left, as this has a similar effect.
  151. [collectionView scrollToItemAtIndexPath:indexPath
  152. atScrollPosition:UICollectionViewScrollPositionLeft
  153. animated:YES];
  154. [self sendActionsForControlEvents:UIControlEventValueChanged];
  155. }
  156. @end