diff --git a/TORoundedButton/TORoundedButton.h b/TORoundedButton/TORoundedButton.h index 3c3c7b5..98a0e65 100644 --- a/TORoundedButton/TORoundedButton.h +++ b/TORoundedButton/TORoundedButton.h @@ -33,6 +33,14 @@ typedef NS_ENUM(NSInteger, TORoundedButtonBackgroundStyle) { TORoundedButtonBackgroundStyleGlass }; +/// The types of impacts that the button can generate when tapped by the user. +typedef NS_ENUM(NSInteger, TORoundedButtonImpactStyle) { + TORoundedButtonImpactStyleNone = -1, + TORoundedButtonImpactStyleLight = UIImpactFeedbackStyleLight, + TORoundedButtonImpactStyleMedium = UIImpactFeedbackStyleMedium, + TORoundedButtonImpactStyleHeavy = UIImpactFeedbackStyleHeavy +}; + NS_SWIFT_NAME(RoundedButtonDelegate) @protocol TORoundedButtonDelegate @@ -75,6 +83,9 @@ IB_DESIGNABLE @interface TORoundedButton : UIControl /// The style, whether static or dynamic of the button's background view. @property (nonatomic, assign) TORoundedButtonBackgroundStyle backgroundStyle; +/// The haptic impact style that this button will generate when tapped by the user. (Default is medium) +@property (nonatomic, assign) TORoundedButtonImpactStyle impactStyle; + /// When `backgroundStyle` is set to `.blur`, the specific blur style to apply. @property (nonatomic, assign) UIBlurEffectStyle blurStyle; diff --git a/TORoundedButton/TORoundedButton.m b/TORoundedButton/TORoundedButton.m index 72fdb05..fd6ec53 100644 --- a/TORoundedButton/TORoundedButton.m +++ b/TORoundedButton/TORoundedButton.m @@ -63,6 +63,9 @@ @implementation TORoundedButton { /** Maintain a reference to the corner configuration in case we swap out the background view */ UICornerConfiguration *_cornerConfiguration API_AVAILABLE(ios(26.0)); #endif + + /** The current haptic feedback generator that will play vibrations when the button is tapped. */ + UIImpactFeedbackGenerator *_impactGenerator; } #pragma mark - View Creation - @@ -118,6 +121,7 @@ - (void)_roundedButtonCommonInit TOROUNDEDBUTTON_OBJC_DIRECT { _tappedTintColorBrightnessOffset = !TORoundedButtonFloatIsZero(_tappedTintColorBrightnessOffset) ?: -0.15f; _contentInset = (UIEdgeInsets){15.0, 15.0, 15.0, 15.0}; _blurStyle = UIBlurEffectStyleDark; + _impactStyle = TORoundedButtonImpactStyleMedium; // Set the corner radius depending on system version #ifdef __IPHONE_26_0 @@ -150,7 +154,7 @@ - (void)_roundedButtonCommonInit TOROUNDEDBUTTON_OBJC_DIRECT { // Create action events for all possible interactions with this control [self addTarget:self action:@selector(_didTouchDownInside) forControlEvents:UIControlEventTouchDown|UIControlEventTouchDownRepeat]; - [self addTarget:self action:@selector(_didTouchUpInside) forControlEvents:UIControlEventTouchUpInside]; + [self addTarget:self action:@selector(_didTouchUpInside:event:) forControlEvents:UIControlEventTouchUpInside]; [self addTarget:self action:@selector(_didDragOutside) forControlEvents:UIControlEventTouchDragExit|UIControlEventTouchCancel]; [self addTarget:self action:@selector(_didDragInside) forControlEvents:UIControlEventTouchDragEnter]; } @@ -211,6 +215,19 @@ - (UIView *)_makeBackgroundViewWithStyle:(TORoundedButtonBackgroundStyle)style T return backgroundView; } +- (UIImpactFeedbackGenerator *)_makeImpactGeneratorWithStyle:(TORoundedButtonImpactStyle)style TOROUNDEDBUTTON_OBJC_DIRECT { + if (style == TORoundedButtonImpactStyleNone) { + return nil; + } + + const UIImpactFeedbackStyle impactStyle = (UIImpactFeedbackStyle)style; + if (@available(iOS 17.5, *)) { + return [UIImpactFeedbackGenerator feedbackGeneratorWithStyle:impactStyle forView:self]; + } + + return [[UIImpactFeedbackGenerator alloc] initWithStyle:impactStyle]; +} + #pragma mark - View Lifecycle - - (void)didMoveToSuperview { @@ -222,6 +239,11 @@ - (void)didMoveToSuperview { // Defer making the background until we're added to the subview in case the user changes it _backgroundView = [self _makeBackgroundViewWithStyle:_backgroundStyle]; [_containerView insertSubview:_backgroundView atIndex:0]; + + // If the button has been configured to set up an impact generator, create it now. + if (_impactStyle != TORoundedButtonImpactStyleNone && !_impactGenerator) { + _impactGenerator = [self _makeImpactGeneratorWithStyle:_impactStyle]; + } } - (void)tintColorDidChange { @@ -308,15 +330,26 @@ - (CGSize)sizeThatFits:(CGSize)size { - (void)_didTouchDownInside { _isTapped = YES; + // Preheat the impact generator to prepare playing it when we tap up. + [_impactGenerator prepare]; + // The user touched their finger down into the button bounds [self _setLabelAlphaTappedAnimated:NO]; [self _setBackgroundColorTappedAnimated:YES]; [self _setButtonScaledTappedAnimated:YES]; } -- (void)_didTouchUpInside { +- (void)_didTouchUpInside:(id)sender event:(UIEvent *)event { _isTapped = NO; + // Play the impact to lock in that the user committed to this action. + if (@available(iOS 17.5, *)) { + const CGPoint touchPoint = [event.allTouches.anyObject locationInView:self]; + [_impactGenerator impactOccurredAtLocation:touchPoint]; + } else { + [_impactGenerator impactOccurred]; + } + // The user lifted their finger up from inside the button bounds [self _setLabelAlphaTappedAnimated:YES]; [self _setBackgroundColorTappedAnimated:YES]; @@ -585,6 +618,12 @@ - (void)setEnabled:(BOOL)enabled { _containerView.alpha = enabled ? 1 : 0.4; } +- (void)setImpactStyle:(TORoundedButtonImpactStyle)impactStyle { + if (_impactStyle == impactStyle) { return; } + _impactStyle = impactStyle; + _impactGenerator = [self _makeImpactGeneratorWithStyle:impactStyle]; +} + #pragma mark - Private - - (void)_setBackgroundTintColor:(UIColor *)tintColor TOROUNDEDBUTTON_OBJC_DIRECT {