Skip to content

Commit 9e570fd

Browse files
authored
Merge pull request #32 from TimOliver/ipados-mouse
Add iPadOS mouse pointer support
2 parents 9ee5bd7 + b8584f8 commit 9e570fd

7 files changed

Lines changed: 160 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
x.y.z Release Notes (yyyy-MM-dd)
22
=============================================================
33

4+
1.2.0 Release Notes (2022-01-23)
5+
=============================================================
6+
7+
### Enhancements
8+
9+
* Added iPadOS mouse pointer support.
10+
11+
### Fixed
12+
13+
* Tapping a segment may not have properly played the selection animation.
14+
415
1.1.0 Release Notes (2020-05-20)
516
=============================================================
617

TOSegmentedControl.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'TOSegmentedControl'
3-
s.version = '1.1.0'
3+
s.version = '1.2.0'
44
s.license = { :type => 'MIT', :file => 'LICENSE' }
55
s.summary = 'A segmented control in the style of iOS 13 compatible with previous versions of iOS.'
66
s.homepage = 'https://github.com/TimOliver/TOSegmentedControl'

TOSegmentedControl/Private/TOSegmentedControlSegment.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// TOSegmentedControlItem.h
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
@@ -55,6 +55,9 @@ NS_ASSUME_NONNULL_BEGIN
5555
/** Whether the item is selected or not. */
5656
@property (nonatomic, assign) BOOL isSelected;
5757

58+
/** A container view that wraps the item and arrow views */
59+
@property (nonatomic, strong) UIView *containerView;
60+
5861
/** The view (either image or label) for this item */
5962
@property (nonatomic, readonly) UIView *itemView;
6063

TOSegmentedControl/Private/TOSegmentedControlSegment.m

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// TOSegmentedControlItem.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
@@ -161,7 +161,10 @@ + (NSArray *)segmentsWithObjects:(NSArray *)objects forSegmentedControl:(nonnull
161161
#pragma mark - Set-up -
162162

163163
- (void)commonInit
164-
{
164+
{
165+
// Create the container view
166+
_containerView = [[UIView alloc] init];
167+
165168
// Create the initial image / label view
166169
[self refreshItemView];
167170

@@ -185,7 +188,7 @@ - (void)refreshReversibleView
185188
UIImage *arrow = self.segmentedControl.arrowImage;
186189
self.arrowView = [[UIView alloc] initWithFrame:(CGRect){CGPointZero, arrow.size}];
187190
self.arrowView.alpha = 0.0f;
188-
[self.segmentedControl.trackView addSubview:self.arrowView];
191+
[self.containerView addSubview:self.arrowView];
189192

190193
self.arrowImageView = [[UIImageView alloc] initWithImage:arrow];
191194
self.arrowImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
@@ -250,7 +253,7 @@ - (void)refreshItemView
250253
imageView = nil;
251254

252255
self.itemView = [self makeLabelForTitle:self.title];
253-
[self.segmentedControl.trackView addSubview:self.itemView];
256+
[self.containerView addSubview:self.itemView];
254257

255258
label = (UILabel *)self.itemView;
256259
}
@@ -261,13 +264,14 @@ - (void)refreshItemView
261264
label = nil;
262265

263266
self.itemView = [self makeImageViewForImage:self.image];
264-
[self.segmentedControl.trackView addSubview:self.itemView];
267+
[self.containerView addSubview:self.itemView];
265268

266269
imageView = (UIImageView *)self.itemView;
267270
}
268271

269272
// Update the label view
270273
label.textColor = self.segmentedControl.itemColor;
274+
271275
// Set the frame off the selected text as it is larger
272276
label.font = self.segmentedControl.selectedTextFont;
273277
[label sizeToFit];

TOSegmentedControl/TOSegmentedControl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// TOSegmentedControl.h
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

TOSegmentedControl/TOSegmentedControl.m

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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
@@ -43,7 +43,7 @@
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
// -----------------------------------------------

TOSegmentedControlExample.xcodeproj/project.pbxproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
22C93B05232AA79900A281CA /* TOSegmentedControl.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.objc; fileEncoding = 4; path = TOSegmentedControl.h; sourceTree = "<group>"; };
4444
22C93B07232ADBEA00A281CA /* TOSegmentedControlSegment.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOSegmentedControlSegment.h; sourceTree = "<group>"; };
4545
22C93B0D232ADFDD00A281CA /* TOSegmentedControlSegment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TOSegmentedControlSegment.m; sourceTree = "<group>"; };
46+
22D83E6E279D2F3700CF0D8E /* TOSegmentedControl.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = TOSegmentedControl.podspec; sourceTree = "<group>"; };
4647
22DF8D8522FF119C0051F319 /* TOSegmentedControlExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TOSegmentedControlExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
4748
22DF8D8822FF119C0051F319 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
4849
22DF8D8922FF119C0051F319 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@@ -127,6 +128,7 @@
127128
22DF8D8622FF119C0051F319 /* Products */,
128129
22E1FE3A232F7F0600CE1C92 /* README.md */,
129130
22E1FE3B232F7F0D00CE1C92 /* CHANGELOG.md */,
131+
22D83E6E279D2F3700CF0D8E /* TOSegmentedControl.podspec */,
130132
);
131133
sourceTree = "<group>";
132134
};
@@ -555,7 +557,7 @@
555557
buildSettings = {
556558
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
557559
CODE_SIGN_STYLE = Automatic;
558-
DEVELOPMENT_TEAM = "";
560+
DEVELOPMENT_TEAM = 6LF3GMKZAB;
559561
INFOPLIST_FILE = TOSegmentedControlExample/Info.plist;
560562
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
561563
LD_RUNPATH_SEARCH_PATHS = (
@@ -573,7 +575,7 @@
573575
buildSettings = {
574576
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
575577
CODE_SIGN_STYLE = Automatic;
576-
DEVELOPMENT_TEAM = "";
578+
DEVELOPMENT_TEAM = 6LF3GMKZAB;
577579
INFOPLIST_FILE = TOSegmentedControlExample/Info.plist;
578580
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
579581
LD_RUNPATH_SEARCH_PATHS = (

0 commit comments

Comments
 (0)