UIView+Tooltips.m (Broadcasts)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // UIView+Tooltips.m | |
| // Crossword | |
| // | |
| // Created by Steven Troughton-Smith on 13/09/2019. | |
| // Copyright © 2019 Steven Troughton-Smith. All rights reserved. | |
| // | |
| #import "UIView+Tooltips.h" | |
| #import <objc/runtime.h> | |
| /* | |
| WARNING: | |
| USE_APPKIT_TOOLTIPS enables the use of private AppKit API. This may break in the future | |
| */ | |
| #define USE_APPKIT_TOOLTIPS (TARGET_OS_MACCATALYST) | |
| #define SIMULATE_TOOLTIPS_ON_LONGPRESS 0 | |
| #if USE_APPKIT_TOOLTIPS | |
| @interface NSObject (AppKitToolTipManager) | |
| +(id)sharedToolTipManager; | |
| -(void)_displayTemporaryToolTipForView:(id)nsView withString:(NSString *)string; | |
| - (id)nsWindow; | |
| -(id)contentView; | |
| -(void)orderOutToolTipsImmediately:(BOOL)b; | |
| @end | |
| #endif | |
| #if !TARGET_OS_TV | |
| @implementation UITooltipView | |
| + (instancetype)sharedInstance | |
| { | |
| static UITooltipView *sharedInstance = nil; | |
| static dispatch_once_t onceToken; // onceToken = 0 | |
| dispatch_once(&onceToken, ^{ | |
| sharedInstance = [[UITooltipView alloc] init]; | |
| }); | |
| return sharedInstance; | |
| } | |
| - (instancetype)init | |
| { | |
| self = [super init]; | |
| if (self) { | |
| UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial]; | |
| UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; | |
| blurView.bounds = self.bounds; | |
| blurView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; | |
| [self addSubview:blurView]; | |
| self.tooltipLabel = [[UILabel alloc] init]; | |
| self.tooltipLabel.text = @""; | |
| self.tooltipLabel.font = [UIFont systemFontOfSize:14]; | |
| self.tooltipLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; | |
| self.tooltipLabel.lineBreakMode = NSLineBreakByClipping; | |
| [[blurView contentView] addSubview:self.tooltipLabel]; | |
| self.layer.borderWidth = 1.0/[UIScreen mainScreen].scale; | |
| self.layer.borderColor = [UIColor secondarySystemFillColor].CGColor; | |
| self.layer.shadowColor = [UIColor blackColor].CGColor; | |
| self.layer.shadowRadius = 6; | |
| self.layer.shadowOffset = CGSizeMake(0, 0); | |
| self.layer.shadowOpacity = 0.3; | |
| } | |
| return self; | |
| } | |
| -(void)_sizeToFit | |
| { | |
| [self.tooltipLabel sizeToFit]; | |
| self.frame = CGRectInset(self.tooltipLabel.bounds, -5, -3); | |
| self.tooltipLabel.frame = CGRectInset(self.bounds, 5, 3); | |
| } | |
| -(CGRect)displayRectForViewRect:(CGRect)convertedRect | |
| { | |
| CGRect labelRect = self.bounds; | |
| return CGRectMake(convertedRect.origin.x+convertedRect.size.width/2-labelRect.size.width/2, convertedRect.origin.y+convertedRect.size.height, self.frame.size.width, self.frame.size.height); | |
| } | |
| -(void)showFromRect:(CGRect)convertedRect | |
| { | |
| #if USE_APPKIT_TOOLTIPS | |
| id nsWindow = [self.window nsWindow]; | |
| if (NSClassFromString(@"NSToolTipManager") && [NSClassFromString(@"NSToolTipManager") respondsToSelector:@selector(sharedToolTipManager)]) | |
| { | |
| id manager = [NSClassFromString(@"NSToolTipManager") sharedToolTipManager]; | |
| if (manager && [manager respondsToSelector:@selector(_displayTemporaryToolTipForView:withString:)]) | |
| { | |
| [manager _displayTemporaryToolTipForView:[nsWindow contentView] withString:self.tooltipLabel.text]; | |
| } | |
| } | |
| self.visible = YES; | |
| #else | |
| [CATransaction setDisableActions:YES]; | |
| [self _sizeToFit]; | |
| CGRect labelRect = self.bounds; | |
| self.alpha = 0; | |
| CGRect safeRect = CGRectMake(convertedRect.origin.x+convertedRect.size.width/2-labelRect.size.width/2, convertedRect.origin.y+convertedRect.size.height, self.frame.size.width, self.frame.size.height); | |
| CGFloat edgePadding = 8; | |
| if (safeRect.origin.x+safeRect.size.width > self.window.bounds.size.width) | |
| safeRect.origin.x = self.window.bounds.size.width - edgePadding - safeRect.size.width; | |
| if (safeRect.origin.x < 0) | |
| safeRect.origin.x = edgePadding; | |
| self.frame = safeRect; | |
| [CATransaction setDisableActions:NO]; | |
| self.visible = YES; | |
| [UIView animateWithDuration:0.1 animations:^{ | |
| self.alpha = 1; | |
| } completion:nil]; | |
| #endif | |
| } | |
| -(void)hide | |
| { | |
| #if USE_APPKIT_TOOLTIPS | |
| if (NSClassFromString(@"NSToolTipManager") && [NSClassFromString(@"NSToolTipManager") respondsToSelector:@selector(sharedToolTipManager)]) | |
| { | |
| id manager = [NSClassFromString(@"NSToolTipManager") sharedToolTipManager]; | |
| if (manager && [manager respondsToSelector:@selector(orderOutToolTipsImmediately:)]) | |
| { | |
| [manager orderOutToolTipsImmediately:YES]; | |
| } | |
| } | |
| self.visible = NO; | |
| #else | |
| [UIView animateWithDuration:0.1 animations:^{ | |
| self.alpha = 0; | |
| } completion:^(BOOL finished) { | |
| self.visible = NO; | |
| }]; | |
| #endif | |
| } | |
| @end | |
| #endif | |
| @implementation UIView (Tooltips) | |
| #pragma mark - Properties | |
| -(void)set_tooltipMouseOver:(BOOL)tooltipMouseOver | |
| { | |
| objc_setAssociatedObject(self, @selector(_tooltipMouseOver), @(tooltipMouseOver), OBJC_ASSOCIATION_ASSIGN); | |
| } | |
| - (BOOL)_tooltipMouseOver | |
| { | |
| return [objc_getAssociatedObject(self, @selector(_tooltipMouseOver)) boolValue]; | |
| } | |
| #if !TARGET_OS_TV | |
| -(void)set_tooltipRecognizer:(UIHoverGestureRecognizer *)obj | |
| { | |
| objc_setAssociatedObject(self, @selector(_tooltipRecognizer), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
| } | |
| - (UIHoverGestureRecognizer *)_tooltipRecognizer | |
| { | |
| return objc_getAssociatedObject(self, @selector(_tooltipRecognizer)); | |
| } | |
| #endif | |
| -(void)setToolTip:(NSString *)obj | |
| { | |
| objc_setAssociatedObject(self, @selector(toolTip), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
| #if !TARGET_OS_TV | |
| if (!self._tooltipRecognizer && obj && [obj length]) | |
| [self _installTooltipRecognizer]; | |
| #endif | |
| } | |
| - (NSString *)toolTip | |
| { | |
| return objc_getAssociatedObject(self, @selector(toolTip)); | |
| } | |
| -(void)setToolTipRect:(CGRect)obj | |
| { | |
| objc_setAssociatedObject(self, @selector(toolTipRect), @(obj), OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
| } | |
| - (CGRect )toolTipRect | |
| { | |
| NSValue *possibleRect = objc_getAssociatedObject(self, @selector(toolTipRect)); | |
| if (possibleRect) | |
| return [possibleRect CGRectValue]; | |
| return self.bounds; | |
| } | |
| -(void)setLastMouseLocation:(CGPoint)obj | |
| { | |
| objc_setAssociatedObject(self, @selector(lastMouseLocation), @(obj), OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
| } | |
| - (CGPoint )lastMouseLocation | |
| { | |
| NSValue *possibleLocation = objc_getAssociatedObject(self, @selector(lastMouseLocation)); | |
| if (possibleLocation) | |
| return [possibleLocation CGPointValue]; | |
| return CGPointZero; | |
| } | |
| #pragma mark - Recognizer | |
| -(void)_installTooltipRecognizer | |
| { | |
| #if !TARGET_OS_TV | |
| self._tooltipRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(tooltipHover:)]; | |
| if (![[self class] isKindOfClass:[UIButton class]] || ([[self class] isKindOfClass:[UIButton class]] && ((UIButton *)self).buttonType != UIButtonTypeCustom)) | |
| { | |
| [self addGestureRecognizer:self._tooltipRecognizer]; | |
| } | |
| #endif | |
| #if SIMULATE_TOOLTIPS_ON_LONGPRESS | |
| self._tooltipRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(tooltipHover:)]; | |
| [self addGestureRecognizer:self._tooltipRecognizer]; | |
| #endif | |
| } | |
| #if !TARGET_OS_TV | |
| -(void)_beginTooltipTimer | |
| { | |
| CGFloat time = 2.0; | |
| if ([UITooltipView sharedInstance].visible) | |
| time = 0.0; | |
| [NSTimer scheduledTimerWithTimeInterval:time repeats:NO block:^(NSTimer * _Nonnull timer) { | |
| if (self._tooltipMouseOver) | |
| { | |
| if (!CGRectContainsPoint(self.toolTipRect, self.lastMouseLocation)) | |
| { | |
| return; | |
| } | |
| CGRect convertedRect = [self convertRect:self.bounds toView:self.window]; | |
| [UITooltipView sharedInstance].tooltipLabel.text = self.toolTip; | |
| [self.window addSubview:[UITooltipView sharedInstance]]; | |
| [[UITooltipView sharedInstance] showFromRect:convertedRect]; | |
| } | |
| }]; | |
| } | |
| -(void)tooltipHover:(UIHoverGestureRecognizer *)recognizer | |
| { | |
| CGPoint p = [recognizer locationInView:self]; | |
| self.lastMouseLocation = p; | |
| switch (recognizer.state) | |
| { | |
| case UIGestureRecognizerStateBegan: | |
| { | |
| self._tooltipMouseOver = YES; | |
| [self _beginTooltipTimer]; | |
| break; | |
| } | |
| case UIGestureRecognizerStateChanged: | |
| { | |
| break; | |
| } | |
| case UIGestureRecognizerStateEnded: | |
| case UIGestureRecognizerStateCancelled: | |
| case UIGestureRecognizerStateFailed: | |
| { | |
| self._tooltipMouseOver = NO; | |
| [[UITooltipView sharedInstance] hide]; | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| } | |
| #endif | |
| @end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment