Make UIScrollView give up UITouch - ios

I have a drawing app with a canvas larger than the size of the phone screen. I want to implement scrolling with two fingers and drawing with one finger. So far I can make the scrolling work just fine but when it comes to drawing, the line begins and then the view where the drawing is loses control of the touch such that only the first part of the line is drawn. I think the scrollview takes control back. Dots can be draw just fine.
This is my subclassed UIScrollView
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesBegan(touches, with: event)
} else {
super.touchesBegan(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesEnded(touches, with: event)
} else {
super.touchesEnded(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesMoved(touches, with: event)
} else {
super.touchesMoved(touches, with: event)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesCancelled(touches, with: event)
} else {
super.touchesCancelled(touches, with: event)
}
}
override func touchesShouldCancel(in view: UIView) -> Bool {
if (type(of: view)) == UIScrollView.self {
return true
}
return false
}

You will need a Long Press Gesture Recognizer connected to your ScrollerView, set with Min Duration 0 seconds, also to recognize only 1 Touch and Cancel touches in view option active.
You can find all these options under the Attributes Inspector on Interface Builder.
Please play a little with the Tolerance settings to fine tune the results.

Related

How to handle touches by an UIView on top of a UIScrollView and the UIScrollView both at the same time

I have the following hierarchy, the root view of the controller. Added two subviews, a standard UIView and a UIScrollView. The UIView is on top.
What I want to do is to receive touches on that UIView, meaning touchesBegan, touchesMoved... like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Began")
super.touchesBegan(touches, with: event)
next?.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Moved")
super.touchesMoved(touches, with: event)
next?.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Ended")
super.touchesEnded(touches, with: event)
next?.touchesEnded(touches, with: event)
}
But at the same time I want the scroll to work. The UIScrollView that is below, I want that to keep scrolling as usual.
This seems impossible to do. If my view handles touches, then I can't forward them to the UIScrollView. But I want both things: see the raw touches on the top view while the scrollview to work as usual.
How can I accomplish this?
The opposite would also work. Meaning, a scrollview on top that scrolls as usual but I also receive the raw touches (touchesBegan, touchesMoved...) on the view that is underneath.
Following this answer worked for me, though with some slight changes. Do note that for this solution, the UIScrollView is on top of the UIView. Firstly, you need to create a subclass of UIScrollView and override the touch methods.
protocol CustomScrollViewDelegate {
func scrollViewTouchBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func scrollViewTouchMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func scrollViewTouchEnded(_ touches: Set<UITouch>, with event: UIEvent?)
}
class CustomScrollView: UIScrollView {
var customDelegate: CustomScrollViewDelegate?
override func awakeFromNib() {
super.awakeFromNib()
for gesture in self.gestureRecognizers ?? [] {
gesture.cancelsTouchesInView = false
gesture.delaysTouchesBegan = false
gesture.delaysTouchesEnded = false
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
customDelegate?.scrollViewTouchBegan(touches, with: event)
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
customDelegate?.scrollViewTouchMoved(touches, with: event)
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
customDelegate?.scrollViewTouchEnded(touches, with: event)
super.touchesEnded(touches, with: event)
}
}
Take note of the code in awakeFromNib(). UIScrollView has its own set of gestures. So for each gesture, delaysTouchesBegan and delaysTouchesEnded needs to be false to prevent delays for the touch events.
Finally, just assign the delegate to your ViewController and implement the methods like so.
class ViewController: UIViewController {
#IBOutlet weak var scrollView: CustomScrollView!
#IBOutlet weak var touchView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.customDelegate = self
}
}
extension ViewController: CustomScrollViewDelegate {
func scrollViewTouchBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if touch location is within the bounds of the UIView
if let touch = touches.first {
let position = touch.location(in: view)
if touchView.bounds.contains(position) {
print("Began")
}
}
}
func scrollViewTouchMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if touch location is within the bounds of the UIView
if let touch = touches.first {
let position = touch.location(in: view)
if touchView.bounds.contains(position) {
print("Moved")
}
}
}
func scrollViewTouchEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if touch location is within the bounds of the UIView
if let touch = touches.first {
let position = touch.location(in: view)
if touchView.bounds.contains(position) {
print("Ended")
}
}
}
}

Get touch type in hitTest() or pointInside()

I'm implementing a passThroughView by creating a transparent View on top and override hitTest().
passThroughView should consume touches from Apple Pencil and if touch type is not from pencil, it pass touches to the view underneath.
The problems are:
parameter "event" in hitTest contains no touch, so I can't check touch type in hitTest
I can get touch from touchesBegan and check touch type, but it get called only after hitTest returned true
I subclass UIWindow and override sendEvent() but this function also called after hitTest (and I don't know why)
class WindowAbleToKnowTouchTypes: UIWindow {
override func sendEvent(_ event: UIEvent) {
if event.type == .touches {
// This get called after Hittest
if event.allTouches!.first!.type == .pencil {
print("This touch is from Apple Pencil")
}
}
super.sendEvent(event)
}
}
Is there anyway to check touchType to decide to pass or consume touches?
I ended up using a different approach, it could be useful for many cases: If I can't get touchType in hitTest(), I can still get the touchType with GestureRecognize:
class CustomGestureRecognizer : ImmediatePanGesture {
var beganTouch : UITouch!
var movedTouch : UITouch!
var endedTouch : UITouch!
override func shouldReceive(_ event: UIEvent) -> Bool {
// You can check for touchType here and decide if this gesture should receice the touch or not
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let firstTouch = touches.first {
beganTouch = firstTouch
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let touch = touches.first {
movedTouch = touch
}
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let touch = touches.first {
endedTouch = touch
}
super.touchesEnded(touches, with: event)
}
}
In the target function of the gestureRecognizer, you can get the UITouch by:
let beganTouch = customGesture.beganTouch
let touchType = beganTouch.touchType

UIGestureRecognizer subclass-- touchesMoved automatically changes state to .changed

I am trying to create a UIGestureRecognizer subclass, and in touchesMoved I have no code, yet state is being set to .changed.
Does anyone know why this is happening? I need to be able to decide for myself when we are in the .changed state.
If I remove my touchesBegan function and never move to .began, this doesn't happen.
import UIKit.UIGestureRecognizerSubclass
class MyRecognizer: UIGestureRecognizer {
private var refView: UIView? {
return self.view?.window
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if touches.count != 1 {
state = .failed
return
}
guard let location = touches.first?.location(in: refView) else {
self.state = .failed
return
}
state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
// there is no code here!
}
//This is how I'm debugging state
override var state: UIGestureRecognizerState {
get { return super.state }
set {
super.state = newValue
print("state = \(newValue.debugDescription)")
}
}
}
extension UIGestureRecognizerState: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "began"
case .possible: return "possible"
case .changed: return "changed"
case .ended: return "ended"
case .cancelled: return "cancelled"
case .failed: return "failed"
}
}
}
Your testing approach is flawed. I tried this (this is my test app's complete code):
import UIKit
import UIKit.UIGestureRecognizerSubclass
class MyRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
print(self.state.rawValue)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
print(self.state.rawValue)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let g = MyRecognizer(target: self, action: #selector(grFired))
self.view.addGestureRecognizer(g)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func grFired(_ g:UIGestureRecognizer) {
print("here", g.state.rawValue)
}
}
Then I dragged on the background view, to which the gesture recognizer is attached. My gesture recognizer never moved from the possible state.
Also note that your expectations may be incorrect ("I need to be able to decide for myself when we are in the .changed state"). The normal and correct behavior is that, having declared the state to be .began in touchesBegan, your touchesMoved will be called once with state .began and the gesture recognizer will immediately advance to .changed automatically. That is correct; if this is continuous gesture, it cannot be the case that a moved event should come in while we are in .began state without our proceeding to .changed. Again, here's a test:
import UIKit
import UIKit.UIGestureRecognizerSubclass
class MyRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if touches.count != 1 {
state = .failed
return
}
print(self.state.rawValue)
self.state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
print("moved", self.state.rawValue)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let g = MyRecognizer(target: self, action: #selector(grFired))
self.view.addGestureRecognizer(g)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func grFired(_ g:UIGestureRecognizer) {
print("here", g.state.rawValue)
}
}

Detecting UITouches while UIViewController is being shown as 3DTouch preview

Is it possible to detect touches and get the location of a touch from a UIViewController which is being currently used as previewingContext view controller for 3D Touch? (I want to change the image of within the preview controller when the touch moves from left to right)
I've tried both touchesBegan and touchesMoved none of them are fired.
class ThreeDTouchPreviewController: UIViewController {
func getLocationFromTouch(touches: Set<UITouch>) -> CGPoint?{
guard let touch = touches.first else { return nil }
return touch.location(in: self.view)
}
//Not fired
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
//Not fired
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
}
Even tried adding a UIPanGesture.
Attempting to replicate FaceBook's 3D Touch feature where a user can move finger from left to right to change the current image being displayed.
Video for context: https://streamable.com/ilnln
If you want to achive result same as in FaceBook's 3D Touch feature you need to create your own 3D Touch gesture class
import UIKit.UIGestureRecognizerSubclass
class ForceTouchGestureRecognizer: UIGestureRecognizer {
var forceValue: CGFloat = 0
var isForceTouch: Bool = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
handleForceWithTouches(touches: touches)
state = .began
self.isForceTouch = false
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
handleForceWithTouches(touches: touches)
if self.forceValue > 6.0 {
state = .changed
self.isForceTouch = true
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
handleForceWithTouches(touches: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = .cancelled
handleForceWithTouches(touches: touches)
}
func handleForceWithTouches(touches: Set<UITouch>) {
if touches.count != 1 {
state = .failed
return
}
guard let touch = touches.first else {
state = .failed
return
}
forceValue = touch.force
}
}
and now you can add this gesture in your ViewController in viewDidLoad method
override func viewDidLoad() {
super.viewDidLoad()
let gesture = ForceTouchGestureRecognizer(target: self, action: #selector(imagePressed(sender:)))
self.view.addGestureRecognizer(gesture)
}
Now you can manage your controller UI in Storyboard. Add cover view above UICollectionView and UIImageView in a center and connect it with IBOutlets in code.
Now you can add handler methods for gesture
func imagePressed(sender: ForceTouchGestureRecognizer) {
let location = sender.location(in: self.view)
guard let indexPath = collectionView?.indexPathForItem(
at: location) else { return }
let image = self.images[indexPath.row]
switch sender.state {
case .changed:
if sender.isForceTouch {
self.coverView?.isHidden = false
self.selectedImageView?.isHidden = false
self.selectedImageView?.image = image
}
case .ended:
print("force: \(sender.forceValue)")
if sender.isForceTouch {
self.coverView?.isHidden = true
self.selectedImageView?.isHidden = true
self.selectedImageView?.image = nil
} else {
//TODO: handle selecting items of UICollectionView here,
//you can refer to this SO question for more info: https://stackoverflow.com/questions/42372609/collectionview-didnt-call-didselectitematindexpath-when-superview-has-gesture
print("Did select row at indexPath: \(indexPath)")
self.collectionView?.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
}
default: break
}
}
From this point you need to customize your view to make it look in same way as Facebook do.
Also I created small example project on GitHub https://github.com/ChernyshenkoTaras/3DTouchExample to demonstrate it

touchesMoved called without moving a finger

I'm trying to implement user control node with one ThumbStick and two buttons. I've create separate SKSpriteNode with control elements on it and override touch events of parent node to handle user touch.
The problem is that when I start touch screen, touchesMoved get called many times even if I don't move my finger.
Here is my touch events code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
for touch in touches {
let touchPoint = touch.location(in: self)
touchStatusLabel.text = String(format: "TOUCH BEGAN %#", arguments:[NSStringFromCGPoint(touchPoint)])
if aButton.frame.contains(touchPoint) {
NSLog("A BUTTON PRESSED")
delegate?.controlInputNode(self, beganTouchButtonWithName: aButton.name!)
}
else if bButton.frame.contains(touchPoint) {
NSLog("B BUTTON PRESSED")
delegate?.controlInputNode(self, beganTouchButtonWithName: bButton.name!)
}
else if touchPoint.x < 0 && touchPoint.y < 0 {
NSLog("THUMBSTICK PRESSED")
leftThumbStickNode.touchesBegan([touch], with: event)
leftThumbStickNode.position = pointByCheckingControlOffset(touchPoint)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
for touch in touches {
let touchPoint = touch.location(in: self)
touchStatusLabel.text = String(format: "TOUCH MOVED %#", arguments:[NSStringFromCGPoint(touchPoint)])
if !aButton.frame.contains(touchPoint) && !bButton.frame.contains(touchPoint) {
if touchPoint.x < 0 && touchPoint.y < 0 {
leftThumbStickNode.touchesMoved([touch], with: event)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
for touch in touches {
let touchPoint = touch.location(in: self)
touchStatusLabel.text = String(format: "TOUCH ENDED %#", arguments:[NSStringFromCGPoint(touchPoint)])
let node = atPoint(touchPoint)
if node == aButton || node == bButton {
delegate?.controlInputNode(self, endTouchButtonWithName: node.name!)
}
else {
leftThumbStickNode.touchesEnded([touch], with: event)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
NSLog("TOUCH CANCELED")
leftThumbStickNode.touchesCancelled(touches, with: event)
}
You can try comparing the translation and if nothing has changed then don't do anything, so your touchesMoved function will look like this;
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
for touch in touches {
let location = touch.location(in: self)
let previousLocation = touch.previousLocation(in: self)
let translation = CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y)
if translation == CGPoint.zero {
continue
}
touchStatusLabel.text = String(format: "TOUCH MOVED %#", arguments:[NSStringFromCGPoint(location)])
if !aButton.frame.contains(location) && !bButton.frame.contains(location) {
if location.x < 0 && location.y < 0 {
leftThumbStickNode.touchesMoved([touch], with: event)
}
}
}
}
It appears that this problem occurs only during debugging so I assume that it is some debugger issue

Resources