Skip to content

Instantly share code, notes, and snippets.

@donpark
Created November 22, 2016 04:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save donpark/634ea6736bb9e99bf6423dc2cebbc20a to your computer and use it in GitHub Desktop.
Save donpark/634ea6736bb9e99bf6423dc2cebbc20a to your computer and use it in GitHub Desktop.
Visible Touch support for demos and ReplayKit uses

Integration

Add VisibleTouch.swift to your project then replace window instance variable declaration in AppDelegate.swift file:

var window: UIWindow?

with:

var window: UIWindow? = VisibleTouch.Window(frame: UIScreen.main.bounds)

Usage

To enable visible touch:

VisibleTouch.enable()

To disable:

VisibleTouch.disable()
//
// VisibleTouch.swift
// Nar8
//
// Created by Don Park on 11/12/16.
// Copyright © 2016 Nar8. All rights reserved.
//
import UIKit
import QuartzCore
class VisibleTouch {
private class Event {
static let enable = Notification.Name("VisibleTouch.enable")
static let disable = Notification.Name("VisibleTouch.disable")
}
static func enable() {
NotificationCenter.default.post(name: Event.enable, object: UIScreen.main, userInfo: nil)
}
static func disable() {
NotificationCenter.default.post(name: Event.disable, object: UIScreen.main, userInfo: nil)
}
class Window: UIWindow {
private class TouchLayer: CAShapeLayer {
let radius: CGFloat = UIScreen.main.scale * 16.0
let popScale: CGFloat = UIScreen.main.scale * 1.5
func apply(touch: UITouch) {
self.path = UIBezierPath(ovalIn: CGRect(x: -radius, y: -radius, width: radius * 2, height: radius * 2)).cgPath
self.strokeColor = UIColor.clear.cgColor
self.fillColor = UIColor.blue.cgColor
self.opacity = 0.25
}
}
private typealias TouchLayers = [Int:TouchLayer]
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(onNotification(_:)), name: nil, object: UIScreen.main)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self, name: nil, object: UIScreen.main)
}
private var isTouchVisible: Bool = false
private var touchLayers: TouchLayers = TouchLayers()
func showTouches() {
self.isTouchVisible = true
}
func hideTouches() {
self.isTouchVisible = false
self.removeAllTouchLayers()
}
func onNotification(_ notification: Notification) {
if notification.name == Event.enable {
self.showTouches()
} else if notification.name == Event.disable {
self.hideTouches()
}
}
// all touch events should pass here
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
guard self.isTouchVisible && event.type == .touches else {
return
}
guard let touches = event.allTouches else {
return
}
var newTouchLayers = self.touchLayers
for touch in touches {
switch touch.phase {
case .began:
self.addTouchLayerFor(touch: touch, touchLayers: &newTouchLayers)
case .moved:
self.moveTouchLayerFor(touch: touch, touchLayers: &newTouchLayers)
case .stationary:
self.keepTouchLayerFor(touch: touch, touchLayers: &newTouchLayers)
case .cancelled:
fallthrough
case .ended:
self.removeTouchLayerFor(touch: touch, touchLayers: &newTouchLayers)
}
}
self.touchLayers = newTouchLayers
}
private func addTouchLayerFor(touch: UITouch, touchLayers: inout TouchLayers) {
let layer = TouchLayer()
layer.apply(touch: touch)
layer.position = touch.location(in: self)
layer.add(self.touchBeganAnimation(), forKey: "add")
self.touchLayers[touch.hash] = layer
self.insertTouchLayer(layer)
}
private func removeTouchLayerFor(touch: UITouch, touchLayers: inout TouchLayers) {
guard let layer = touchLayers.removeValue(forKey: touch.hash) else {
print("\(#function) touch layer not found")
return
}
self.removeTouchLayer(layer)
}
private func removeAllTouchLayers() {
print("\(#function)")
for (_, layer) in touchLayers {
self.removeTouchLayer(layer)
}
self.touchLayers.removeAll()
}
private func moveTouchLayerFor(touch: UITouch, touchLayers: inout TouchLayers) {
guard let layer = touchLayers[touch.hash] else {
return
}
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.position = touch.location(in: self)
CATransaction.commit()
}
private func keepTouchLayerFor(touch: UITouch, touchLayers: inout TouchLayers) {
// do nothing
}
private func insertTouchLayer(_ touchLayer: TouchLayer) {
self.layer.addSublayer(touchLayer)
}
private func removeTouchLayer(_ touchLayer: TouchLayer) {
touchLayer.removeFromSuperlayer()
}
private func touchBeganAnimation() -> CAAnimation {
let anim = CABasicAnimation(keyPath: #keyPath(CALayer.contentsScale))
anim.fromValue = 1.5
anim.toValue = 1.0
anim.duration = 5
anim.beginTime = CACurrentMediaTime() + 1.0
return anim
}
}
}
@donpark
Copy link
Author

donpark commented Nov 22, 2016

Please ignore the auto-generated copyright notice. License is MIT.

@donpark
Copy link
Author

donpark commented Nov 23, 2016

Last minute thoughtless explicit scoping added causes touch layer not found errors.

To fix it, edit the following line in addTouchLayerFor method:

self.touchLayers[touch.hash] = layer

to:

touchLayers[touch.hash] = layer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment