Skip to content

Instantly share code, notes, and snippets.

@robertmryan
Last active June 5, 2024 15:43
Show Gist options
  • Save robertmryan/b716d102645fa4a0a85acc2176a3e4ab to your computer and use it in GitHub Desktop.
Save robertmryan/b716d102645fa4a0a85acc2176a3e4ab to your computer and use it in GitHub Desktop.
import UIKit
class HandleView: UIView {
var corner: Corner = .unknown
}
extension HandleView {
enum Corner: Int {
case unknown = -1
case topLeft = 0
case topRight = 1
case bottomRight = 2
case bottomLeft = 3
}
}
extension HandleView.Corner {
var isLeft: Bool { self == .topLeft || self == .bottomLeft }
var isTop: Bool { self == .topLeft || self == .topRight }
}
extension HandleView {
func addConstraints() {
NSLayoutConstraint.activate([
widthAnchor.constraint(equalToConstant: 44),
heightAnchor.constraint(equalToConstant: 44),
centerXAnchor.constraint(equalTo: corner.isLeft ? superview!.leadingAnchor : superview!.rightAnchor),
centerYAnchor.constraint(equalTo: corner.isTop ? superview!.topAnchor : superview!.bottomAnchor),
])
}
func addGestureRecognizers() {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
addGestureRecognizer(tap)
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
addGestureRecognizer(pan)
isUserInteractionEnabled = true
}
}
private extension HandleView {
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
print("tapped on", corner)
}
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
let translation = gesture.translation(in: self)
guard
let x = formatter.string(for: translation.x),
let y = formatter.string(for: translation.y)
else { return }
print("panned", corner, "by \(x), \(y)")
}
}
class MainView: UIView {
private var combinedBounds: CGRect = .zero
private var oldTransform: CGAffineTransform!
override func layoutSubviews() {
super.layoutSubviews()
updateCombinedBounds()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard combinedBounds.contains(point) else {
return nil
}
for subview in subviews.reversed() {
let point = subview.convert(point, from: self)
if let hitView = subview.hitTest(point, with: event) {
return hitView
}
}
return super.hitTest(point, with: event)
}
}
extension MainView {
func addRotateGesture() {
let rotate = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
addGestureRecognizer(rotate)
isUserInteractionEnabled = true
}
}
private extension MainView {
/// Calculate combined bounds of this view and its immediate subviews
///
/// - note: This does not (currently) navigate the whole view hierarchy,
/// but rather just considers its immediate subviews.
func updateCombinedBounds() {
combinedBounds = bounds
subviews.forEach { subview in
combinedBounds = combinedBounds.union(subview.frame)
}
}
@objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
switch gesture.state {
case .began: oldTransform = transform
case .changed, .ended: transform = oldTransform.rotated(by: gesture.rotation)
default: ()
}
}
}
class ExampleViewController: UIViewController {
override public func viewDidLoad() {
super.viewDidLoad()
let mainView = MainView(frame: CGRect(x: 100, y: 100, width: 300, height: 300))
mainView.backgroundColor = .darkGray
mainView.addRotateGesture()
view.addSubview(mainView)
let handles = (0..<4).map { i in
let view = HandleView()
view.corner = .init(rawValue: i)!
view.backgroundColor = .gray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}
handles.forEach { subview in
mainView.addSubview(subview)
subview.addConstraints()
subview.addGestureRecognizers()
}
}
}
@robertmryan
Copy link
Author

This creates a view, adds four “handle views” in the corners, allows you to rotate the “main view” and detect taps on its four “handle views”. Just illustrates that with a proper hitTest implementation, you can (a) detect hits outside the view’s own bounds; and (b) you can detect hits on the four handles by ensuring that you transform the point to the subview‘s coordinate space.

@robertmryan
Copy link
Author

E.g., here I rotated the main view and (a) tapped on upper left handle; and (b) panned on bottom right handle. All the hits were detected by the respective gesture recognizers:

IMG_0438

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