11//
22// TOSegmentedControl.m
33//
4- // Copyright 2019 Timothy Oliver. All rights reserved.
4+ // Copyright 2019-2022 Timothy Oliver. All rights reserved.
55//
66// Permission is hereby granted, free of charge, to any person obtaining a copy
77// of this software and associated documentation files (the "Software"), to
4343// ----------------------------------------------------------------
4444// Private Members
4545
46- @interface TOSegmentedControl ()
46+ @interface TOSegmentedControl () <UIPointerInteractionDelegate>
4747
4848/* * The private list of item objects, storing state and view data */
4949@property (nonatomic , strong ) NSMutableArray <TOSegmentedControlSegment *> *segments;
@@ -81,6 +81,12 @@ @interface TOSegmentedControl ()
8181/* * Convenience property for testing if there are no segments */
8282@property (nonatomic , readonly ) BOOL hasNoSegments;
8383
84+ /* * Pointer interaction for mice interactions */
85+ #pragma clang diagnostic push
86+ #pragma clang diagnostic ignored "-Weverything"
87+ @property (nonatomic , strong ) UIPointerInteraction *pointerInteraction;
88+ #pragma clang diagnostic pop
89+
8490@end
8591
8692@implementation TOSegmentedControl
@@ -166,7 +172,16 @@ - (void)commonInit
166172 self.thumbShadowRadius = 3 .0f ;
167173 self.thumbShadowOffset = 2 .0f ;
168174 self.thumbShadowOpacity = 0 .13f ;
169-
175+
176+ // Set focused index to -1 to indicate nothing is focused
177+ self.focusedIndex = -1 ;
178+
179+ // Configure indirect pointing support
180+ if (@available (iOS 13.4 , *)) {
181+ self.pointerInteraction = [[UIPointerInteraction alloc ] initWithDelegate: self ];
182+ [self addInteraction: self .pointerInteraction];
183+ }
184+
170185 // Configure view interaction
171186 // When the user taps down in the view
172187 [self addTarget: self
@@ -398,6 +413,7 @@ - (void)removeSegmentAtIndex:(NSInteger)index
398413 _items = items;
399414
400415 // Remove item object
416+ [self .segments[index].containerView removeFromSuperview ];
401417 [self .segments removeObjectAtIndex: index];
402418
403419 // Update number of separators
@@ -407,6 +423,9 @@ - (void)removeSegmentAtIndex:(NSInteger)index
407423- (void )removeAllSegments
408424{
409425 // Remove all item objects
426+ for (TOSegmentedControlSegment * segment in self.segments ) {
427+ [segment.containerView removeFromSuperview ];
428+ }
410429 self.segments = [NSMutableArray array ];
411430
412431 // Remove all separators
@@ -526,11 +545,12 @@ - (void)layoutItemViews
526545 for (TOSegmentedControlSegment *item in self.segments ) {
527546 UIView *itemView = item.itemView ;
528547 [itemView sizeToFit ];
529- [self .trackView addSubview: itemView ];
548+ [self .trackView addSubview: item.containerView ];
530549
531550 // Get the container frame that the item will be aligned with
532551 CGRect thumbFrame = [self frameForSegmentAtIndex: i];
533-
552+ item.containerView .frame = thumbFrame;
553+
534554 // Work out the appropriate size of the item
535555 CGRect itemFrame = itemView.frame ;
536556
@@ -545,8 +565,8 @@ - (void)layoutItemViews
545565 }
546566
547567 // Center the item in the container
548- itemFrame.origin .x = CGRectGetMidX ( thumbFrame) - ( itemFrame.size .width * 0 .5f ) ;
549- itemFrame.origin .y = CGRectGetMidY ( thumbFrame) - ( itemFrame.size .height * 0 .5f ) ;
568+ itemFrame.origin .x = ( CGRectGetWidth ( thumbFrame) - itemFrame.size .width ) * 0 .5f ;
569+ itemFrame.origin .y = ( CGRectGetHeight ( thumbFrame) - itemFrame.size .height ) * 0 .5f ;
550570
551571 // Set the item frame
552572 itemView.frame = CGRectIntegral (itemFrame);
@@ -747,6 +767,11 @@ - (void)setItemAtIndex:(NSInteger)index faded:(BOOL)faded
747767}
748768
749769- (void )refreshSeparatorViewsForSelectedIndex : (NSInteger )index
770+ {
771+ [self refreshSeparatorViewsForSelectedIndexes: [NSSet setWithObject: @(index)]];
772+ }
773+
774+ - (void )refreshSeparatorViewsForSelectedIndexes : (NSSet <NSNumber *> *)indexes
750775{
751776 // Hide the separators on either side of the selected segment
752777 NSInteger i = 0 ;
@@ -757,7 +782,9 @@ - (void)refreshSeparatorViewsForSelectedIndex:(NSInteger)index
757782 continue ;
758783 }
759784
760- separatorView.alpha = (i == index || i == (index - 1 )) ? 0 .0f : 1 .0f ;
785+ // Hide the index (right side) and the previous index (left side) if it's in the set
786+ BOOL containsIndex = ([indexes containsObject: @(i)] || [indexes containsObject: @(i+1 )]);
787+ separatorView.alpha = containsIndex ? 0 .0f : 1 .0f ;
761788 i++;
762789 }
763790}
@@ -787,6 +814,11 @@ - (void)didTapDown:(UIControl *)control withEvent:(UIEvent *)event
787814 // Track the currently selected item as the focused one
788815 self.focusedIndex = tappedIndex;
789816
817+ // On iOS 13.4, update the point interaction
818+ if (@available (iOS 13.4 , *)) {
819+ [self .pointerInteraction invalidate ];
820+ }
821+
790822 // Work out which animation effects to apply
791823 if (!self.isDraggingThumbView ) {
792824 [UIView animateWithDuration: 0 .35f animations: ^{
@@ -819,7 +851,12 @@ - (void)didDragTap:(UIControl *)control withEvent:(UIEvent *)event
819851
820852 CGPoint tapPoint = [event.allTouches.anyObject locationInView: self ];
821853 NSInteger tappedIndex = [self segmentIndexForPoint: tapPoint];
822-
854+
855+ // On iOS 13.4, update the point interaction
856+ if (@available (iOS 13.4 , *)) {
857+ [self .pointerInteraction invalidate ];
858+ }
859+
823860 if (tappedIndex == self.focusedIndex ) {
824861 return ;
825862 }
@@ -965,6 +1002,11 @@ - (void)didEndTap:(UIControl *)control withEvent:(UIEvent *)event
9651002 // Reset the focused index flag
9661003 self.focusedIndex = -1 ;
9671004
1005+ // On iOS 13.4, update the point interaction
1006+ if (@available (iOS 13.4 , *)) {
1007+ [self .pointerInteraction invalidate ];
1008+ }
1009+
9681010 return ;
9691011 }
9701012
@@ -980,6 +1022,11 @@ - (void)didEndTap:(UIControl *)control withEvent:(UIEvent *)event
9801022 [self sendIndexChangedEventActions ];
9811023 }
9821024
1025+ // On iOS 13.4, update the point interaction
1026+ if (@available (iOS 13.4 , *)) {
1027+ [self .pointerInteraction invalidate ];
1028+ }
1029+
9831030 // Work out which animation effects to apply
9841031 id animationBlock = ^{
9851032 [self setThumbViewShrunken: NO ];
@@ -1013,6 +1060,80 @@ - (void)sendIndexChangedEventActions
10131060 }
10141061}
10151062
1063+ #pragma mark - Pointer Interaction -
1064+
1065+ - (UIPointerRegion *)pointerInteraction : (UIPointerInteraction *)interaction
1066+ regionForRequest : (UIPointerRegionRequest *)request
1067+ defaultRegion : (UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4 ))
1068+ {
1069+ CGRect frame = defaultRegion.rect ;
1070+
1071+ // Determine which segment the pointer is in
1072+ CGFloat segmentWidth = frame.size .width / self.segments .count ;
1073+ CGPoint location = request.location ;
1074+ NSInteger segment = floorf (location.x / segmentWidth);
1075+
1076+ // Define the selectable region for this segment
1077+ frame.origin .x = segmentWidth * segment;
1078+ frame.size .width = segmentWidth;
1079+
1080+ // Return the region and an identifier for the segment
1081+ return [UIPointerRegion regionWithRect: frame identifier: @(segment)];
1082+ }
1083+
1084+ - (UIPointerStyle *)pointerInteraction : (UIPointerInteraction *)interaction
1085+ styleForRegion : (UIPointerRegion *)region API_AVAILABLE(ios(13.4 ))
1086+ {
1087+ // Fetch which segment was selected
1088+ NSInteger segment = [(NSNumber *)region.identifier integerValue ];
1089+
1090+ // Fetch the frame size of the segment
1091+ CGRect frame = [self frameForSegmentAtIndex: segment];
1092+
1093+ // Create a preview link to the container view containing the item and arrow
1094+ UITargetedPreview *preview = [[UITargetedPreview alloc ] initWithView: [self .segments[segment] containerView ]];
1095+
1096+ // Define the selection shape as the size of the segment with the thumb's corner radius
1097+ UIPointerShape *shape = [UIPointerShape shapeWithRoundedRect: frame
1098+ cornerRadius: self .thumbView.layer.cornerRadius];
1099+
1100+ // For selected segments
1101+ UIPointerEffect *effect = nil ;
1102+ if (segment == self.selectedSegmentIndex ) {
1103+ effect = [UIPointerLiftEffect effectWithPreview: preview];
1104+ } else { // Un-selected segments
1105+ effect = [UIPointerHighlightEffect effectWithPreview: preview];
1106+ }
1107+
1108+ // Return the final generated shape
1109+ return [UIPointerStyle styleWithEffect: effect shape: shape];
1110+ }
1111+
1112+ - (void )pointerInteraction : (UIPointerInteraction *)interaction
1113+ willEnterRegion : (UIPointerRegion *)region
1114+ animator : (id <UIPointerInteractionAnimating>)animator API_AVAILABLE(ios(13.4 ))
1115+ {
1116+ NSInteger segment = [(NSNumber *)region.identifier integerValue ];
1117+ NSSet *selectedSegments = [NSSet setWithArray: @[@(segment), @(self .selectedSegmentIndex)]];
1118+
1119+ // Animate the separator views fading in and out to match
1120+ [animator addAnimations: ^{
1121+ [self refreshSeparatorViewsForSelectedIndexes: selectedSegments];
1122+ }];
1123+ }
1124+
1125+ - (void )pointerInteraction : (UIPointerInteraction *)interaction
1126+ willExitRegion : (UIPointerRegion *)region
1127+ animator : (id <UIPointerInteractionAnimating>)animator API_AVAILABLE(ios(13.4 ))
1128+ {
1129+ // Restore the thumb view
1130+ self.trackView .clipsToBounds = YES ;
1131+
1132+ [animator addAnimations: ^{
1133+ [self refreshSeparatorViewsForSelectedIndex: self .selectedSegmentIndex];
1134+ }];
1135+ }
1136+
10161137#pragma mark - Accessors -
10171138
10181139// -----------------------------------------------
0 commit comments