Skip to content

Instantly share code, notes, and snippets.

@steventroughtonsmith
Created February 22, 2021 23:04
Embed
What would you like to do?
UIView+Tooltips.m (Broadcasts)
//
// 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