cancelTracking called unexpectedly when dragging in customized segmented control - ios

I'm using the customized segmented control from this tutorial, in addition, I would like the selected segment to be changed on a swipe/drag, so I added these functions:
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
super.beginTracking(touch, with: event)
let location = touch.location(in: self)
lastTouchLocation = location
return true
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
super.continueTracking(touch, with: event)
let location = touch.location(in: self)
print(location.x - lastTouchLocation!.x)
let newX = thumbView.frame.origin.x + (location.x - lastTouchLocation!.x)
if frame.minX <= newX && newX + thumbView.frame.width <= frame.maxX {
thumbView.frame.origin.x = newX
}
lastTouchLocation = location
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
let location = touch != nil ? touch!.location(in: self) : lastTouchLocation!
var calculatedIndex : Int?
for (index, item) in labels.enumerated() {
if item.frame.contains(location) {
calculatedIndex = index
}
}
if calculatedIndex != nil && calculatedIndex != selectedIndex {
selectedIndex = calculatedIndex!
sendActions(for: .valueChanged)
} else {
displayNewSelectedIndex()
}
}
I've embedded the control in a UIView container, somehow the touch gets canceled when I drag the thumb view for a short distance
Could this be a problem with the view container, and how can I fix this?
Thank you if you've read the whole thing.

I ran into this recently when my custom UIControl was on a formsheet. It worked fine on a popover, but when I put the same control on a formsheet, it would abort the ability to drag sideways abruptly for seemingly no reason. cancelTracking was called but the event didn't tell me why. I figured out it had to do with iOS13's new way of dismissing Formsheets by dragging down. To fix it, I added this code to my class that extended UIControl:
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
return false
}
return true
}

Related

Custom UIButton .touchDragEnter and .touchDragExit area/size?

Is it possible to customize the area from the button at which it is considered .touchDragExit (or .touchDragEnter) (out of its selectable area?)?
To be more specific, I am speaking about this situation: I tap the UIButton, the .touchDown gets called, then I start dragging my finger away from the button and at some point (some distance away) it will not select anymore (and of course I can drag back in to select...). I would like the modify that distance...
Is this even possible?
You need to overwrite the UIButton continueTracking and touchesEnded functions.
Adapting #Dean's link, the implementation would be as following (swift 4.2):
class ViewController: UIViewController {
#IBOutlet weak var button: DragButton!
override func viewDidLoad() {
super.viewDidLoad()
}
}
class DragButton: UIButton {
private let _boundsExtension: CGFloat = 0 // Adjust this as needed
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let previousLocation: CGPoint = touch.previousLocation(in: self)
let touchOutside: Bool = !outerBounds.contains(currentLocation)
if touchOutside {
let previousTouchInside: Bool = outerBounds.contains(previousLocation)
if previousTouchInside {
print("touchDragExit")
sendActions(for: .touchDragExit)
} else {
print("touchDragOutside")
sendActions(for: .touchDragOutside)
}
} else {
let previousTouchOutside: Bool = !outerBounds.contains(previousLocation)
if previousTouchOutside {
print("touchDragEnter")
sendActions(for: .touchDragEnter)
} else {
print("touchDragInside")
sendActions(for: .touchDragInside)
}
}
return true
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let touchInside: Bool = outerBounds.contains(currentLocation)
if touchInside {
print("touchUpInside action")
return sendActions(for: .touchUpInside)
} else {
print("touchUpOutside action")
return sendActions(for: .touchUpOutside)
}
}
}
Try changing the _boundsExtension value
The drag area is exaclty equal to the area define by bounds.
So if you want to customize the drag are simple customise the bounds of your button.

Forward touch event from UIView, to its TableView inside it

I built a SplitView class that looks like this picture below:
As you can see the SplitView always have two subviews, therefore it has two properties that are leftView and rightView.
The job of the SplitView is to manage its subview size proportion.
If I do a swipe gesture to the left, it will move the separator location and it changes the size of each subviews, so it will look like this:
It works perfectly until I use UITableView as the leftView and rightView.
This is because the UITableView inside it is the one which will process the touch event, not the superview (which is SplitView)
And because the code to intercept and respond to the touch is in SplitView, it doesn't do anything if the UITableView inside it is the one that receives the touch event.
To do this I implement hitTest to make the SplitView the responder, not the UITableView inside it.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
Now I can get the touch event in the SplitView even though the user swipe on the UITableView.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
self.startInfo = StartInfo(timestamp: touch.timestamp,
touchLocation: touch.location(in: self),
separatorLocation: separatorView.frame.origin)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let startInfo = startInfo {
let location = touch.location(in: self)
if moveHorizontally == nil {
let deltaX = abs(location.x - startInfo.touchLocation.x)
let deltaY = abs(location.y - startInfo.touchLocation.y)
if deltaX > 4.0 || deltaY > 4.0 {
moveHorizontally = deltaX > deltaY
}
}
if let moveHorizontally = moveHorizontally {
if moveHorizontally {
print("the user intends to adjust separator position")
adjustSeparatorViewPosition(usingTouchLocation: location)
} else {
print("the user intends to scroll the table view")
if rightView.frame.contains(location) {
rightView.touchesMoved(touches, with: event) // doesn't work
} else {
leftView.touchesMoved(touches, with: event) // doesn't work
}
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startInfo = nil
moveHorizontally = nil
leftView.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
if touch.location(in: self).x < self.bounds.width / 2 {
UIView.animate(withDuration: 0.3) {
self.setSeparatorLocationX(to: self.separatorMinX)
}
} else {
UIView.animate(withDuration: 0.3) {
self.setSeparatorLocationX(to: self.separatorMaxX)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
startInfo = nil
moveHorizontally = nil
leftView.touchesCancelled(touches, with: event)
}
As you can see in this code, I added a delay, just like UIScrollView does when swiping to get the user intention, whether the user want to scroll horizontally or vertically.
- If the swipe direction is horizontal, I want to adjust the separator location
- If the swipe direction is vertical, I want to forward the event to the UITableView inside it (for example the leftView)
But, forwarding the touch event using rightView.touchesMoved(touches, with: event) doesn't work.
How to forward the touch event from the SplitView to the UITableView inside it?

Function to detect if there's any touch occurring on the screen

I'm trying to create a simple function, similar to the touchesBegan, that detects if there's any touch occurring on the screen.
I've hit a brick wall trying it out myself because I'm not comfortable with UITouch class, but I really need some self made function, outside the touchesBegan default one.
I was trying to do something like this 'pseudo-code/swift'
func isTouchingTheScreen() -> Bool {
let someTouchHandleConstant: uitouch ???
if imTouchingTheScreen {
return true
} else {
return false
}
}
Do you have any hints?
PS: I know that code doesn't work, don't call that out, it was just to give you some 'image' of what I was trying to do (:
The idea
You can simply keep track of every touch begun, ended or cancelled by the user.
class GameScene: SKScene {
private var activeTouches = Set<UITouch>()
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
activeTouches.unionInPlace(touches)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
activeTouches.subtractInPlace(touches)
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
if let touches = touches {
activeTouches.subtractInPlace(touches)
}
}
var isTouchingTheScreen: Bool { return !activeTouches.isEmpty }
}
Keeping activeTouches updated
As you can see I am keeping updated the activeTouches Set.
Every time a touch does begin I add it to activeTouches. And every time a touch does end or is cancelled I remove it from activeTouches.
The isTouchingTheScreen computed variable
This allows me to define the isTouchingTheScreen computed property that simply returns true when the Set contains some element.
You can implement UITapGestureRecognizer as below:
var tapGesture :UITapGestureRecognizer!
override func didMoveToNode() {
// Add UITapGestureRecognizer to view
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.touchedView(_:)))
view.addGestureRecognizer(tapGesture)
}
func touchedView(sender: UITapGestureRecognizer) {
print("view touched")
}
You could implement UITapGestureRecognizer:
// global var
var tapGesture :UITapGestureRecognizer!
override func didMoveToView() {
// Add tap gesture recognizer to view
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(GameScene.handleTap(_:)))
self.tapGesture.cancelsTouchesInView = false
view.addGestureRecognizer(tapGesture)
}
func handleTap(sender: UITapGestureRecognizer) {
print("GameScene tap")
if sender.state == .Ended {
var positionInScene: CGPoint = sender.locationInView(sender.view)
positionInScene = self.scene!.convertPointFromView(positionInScene)
let touchedNode = self.nodeAtPoint(positionInScene)
if touchedNode.name != "myHero" {
print("The SKSpriteNode myHero was tapped")
}
}
}
You can find more details in Apple docs here.

Disable touches everywhere except inside of a button? - Swift

When I die in my game, I want to ignore all touch events by the user EXCEPT if the user taps inside of or on the reset game button. Here is my code.
for touch in touches{
let location = touch.locationInNode(self)
if died == true{
Ball.physicsBody?.velocity = CGVectorMake(0, 0)
if resetGame.containsPoint(location){
restartScene()
runAction(SKAction.playSoundFileNamed("Woosh.wav", waitForCompletion: false))
}
else {
self.userInteractionEnabled = false
}
This is all inside of my touchesBegan. This was my attempt at ignoring the user's touch unless the location of the touch was within the size of button. How can I ignore a user's touches everywhere on the screen except the button? resetGame is an SKSpriteNode image.
There are two solutions to your issue.
The first case I want to propose to you is based to gesture recognizer.
You can separate the button from the other touches events and switch on/off the touches event by a boolean var like this:
Gesture recognizers:
In the global var declaration section of your class:
var tapGesture :UITapGestureRecognizer!
var enableTouches:Bool = true
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(GameClass.simpleTap(_:)))
self.tapGesture.cancelsTouchesInView = false
view.addGestureRecognizer(tapGesture)
myBtn.name = "myBtn"
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return self.enableTouches
}
func simpleTap(sender: UITapGestureRecognizer) {
print("simple tap")
if sender.state == .Ended {
var touchLocation: CGPoint = sender.locationInView(sender.view)
touchLocation = self.convertPointFromView(touchLocation)
let touchedNode = self.nodeAtPoint(touchLocation)
// do your stuff
if touchedNode.name == "myBtn" {
// button pressed, do your stuff
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if died == true {
self.enableTouches = false // disable all touches but leave gestures enabled
//do whatever you want when hero is died
}
}
Only a Boolean
The second solution I want to propose is simply to stopping touches flow by using a simple boolean (it's not very elegant but it works).
This method look when button is tapped and the update method check if your hero is dead so all touches will be disabled:
In the global var declaration section of your class:
var enableTouches:Bool = true
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
myBtn.name = "myBtn"
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!enableTouches) {
return
}
let touch = touches.first
let positionInScene = touch!.locationInNode(self)
let touchedNode = self.nodeAtPoint(positionInScene)
// do your stuff
if touchedNode.name == "myBtn" {
// button pressed, do your stuff
if died == true {
self.enableTouches = false // disable all touches
//do whatever you want when hero is died
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!enableTouches) {
return
}
let touch = touches.first
let positionInScene = touch!.locationInNode(self)
let touchedNode = self.nodeAtPoint(positionInScene)
// do your stuff
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!enableTouches) {
return
}
}
The first case permit to you to have always the gesture enabled so you can do also other stuff with gestures when your hero will be died. The second case stop your touches when you press your button and do the "die flow". Choose which may be the most suitable for you.

Touch Drag Inside a UIButton after 5 pixels instead of default 1 pixel

I have a single UIButton on a storyboard. The UIButton is connected to a Touch Drag Inside action myTouchDragInsideAction. The action is triggered when a user drags the button from inside (UIControlEventTouchDragInside).
The problem is that the action is triggered after 1 pixel of inside dragging. However 1 pixel is too sensitive and can be triggered with just the slightest finger movement.
#IBAction func myTouchDragInsideAction(sender: UIButton) {
print("Button dragged inside")
}
Question:
How can I extend this action to trigger the inside dragging action only after at least 5 pixels of movement?
You have to create custom button for it. Following CustomButton may help you.
let DelayPoint:CGFloat = 5
class CustomButton: UIButton {
var startPoint:CGPoint?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.startPoint == nil {
self.startPoint = touches.first?.previousLocationInView(self)
}
if self.shouldAllowForSendActionForPoint((touches.first?.locationInView(self))!) {
super.touchesMoved(touches, withEvent: event)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.startPoint = nil
super.touchesEnded(touches, withEvent: event)
}
func shouldAllowForSendActionForPoint(location:CGPoint) -> Bool {
if self.startPoint != nil {
let xDiff = (self.startPoint?.x)! - location.x
let yDiff = (self.startPoint?.y)! - location.y
if (xDiff > DelayPoint || xDiff < -DelayPoint || yDiff > DelayPoint || yDiff < -DelayPoint) {
return true
}
}
return false
}
}
You change the "DelayPoint" as per your req.
Hope this will help you.
My implementation of solution for Swift 3
final class DraggedButton: UIButton {
// MARK: - Public Properties
#IBInspectable var sensitivityOfDrag: CGFloat = 5
// MARK: - Private Properties
private var startDragPoint: CGPoint?
// MARK: - UIResponder
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let firstTouch = touches.first else {
return
}
let location = firstTouch.location(in: self)
let previousLocation = firstTouch.previousLocation(in: self)
if startDragPoint == nil {
startDragPoint = previousLocation
}
if shouldAllowForSendActionForPoint(location: location) {
super.touchesMoved(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startDragPoint = nil
super.touchesEnded(touches, with: event)
}
// MARK: - Private methods
private func shouldAllowForSendActionForPoint(location: CGPoint) -> Bool {
guard let startDragPoint = startDragPoint else {
return false
}
let xDifferent = abs(startDragPoint.x - location.x)
let yDifferent = abs(startDragPoint.y - location.y)
return xDifferent > sensitivityOfDrag || yDifferent > sensitivityOfDrag
}
}

Resources