Last active
June 5, 2024 15:43
-
-
Save robertmryan/b716d102645fa4a0a85acc2176a3e4ab to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| 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() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

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
hitTestimplementation, you can (a) detect hits outside the view’s ownbounds; and (b) you can detect hits on the four handles by ensuring that you transform the point to the subview‘s coordinate space.