How can one unselect a given segment in a UISegmented control, by pressing the same segment again?
E.g. Press segment 0, it becomes selected and remains highlighted. Press segment 0 again and it becomes unselected and unhighlighted.
The control only fires UIControlEventValueChanged events. Other events don't seem to work with it.
There is a property 'momentary' which when set to YES almost allows the above behavior, excepting that highlighting is only momentary. When momentary=YES pressing the same segment twice results in two UIControlEventValueChanged events, but when momentary=NO only the first press of a given segment results in a UIControlEventValueChanged event being fired. I.e. subsequent presses on the same segment will not fire the UIControlEventValueChanged event.
you can subclass UISegmentedControl:
Swift 3
class ReselectableSegmentedControl: UISegmentedControl {
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let previousSelectedSegmentIndex = self.selectedSegmentIndex
super.touchesEnded(touches, withEvent: event)
if previousSelectedSegmentIndex == self.selectedSegmentIndex {
if let touch = touches.first {
let touchLocation = touch.locationInView(self)
if CGRectContainsPoint(bounds, touchLocation) {
self.sendActionsForControlEvents(.ValueChanged)
}
}
}
}
}
Swift 4
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let previousSelectedSegmentIndex = self.selectedSegmentIndex
super.touchesEnded(touches, with: event)
if previousSelectedSegmentIndex == self.selectedSegmentIndex {
let touch = touches.first!
let touchLocation = touch.location(in: self)
if bounds.contains(touchLocation) {
self.sendActions(for: .valueChanged)
}
}
}
and then
#IBAction func segmentChanged(sender: UISegmentedControl) {
if (sender.selectedSegmentIndex == selectedSegmentIndex) {
sender.selectedSegmentIndex = UISegmentedControlNoSegment;
selectedSegmentIndex = UISegmentedControlNoSegment;
}
else {
selectedSegmentIndex = sender.selectedSegmentIndex;
}
}
Not EXACTLY what you ask for but I was searching for a similar feature and decided on this.
Add doubleTap gesture to UISegmentedControl that sets the selectedSegmentIndex
When you initialize your UISegmentedControl.
let doubleTap = UITapGestureRecognizer(target: self, action #selector(YourViewController.doubleTapToggle))
doubleTap.numberOfTapsRequired = 2
yourSegmentedControl.addGestureRecognizer(doubleTap)
Function to deselect segment
#objc func doubleTapToggle () {
yourSegmentedControl.selectedSegmentIndex = -1
yourSegmentedControl.sendActions(for: valueChanged)
}
Based on #protspace answer, a simpler way, without having to write anything in the #IBAction:
class DeselectableSegmentedControl: UISegmentedControl {
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let previousSelectedSegmentIndex = selectedSegmentIndex
super.touchesEnded(touches, with: event)
if previousSelectedSegmentIndex == selectedSegmentIndex {
let touch = touches.first!
let touchLocation = touch.location(in: self)
if bounds.contains(touchLocation) {
selectedSegmentIndex = UISegmentedControl.noSegment
sendActions(for: .valueChanged)
}
}
}
}
UISegmentedControl can be programmatically deselected with Swift using #IBOutlet weak var mySegmentedControl: UISegmentedControl! and mySegmentedControl.selectedSegmentIndex = -1. As far as detecting the touch of the segment, I do not know. Hopefully someone else will have that answer.
Related
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
I want to get values from touches began and touches move. I have this code but this is only use for moving not getting value.
override func touchesMoved(touches: Set, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent: event)
let touch: UITouch = touches.first as! UITouch
if touch.view == btn1 {
println("touchesMoved")
} else {
println("touchesMoved This is not an ImageView")
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
if touch.view == self.button {
// Do whatever
// get button tag => button.tag
// get button titleLabel => button.titleLabel.text
// get button image => button.imageView.image
} else {
return
}
}
}
Use this.
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.
I have a UITableView with UITableViewCell like this:
I added a UITapGestureRecognizer (let's call the target targetCell) on the tableView to manage when the user tap on a cell. On the Button, let's call the target targetButton.
On top of the cell, I added a UIScrollView as an overlay.
What I would like to do it that when there is a pan/swipe anywhere on the cell/scrollView, then the pan is detected.
But if there is a tap on the button then targerButton is triggered, if it's outside targetCell is triggered.
So far, only targetCell is triggered, inside or outside of the button area. If I remove the UIScrollView, it works fine.
So at first I thought about overriding func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool. The problem is that at this stage, it doesn't make the difference between a tap and a pan so it doesn't solve my problem.
Then I thought about overriding func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) like the following:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let location = touches.first?.locationInView(tableView) {
var buttonFrame = CGRectMake(frame.width - 50, 0, 50, bounds.height)
buttonFrame.origin.y = (convertRect(buttonFrame, toView: tableView).minY)
let toNextResponder = buttonFrame.contains(location)
if toNextResponder {
self.nextResponder()?.touchesBegan(touches, withEvent: event)
} else {
super.touchesBegan(touches, withEvent: event)
}
} else {
super.touchesBegan(touches, withEvent: event)
}
}
It didn't work. Then I realised that self.nextResponder()? was the UITableViewCellContentView so I thought maybe, call touchesBegan(touches, withEvent: event)directly on the button. So I changed touchesBegan to this:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let location = touches.first?.locationInView(tableView) {
var buttonFrame = CGRectMake(frame.width - 50, 0, 50, bounds.height)
buttonFrame.origin.y = (convertRect(buttonFrame, toView: tableView).minY)
let toNextResponder = buttonFrame.contains(location)
if toNextResponder {
if let cellContentView = self.nextResponder() as? UIView {
let button = findButtonInView(cellContentView)
button?.touchesBegan(touches, withEvent: event)
}
} else {
super.touchesBegan(touches, withEvent: event)
}
} else {
super.touchesBegan(touches, withEvent: event)
}
}
private func findButtonInView(view: UIView) -> UIButton? {
if view is UIButton {
return view as? UIButton
} else {
var button: UIButton?
for subview in view.subviews {
button = findButtonInView(subview)
if let _ = button {
return button
}
}
return button
}
}
And... it doesn't work, targetCell only gets triggered whether you tap on the button or not. I noticed something though it that when I call chartButton?.touchInside I get false.
What did I do wrong? Or what should I have done?
Well I found a solution but I don't really like it as it feels more like a hack than a proper one.
I basically execute the button's target when a tap gesture is recognized within the button's area:
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
let location = (gestureRecognizer as! UITapGestureRecognizer).locationInView(tableView)
var buttonFrame = CGRectMake(frame.width - 50, 0, 50, bounds.height)
buttonFrame.origin.y = (convertRect(buttonFrame, toView: tableView).minY)
if buttonFrame.contains(location) {
button.sendActionsForControlEvents(.TouchUpInside)
return false
}
}
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
If anyone has a better way or think my way is wrong please let me know.
I'm making a calculator app that has several UIButtons for input of digits etc. I want the user to be able to touch down on one button and, if this was not the intended button, move the finger to another button and touch up inside that one. The button that the user has his/her finger on should change background color to indicate to the user what is happening, much like Apples built in calculator app.
I've tried to do this by using touch drag inside/outside and touch drag enter/exit on the buttons, but it only works for the button where the touch originated. Meaning I can touch down on one button, drag outside, back inside and touch up inside, but I can't touch down, drag outside and touch up inside another button.
Also the area that is recognized as being inside or outside the button is larger than the bounds of the button.
Here's an example of the code I've tried for one of the buttons:
#IBAction func didTouchDownThreeButton(sender: AnyObject) {
threeButton.backgroundColor = blueColor
}
#IBAction func didTouchUpInsideThreeButton(sender: AnyObject) {
inputTextView.text = inputTextView.text + "3"
threeButton.backgroundColor = lightGrayColor
}
#IBAction func didTouchDragExitThreeButton(sender: AnyObject) {
threeButton.backgroundColor = lightGrayColor
}
#IBAction func didTouchDragEnterThreeButton(sender: AnyObject) {
threeButton.backgroundColor = blueColor
}
Any help would be much appreciated!
I managed to create a suitable solution by overriding the functions below and keeping track of which button was touched last and second to last. The last button touched gets highlighted and the second to last gets unhighlighted. Here's my code for a two button test in case anyone should find it useful:
#IBOutlet weak var bottomButton: UIButton!
#IBOutlet weak var topButton: UIButton!
var lastTouchedButton: UIButton? = nil
var secondToLastTouchedButton: UIButton? = nil
override func touchesBegan(touches: Set<UITouch>?, withEvent event: UIEvent?) {
let touch = touches?.first
let location : CGPoint = (touch?.locationInView(self.view))!
if topButton.pointInside(self.view.convertPoint(location, toView: topButton.viewForLastBaselineLayout), withEvent: nil) {
topButton?.backgroundColor = UIColor.redColor()
}
else if bottomButton.pointInside(self.view.convertPoint(location, toView: bottomButton.viewForLastBaselineLayout), withEvent: nil) {
bottomButton?.backgroundColor = UIColor.redColor()
}
super.touchesBegan(touches!, withEvent:event)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let location : CGPoint = (touch?.locationInView(self.view))!
if topButton.pointInside(self.view.convertPoint(location, toView: topButton.viewForLastBaselineLayout), withEvent: nil) {
secondToLastTouchedButton = lastTouchedButton
lastTouchedButton = topButton
lastTouchedButton?.backgroundColor = UIColor.redColor()
}
else if bottomButton.pointInside(self.view.convertPoint(location, toView: bottomButton.viewForLastBaselineLayout), withEvent: nil) {
secondToLastTouchedButton = lastTouchedButton
lastTouchedButton = bottomButton
lastTouchedButton?.backgroundColor = UIColor.redColor()
}
else {
lastTouchedButton?.backgroundColor = UIColor.whiteColor()
}
if secondToLastTouchedButton != lastTouchedButton {
secondToLastTouchedButton?.backgroundColor = UIColor.whiteColor()
}
super.touchesMoved(touches, withEvent: event)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let location : CGPoint = (touch?.locationInView(self.view))!
if topButton.pointInside(self.view.convertPoint(location, toView: topButton.viewForLastBaselineLayout), withEvent: nil) {
topButton?.backgroundColor = UIColor.whiteColor()
}
else if bottomButton.pointInside(self.view.convertPoint(location, toView: bottomButton.viewForLastBaselineLayout), withEvent: nil) {
bottomButton?.backgroundColor = UIColor.whiteColor()
}
super.touchesEnded(touches, withEvent: event)
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
lastTouchedButton?.backgroundColor = UIColor.whiteColor()
super.touchesCancelled(touches, withEvent: event)
}
I've tried to do this by using touch drag inside/outside and touch drag enter/exit on the buttons, but it only works for the button where the touch originated
Absolutely right. The touch "belongs" to a view, and for as long as you keep dragging, it will still only belong to that view. Thus, the only way something like what you describe can be possible is for the touch detection to take place in the common superview of these button views.