diff --git a/Fingertips.h b/Fingertips.h deleted file mode 100644 index 7285380..0000000 --- a/Fingertips.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Fingertips.h -// Fingertips -// -// Copyright © 2016 Mapbox. All rights reserved. -// - -#import - -//! Project version number for Fingertips. -FOUNDATION_EXPORT double FingertipsVersionNumber; - -//! Project version string for Fingertips. -FOUNDATION_EXPORT const unsigned char FingertipsVersionString[]; - -#import - - diff --git a/Fingertips.xcodeproj/project.pbxproj b/Fingertips.xcodeproj/project.pbxproj index a86915c..7345578 100644 --- a/Fingertips.xcodeproj/project.pbxproj +++ b/Fingertips.xcodeproj/project.pbxproj @@ -7,17 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - BE6B261A1CFA17AF001BFB6F /* Fingertips.h in Headers */ = {isa = PBXBuildFile; fileRef = BE6B26191CFA17AF001BFB6F /* Fingertips.h */; settings = {ATTRIBUTES = (Public, ); }; }; - BE6B26231CFB5636001BFB6F /* MBFingerTipWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = BE6B26211CFB5636001BFB6F /* MBFingerTipWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; - BE6B26241CFB5636001BFB6F /* MBFingerTipWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6B26221CFB5636001BFB6F /* MBFingerTipWindow.m */; }; + DCAD3F3622FC555A00C18B3A /* MBFingerTipWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD3F3522FC555A00C18B3A /* MBFingerTipWindow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ BE6B26161CFA17AF001BFB6F /* Fingertips.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Fingertips.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BE6B26191CFA17AF001BFB6F /* Fingertips.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Fingertips.h; sourceTree = ""; }; BE6B261B1CFA17AF001BFB6F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - BE6B26211CFB5636001BFB6F /* MBFingerTipWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBFingerTipWindow.h; sourceTree = ""; }; - BE6B26221CFB5636001BFB6F /* MBFingerTipWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBFingerTipWindow.m; sourceTree = ""; }; + DCAD3F3522FC555A00C18B3A /* MBFingerTipWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MBFingerTipWindow.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -50,9 +46,7 @@ BE6B26181CFA17AF001BFB6F /* Fingertips */ = { isa = PBXGroup; children = ( - BE6B26191CFA17AF001BFB6F /* Fingertips.h */, - BE6B26211CFB5636001BFB6F /* MBFingerTipWindow.h */, - BE6B26221CFB5636001BFB6F /* MBFingerTipWindow.m */, + DCAD3F3522FC555A00C18B3A /* MBFingerTipWindow.swift */, BE6B261B1CFA17AF001BFB6F /* Info.plist */, ); name = Fingertips; @@ -65,8 +59,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - BE6B26231CFB5636001BFB6F /* MBFingerTipWindow.h in Headers */, - BE6B261A1CFA17AF001BFB6F /* Fingertips.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -97,11 +89,12 @@ BE6B260D1CFA17AF001BFB6F /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0730; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = Mapbox; TargetAttributes = { BE6B26151CFA17AF001BFB6F = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1020; }; }; }; @@ -110,6 +103,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = BE6B260C1CFA17AF001BFB6F; @@ -137,7 +131,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BE6B26241CFB5636001BFB6F /* MBFingerTipWindow.m in Sources */, + DCAD3F3622FC555A00C18B3A /* MBFingerTipWindow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -148,18 +142,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -196,18 +201,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -227,6 +243,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -237,6 +254,8 @@ BE6B261F1CFA17AF001BFB6F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -247,12 +266,16 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mapbox.Fingertips; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; BE6B26201CFA17AF001BFB6F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -263,6 +286,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mapbox.Fingertips; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/Fingertips.xcodeproj/xcshareddata/xcschemes/Fingertips.xcscheme b/Fingertips.xcodeproj/xcshareddata/xcschemes/Fingertips.xcscheme index e4c44e4..50c23cc 100644 --- a/Fingertips.xcodeproj/xcshareddata/xcschemes/Fingertips.xcscheme +++ b/Fingertips.xcodeproj/xcshareddata/xcschemes/Fingertips.xcscheme @@ -1,6 +1,6 @@ - -/** A MBFingerTipWindow gives you automatic presentation mode in your iOS app. Note that currently, this is only designed for the iPad 2 and iPhone 4S (or later), which feature hardware video mirroring support. This library does not do the mirroring for you! -* -* Use MBFingerTipWindow in place of UIWindow and your app will automatically determine when an external screen is available. It will show every touch on-screen with a nice partially-transparent graphic that automatically fades out when the touch ends. */ -@interface MBFingerTipWindow : UIWindow - -/** A custom image to use to show touches on screen. If unset, defaults to a partially-transparent stroked circle. */ -@property (nonatomic, strong) UIImage *touchImage; - -/** The alpha transparency value to use for the touch image. Defaults to 0.5. */ -@property (nonatomic, assign) CGFloat touchAlpha; - -/** The time over which to fade out touch images. Defaults to 0.3. */ -@property (nonatomic, assign) NSTimeInterval fadeDuration; - -/** If using the default touchImage, the color with which to stroke the shape. Defaults to black. */ -@property (nonatomic, strong) UIColor *strokeColor; - -/** If using the default touchImage, the color with which to fill the shape. Defaults to white. */ -@property (nonatomic, strong) UIColor *fillColor; - -/** Sets whether touches should always show regardless of whether the display is mirroring. Defaults to NO. */ -@property (nonatomic, assign) BOOL alwaysShowTouches; - -@end diff --git a/MBFingerTipWindow.m b/MBFingerTipWindow.m deleted file mode 100644 index 130a265..0000000 --- a/MBFingerTipWindow.m +++ /dev/null @@ -1,401 +0,0 @@ -// -// MBFingerTipWindow.m -// -// Copyright 2011-2017 Mapbox, Inc. All rights reserved. -// - -#import "MBFingerTipWindow.h" - -// This file must be built with ARC. -// -#if !__has_feature(objc_arc) - #error "ARC must be enabled for MBFingerTipWindow.m" -#endif - -@interface MBFingerTipView : UIImageView - -@property (nonatomic, assign) NSTimeInterval timestamp; -@property (nonatomic, assign) BOOL shouldAutomaticallyRemoveAfterTimeout; -@property (nonatomic, assign, getter=isFadingOut) BOOL fadingOut; - -@end - -#pragma mark - - -@interface MBFingerTipOverlayWindow : UIWindow -@end - -#pragma mark - - -@interface MBFingerTipWindow () - -@property (nonatomic, strong) UIWindow *overlayWindow; -@property (nonatomic, assign) BOOL active; -@property (nonatomic, assign) BOOL fingerTipRemovalScheduled; - -- (void)MBFingerTipWindow_commonInit; -- (BOOL)anyScreenIsMirrored; -- (void)updateFingertipsAreActive; -- (void)scheduleFingerTipRemoval; -- (void)cancelScheduledFingerTipRemoval; -- (void)removeInactiveFingerTips; -- (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated; -- (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch; - -@end - -#pragma mark - - -@implementation MBFingerTipWindow - -@synthesize touchImage=_touchImage; - -- (id)initWithCoder:(NSCoder *)decoder -{ - // This covers NIB-loaded windows. - // - self = [super initWithCoder:decoder]; - - if (self != nil) - [self MBFingerTipWindow_commonInit]; - - return self; -} - -- (id)initWithFrame:(CGRect)rect -{ - // This covers programmatically-created windows. - // - self = [super initWithFrame:rect]; - - if (self != nil) - [self MBFingerTipWindow_commonInit]; - - return self; -} - -- (void)MBFingerTipWindow_commonInit -{ - self.strokeColor = [UIColor blackColor]; - self.fillColor = [UIColor whiteColor]; - - self.touchAlpha = 0.5; - self.fadeDuration = 0.3; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(screenConnect:) - name:UIScreenDidConnectNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(screenDisconnect:) - name:UIScreenDidDisconnectNotification - object:nil]; - - // Set up active now, in case the screen was present before the window was created (or application launched). - // - [self updateFingertipsAreActive]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenDidConnectNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenDidDisconnectNotification object:nil]; -} - -#pragma mark - - -- (UIWindow *)overlayWindow -{ - if ( ! _overlayWindow) - { - _overlayWindow = [[MBFingerTipOverlayWindow alloc] initWithFrame:self.frame]; - - _overlayWindow.userInteractionEnabled = NO; - _overlayWindow.windowLevel = UIWindowLevelStatusBar; - _overlayWindow.backgroundColor = [UIColor clearColor]; - _overlayWindow.hidden = NO; - } - - return _overlayWindow; -} - -- (UIImage *)touchImage -{ - if ( ! _touchImage) - { - UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50.0, 50.0)]; - - UIGraphicsBeginImageContextWithOptions(clipPath.bounds.size, NO, 0); - - UIBezierPath *drawPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(25.0, 25.0) - radius:22.0 - startAngle:0 - endAngle:2 * M_PI - clockwise:YES]; - - drawPath.lineWidth = 2.0; - - [self.strokeColor setStroke]; - [self.fillColor setFill]; - - [drawPath stroke]; - [drawPath fill]; - - [clipPath addClip]; - - _touchImage = UIGraphicsGetImageFromCurrentImageContext(); - - UIGraphicsEndImageContext(); - } - - return _touchImage; -} - -#pragma mark - Setter - -- (void)setAlwaysShowTouches:(BOOL)flag -{ - if (_alwaysShowTouches != flag) - { - _alwaysShowTouches = flag; - - [self updateFingertipsAreActive]; - } -} - -#pragma mark - -#pragma mark Screen notifications - -- (void)screenConnect:(NSNotification *)notification -{ - [self updateFingertipsAreActive]; -} - -- (void)screenDisconnect:(NSNotification *)notification -{ - [self updateFingertipsAreActive]; -} - -- (BOOL)anyScreenIsMirrored -{ - if ( ! [UIScreen instancesRespondToSelector:@selector(mirroredScreen)]) - return NO; - - for (UIScreen *screen in [UIScreen screens]) - { - if ([screen mirroredScreen] != nil) - return YES; - } - - return NO; -} - -- (void)updateFingertipsAreActive; -{ - if (self.alwaysShowTouches || ([[[[NSProcessInfo processInfo] environment] objectForKey:@"DEBUG_FINGERTIP_WINDOW"] boolValue])) - { - self.active = YES; - } - else - { - self.active = [self anyScreenIsMirrored]; - } -} - -#pragma mark - -#pragma mark UIWindow overrides - -- (void)sendEvent:(UIEvent *)event -{ - if (self.active) - { - NSSet *allTouches = [event allTouches]; - - for (UITouch *touch in [allTouches allObjects]) - { - switch (touch.phase) - { - case UITouchPhaseBegan: - case UITouchPhaseMoved: - case UITouchPhaseStationary: - { - MBFingerTipView *touchView = (MBFingerTipView *)[self.overlayWindow viewWithTag:touch.hash]; - - if (touch.phase != UITouchPhaseStationary && touchView != nil && [touchView isFadingOut]) - { - [touchView removeFromSuperview]; - touchView = nil; - } - - if (touchView == nil && touch.phase != UITouchPhaseStationary) - { - touchView = [[MBFingerTipView alloc] initWithImage:self.touchImage]; - [self.overlayWindow addSubview:touchView]; - } - - if ( ! [touchView isFadingOut]) - { - touchView.alpha = self.touchAlpha; - touchView.center = [touch locationInView:self.overlayWindow]; - touchView.tag = touch.hash; - touchView.timestamp = touch.timestamp; - touchView.shouldAutomaticallyRemoveAfterTimeout = [self shouldAutomaticallyRemoveFingerTipForTouch:touch]; - } - break; - } - - case UITouchPhaseEnded: - case UITouchPhaseCancelled: - { - [self removeFingerTipWithHash:touch.hash animated:YES]; - break; - } - } - } - } - - [super sendEvent:event]; - - [self scheduleFingerTipRemoval]; // We may not see all UITouchPhaseEnded/UITouchPhaseCancelled events. -} - -#pragma mark - -#pragma mark Private - -- (void)scheduleFingerTipRemoval -{ - if (self.fingerTipRemovalScheduled) - return; - - self.fingerTipRemovalScheduled = YES; - [self performSelector:@selector(removeInactiveFingerTips) withObject:nil afterDelay:0.1]; -} - -- (void)cancelScheduledFingerTipRemoval -{ - self.fingerTipRemovalScheduled = YES; - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(removeInactiveFingerTips) object:nil]; -} - -- (void)removeInactiveFingerTips -{ - self.fingerTipRemovalScheduled = NO; - - NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime]; - const CGFloat REMOVAL_DELAY = 0.2; - - for (MBFingerTipView *touchView in [self.overlayWindow subviews]) - { - if ( ! [touchView isKindOfClass:[MBFingerTipView class]]) - continue; - - if (touchView.shouldAutomaticallyRemoveAfterTimeout && now > touchView.timestamp + REMOVAL_DELAY) - [self removeFingerTipWithHash:touchView.tag animated:YES]; - } - - if ([[self.overlayWindow subviews] count] > 0) - [self scheduleFingerTipRemoval]; -} - -- (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated; -{ - MBFingerTipView *touchView = (MBFingerTipView *)[self.overlayWindow viewWithTag:hash]; - if ( ! [touchView isKindOfClass:[MBFingerTipView class]]) - return; - - if ([touchView isFadingOut]) - return; - - BOOL animationsWereEnabled = [UIView areAnimationsEnabled]; - - if (animated) - { - [UIView setAnimationsEnabled:YES]; - [UIView beginAnimations:nil context:nil]; - [UIView setAnimationDuration:self.fadeDuration]; - } - - touchView.frame = CGRectMake(touchView.center.x - touchView.frame.size.width, - touchView.center.y - touchView.frame.size.height, - touchView.frame.size.width * 2, - touchView.frame.size.height * 2); - - touchView.alpha = 0.0; - - if (animated) - { - [UIView commitAnimations]; - [UIView setAnimationsEnabled:animationsWereEnabled]; - } - - touchView.fadingOut = YES; - [touchView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:self.fadeDuration]; -} - -- (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch; -{ - // We don't reliably get UITouchPhaseEnded or UITouchPhaseCancelled - // events via -sendEvent: for certain touch events. Known cases - // include swipe-to-delete on a table view row, and tap-to-cancel - // swipe to delete. We automatically remove their associated - // fingertips after a suitable timeout. - // - // It would be much nicer if we could remove all touch events after - // a suitable time out, but then we'll prematurely remove touch and - // hold events that are picked up by gesture recognizers (since we - // don't use UITouchPhaseStationary touches for those. *sigh*). So we - // end up with this more complicated setup. - - UIView *view = [touch view]; - view = [view hitTest:[touch locationInView:view] withEvent:nil]; - - while (view != nil) - { - if ([view isKindOfClass:[UITableViewCell class]]) - { - for (UIGestureRecognizer *recognizer in [touch gestureRecognizers]) - { - if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]]) - return YES; - } - } - - if ([view isKindOfClass:[UITableView class]]) - { - if ([[touch gestureRecognizers] count] == 0) - return YES; - } - - view = view.superview; - } - - return NO; -} - -@end - -#pragma mark - - -@implementation MBFingerTipView - -@end - -#pragma mark - - -@implementation MBFingerTipOverlayWindow - -// UIKit tries to get the rootViewController from the overlay window. Use the Fingertips window instead. This fixes -// issues with status bar behavior, as otherwise the overlay window would control the status bar. - -- (UIViewController *)rootViewController -{ - NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) - { - return [evaluatedObject isKindOfClass:[MBFingerTipWindow class]]; - }]; - UIWindow *mainWindow = [[[[UIApplication sharedApplication] windows] filteredArrayUsingPredicate:predicate] firstObject]; - return mainWindow.rootViewController ?: [super rootViewController]; -} - -@end diff --git a/MBFingerTipWindow.swift b/MBFingerTipWindow.swift new file mode 100644 index 0000000..70ba2f9 --- /dev/null +++ b/MBFingerTipWindow.swift @@ -0,0 +1,265 @@ +import Foundation +import UIKit + +@objc(MBXFingerTipView) +class FingerTipView: UIImageView { + var timestamp: TimeInterval? + var shouldAutomaticallyRemoveAfterTimeout: Bool? + var fadingOut: Bool = false +} + +@objc (MBXFingerTipOverlayWindow) +class FingerTipOverlayWindow: UIWindow { + override var rootViewController: UIViewController? { + set { + super.rootViewController = newValue + } + + get { + for window in UIApplication.shared.windows { + if let window = window as? FingerTipWindow { + return window.rootViewController + } + } + + return super.rootViewController + } + } +} + +@objc (MBXFingerTipWindow) +class FingerTipWindow: UIWindow { + + var touchAlpha: CGFloat = 0.5 + var fadeDuration: TimeInterval = 0.3 + var strokeColor: UIColor = .black + var fillColor: UIColor = .white + + private var active: Bool = false + + // if set to 'true' the touches are shown even when no external screen is connected + var alwaysShowTouches: Bool = false { + didSet { + if oldValue != alwaysShowTouches { + updateFingertipsAreActive() + } + } + } + + private var _touchImage: UIImage? = nil + var touchImage: UIImage { + if _touchImage == nil { + let clipPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 50, height: 50)) + + UIGraphicsBeginImageContextWithOptions(clipPath.bounds.size, false, 0) + + let drawPath = UIBezierPath(arcCenter: CGPoint(x: 25, y: 25), radius: 22, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true) + drawPath.lineWidth = 2 + + strokeColor.setStroke() + fillColor.setFill() + + drawPath.stroke() + drawPath.fill() + + clipPath.addClip() + + _touchImage = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + } + + return _touchImage! + } + + private var _overlayWindow: UIWindow? + var overlayWindow: UIWindow { + get { + if _overlayWindow == nil { + _overlayWindow = FingerTipOverlayWindow(frame: frame) + _overlayWindow?.isUserInteractionEnabled = false + _overlayWindow?.windowLevel = UIWindow.Level.statusBar + _overlayWindow?.backgroundColor = .clear + _overlayWindow?.isHidden = false + } + + return _overlayWindow! + } + + set { + _overlayWindow = newValue + } + } + var action: Bool? + var fingerTipRemovalScheduled: Bool = false + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + commonInit() + } + + func commonInit() { + NotificationCenter.default.addObserver(self, selector: #selector(screenConnect), name: UIScreen.didConnectNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(screenDisconnect), name: UIScreen.didDisconnectNotification, object: nil) + + updateFingertipsAreActive() + } + + deinit { + NotificationCenter.default.removeObserver(self, name: UIScreen.didConnectNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIScreen.didDisconnectNotification, object: nil) + } + + func anyScreenIsMirrored() -> Bool { + if UIScreen.instancesRespond(to: #selector(getter: UIScreen.mirrored)) { + for screen in UIScreen.screens { + if let _ = screen.mirrored { + return true + } + } + } + + return false + } + + func updateFingertipsAreActive() { + if alwaysShowTouches { + active = true + } else { + active = anyScreenIsMirrored() + } + } + + override func sendEvent(_ event: UIEvent) { + if active { + guard let allTouches: Set = event.allTouches else { return } + + for touch in allTouches { + switch touch.phase { + case .began, .moved, .stationary: + var touchView = overlayWindow.viewWithTag(touch.hashValue) as? FingerTipView + + if touch.phase != .stationary && touchView != nil && touchView?.fadingOut == true { + touchView?.removeFromSuperview() + touchView = nil + } + + if touchView == nil && touch.phase != .stationary { + touchView = FingerTipView(image: touchImage) + overlayWindow.addSubview(touchView!) + } + + if touchView?.fadingOut == false { + touchView?.alpha = touchAlpha + touchView?.center = touch.location(in: overlayWindow) + touchView?.tag = touch.hashValue + touchView?.timestamp = touch.timestamp + touchView?.shouldAutomaticallyRemoveAfterTimeout = shouldAutomaticallyRemoveFingerTip(for: touch) + } + break + case .ended, .cancelled: + removeFingerTip(with: touch.hashValue, animated: true) + break + @unknown default: + break + } + } + } + + super.sendEvent(event) + scheduleFingerTipRemoval() + } + + func scheduleFingerTipRemoval() { + if fingerTipRemovalScheduled { + return + } + + fingerTipRemovalScheduled = true + perform(#selector(removeInactiveFingerTips), with: nil, afterDelay: 0.1) + } + + func cancelScheduledFingerTipRemoval() { + fingerTipRemovalScheduled = true + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(removeInactiveFingerTips), object: nil) + } + + @objc func removeInactiveFingerTips() { + fingerTipRemovalScheduled = false + + let now = ProcessInfo.processInfo.systemUptime + let REMOVAL_DELAY = 0.2 + + for touchView in overlayWindow.subviews { + if let touchView = touchView as? FingerTipView { + if touchView.shouldAutomaticallyRemoveAfterTimeout == true && now > touchView.timestamp ?? 0 + REMOVAL_DELAY { + removeFingerTip(with: touchView.tag, animated: true) + } + } + } + + if overlayWindow.subviews.count > 0 { + scheduleFingerTipRemoval() + } + } + + func removeFingerTip(with hash: Int, animated: Bool) { + guard let touchView = overlayWindow.viewWithTag(hash) as? FingerTipView else { return } + + if touchView.fadingOut == true { + return + } + + UIView.animate(withDuration: fadeDuration) { + touchView.alpha = 0 + touchView.frame = CGRect(x: touchView.center.x - touchView.frame.size.width / 1.5, + y: touchView.center.y - touchView.frame.size.height / 1.5, + width: touchView.frame.size.width * 1.5, + height: touchView.frame.size.height * 1.5) + } + + touchView.fadingOut = true + touchView.perform(#selector(removeFromSuperview), with: nil, afterDelay: fadeDuration) + } + + func shouldAutomaticallyRemoveFingerTip(for touch: UITouch) -> Bool { + + var view = touch.view + view = view?.hitTest(touch.location(in: view), with: nil) + + while view != nil { + if view is UITableViewCell { + for recognizer in touch.gestureRecognizers ?? [] { + if recognizer is UISwipeGestureRecognizer { + return true + } + } + } else if view is UITableView { + if touch.gestureRecognizers?.count == 0 { + return true + } + } + + view = view?.superview + } + + return false + } + + @objc func screenConnect() { + updateFingertipsAreActive() + } + + @objc func screenDisconnect() { + updateFingertipsAreActive() + } +} + +