How to intercept long press on UITextView without disabling context menu? - iphone

I want to intercept long press on UITextview, but don't want to disable the context menu option at the same time.
If I use gesture recognizer on textview, it will disable context menu so I am using the method like below now.
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
//fire my method here
}
But, it only fires the method when context menu shows up after the user long press some words. So when the user long press a blank space, then only the magnifying glass shows up, I can't fire the method at the time.
Does anyone have better ideas? Thanks!
//////The Problem Solved//////
Thanks to #danh and #Beppe, I made it even with tap gesture on UITextView. I wanted to show the font bar on textview by long press.
#First, I subclassed the UITextview.
#interface LisgoTextView : UITextView {
BOOL pressing_;
}
#property (nonatomic) BOOL pressing;
#end
#interface LisgoTextView (private)
- (void)longPress:(UIEvent *)event;
#end
#implementation LisgoTextView
#synthesize pressing = pressing_;
//--------------------------------------------------------------//
#pragma mark -- Long Press Detection --
//--------------------------------------------------------------//
- (void)longPress:(UIEvent *)event {
if (pressing_) {
//post notification to show font edit bar
NSNotification *fontEditBarNotification = [NSNotification notificationWithName:#"fontEditBarNotification"
object:nil userInfo:nil];
[[NSNotificationCenter defaultCenter] postNotification:fontEditBarNotification];
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self performSelector:#selector(longPress:) withObject:event afterDelay:0.7];
pressing_ = YES;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
pressing_ = NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
pressing_ = NO;
}
#I used the delay to solve the conflict with tap gesture I implemented on UITextView.
- (void)tapGestureOnTextView:(UITapGestureRecognizer *)sender {
//cancel here if long press was fired first
if (cancelTapGesture_) {
return;
}
//don't fire show font bar
cancelShowFontBar_ = YES;
[self performSelector:#selector(enableShowFontBar) withObject:nil afterDelay:1.0];
//method here
}
- (void)showFontEditBar {
//cancel here if tap gesture was fired first
if (cancelShowFontBar_) {
return;
}
if (fontEditBarExists_ == NO) {
//method here
//don't fire tap gesture
cancelTapGesture_ = YES;
[self performSelector:#selector(enableTapGesture) withObject:nil afterDelay:1.0];
}
}
- (void)enableTapGesture {
cancelTapGesture_ = NO;
}
- (void)enableShowFontBar {
cancelShowFontBar_ = NO;
}

I usually avoid subclassing unless the docs explicitly suggest, this worked for me. Long press and context menu. Whoops - Answer just loaded by #Beppe. Great minds think alike :-)
#interface TextViewSubclass ()
#property(assign,nonatomic) BOOL pressing;
#end
#implementation TextViewSubclass
#synthesize pressing=_pressing;
- (void)longPress:(UIEvent *)event {
NSLog(#"long press");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
self.pressing = YES;
[self performSelector:#selector(longPress:) withObject:event afterDelay:2.0];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
self.pressing = NO;
}
#end

This worked for me. I just wanted to control what happens when the user taps or long presses a link
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
// check for long press event
BOOL isLongPress = YES;
for (UIGestureRecognizer *recognizer in self.commentTextView.gestureRecognizers) {
if ([recognizer isKindOfClass:[UILongPressGestureRecognizer class]]){
if (recognizer.state == UIGestureRecognizerStateFailed) {
isLongPress = NO;
}
}
}
if (isLongPress) {
// user long pressed a link, do something
} else {
// user tapped on a link, do something
}
// return NO cause you dont want the normal behavior to occur
return NO;
}

Adaptation of Lucas Chwe's answer to work on iOS 9 (and 8). See comment in his answer.
The gist: invert the logic by starting with "no long press", then switch to "long press detected" if at least one UILongPressGestureRecognizer is in Began state.
BOOL isLongPress = NO;
for (UIGestureRecognizer *recognizer in textView.gestureRecognizers) {
if ([recognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
if (recognizer.state == UIGestureRecognizerStateBegan) {
isLongPress = YES;
}
}
}
Still a bit hackish, but working for iOS 8 and 9 now.

This is a little tricky, but it works for me.
I add a subclass of UIButton on top of my UITextView, check for long touches and pass them to UITextView this way:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
isLongTouchDetected = NO;
[self performSelector:#selector(longTouchDetected) withObject:nil afterDelay:1.0];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (isLongTouchDetected == YES) {
[super touchesCancelled:touches withEvent:event];
} else {
[super touchesEnded:touches withEvent:event];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(longTouchDetected) object:nil];
}
}
- (void)longTouchDetected {
isLongTouchDetected = YES;
// pass long touch to UITextView
}

For SWIFT [Easiest Way]
extension UITextField {
override public func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == "paste:" {
return false
}
return super.canPerformAction(action, withSender: sender)
}
override public func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.isKindOfClass(UILongPressGestureRecognizer) {
gestureRecognizer.enabled = false
}
super.addGestureRecognizer(gestureRecognizer)
return
}
}
Above code has also function for disable "PASTE" option in content menu.

Could you use the textViewDidChangeSelection method of the UITextViewDelegate protocol?

Related

TouchesEnded not working in iOS5, working fine in iOS4

I have a custom scrollView with the following method implemented:
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
//if not dragging send it on up the chain
if(!self.dragging){
[self.nextResponder touchesEnded:touches withEvent:event];
}else {
[super touchesEnded:touches withEvent:event];
}
}
And in My View Controller I have the following method:
-(void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event {
NSLog(#"TOUCH");
//---get all touches on the screen---
NSSet *allTouches = [event allTouches];
//---compare the number of touches on the screen---
switch ([allTouches count])
{
//---single touch---
case 1: {
//---get info of the touch---
UITouch *touch = [[allTouches allObjects] objectAtIndex:0];
//---compare the touches---
switch ([touch tapCount])
{
//---single tap---
case 1: {
NSLog(#"Single");
[self mySingleTapMethod];
} break;
//---double tap---
case 2: {
[self myDoubleTapMethod];
} break;
}
} break;
}
}
under iOS 4, this works perfectly, touches on the scrollview are recognized and the touchesEnded gets called in my view controller and all is right with the world. Under iOS 5x, however, the touchesEnded never gets fired. Does anyone know what the heck is going on/wrong?
Found the Answer here, basically if you want to do what I am doing you need to pass the touchesBegan up the chain as well.. because if a viewController didn't see the touchesBegan it won't get the TouchesEnded... So I modified the custom ScrollView to throw up the touchesBegan and everything now works fine in 5.0
Reference:
UIView touch handling behavior changed with Xcode 4.2?

Why is my application not detecting a shake gesture?

I'm trying to enable shake gestures within my application but I'm having a problem.
I have different view controllers, but on one of them (not the main view controller for the application) nothing happens in response to a shake gesture. What could be the problem?
The relevant code is as follows:
-(BOOL)canBecomeFirstResponder {
return YES;
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake)
{
NSLog (#"SHAKED");
}
}
I think you should be checking motion, not event. Like this:
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake ) {
NSLog(#"SHOOK");
}
}

UISegmentedControl register taps on selected segment

I have a segmented control where the user can select how to order a list. Works fine.
However, I would like that when an already selected segment is tapped, the order gets inverted. I have all the code in place, but I don't know how to register the taps on those segments. It seems the only control event you can use is UIControlEventValueChanged, but that isn't working (since the selected segment isn't actually changing).
Is there a solution for this? And if so, what is it?
Thanks in advance!
You can subclass UISegmentedControl, and then override setSelectedSegmentIndex:
- (void) setSelectedSegmentIndex:(NSInteger)toValue {
if (self.selectedSegmentIndex == toValue) {
[super setSelectedSegmentIndex:UISegmentedControlNoSegment];
} else {
[super setSelectedSegmentIndex:toValue];
}
}
If using IB, make sure you set the class of your UISegmentedControl to your subclass.
Now you can listen for the same UIControlEventValueChanged as you would normally, except if the user deselected the segment, you will see a selectedSegmentIndex equal to UISegmentedControlNoSegment:
-(IBAction) valueChanged: (id) sender {
UISegmentedControl *segmentedControl = (UISegmentedControl*) sender;
switch ([segmentedControl selectedSegmentIndex]) {
case 0:
// do something
break;
case 1:
// do something
break;
case UISegmentedControlNoSegment:
// do something
break;
default:
NSLog(#"No option for: %d", [segmentedControl selectedSegmentIndex]);
}
}
I think it is even a little better if you use -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event---as this is the behaviour of UISegmentedControl. Further, it seems you don't need to overload the -(void)setSelectedSegmentIndex:(NSInteger)toValue
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSInteger current = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
if (current == self.selectedSegmentIndex)
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
Wanted this myself. Took Julian's solution (thanks!) and modified slightly.
The following UISegmentedControl subclass simply triggers the UIControlEventValueChanged event even when the value didn't change, which obviously reads a bit weird, but works fine in my case and keeps things simple.
AKSegmentedControl.h
#import <UIKit/UIKit.h>
#interface AKSegmentedControl : UISegmentedControl {
}
#end
AKSegmentedControl.m
#import "AKSegmentedControl.h"
#implementation AKSegmentedControl
- (void)setSelectedSegmentIndex:(NSInteger)toValue {
// Trigger UIControlEventValueChanged even when re-tapping the selected segment.
if (toValue==self.selectedSegmentIndex) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
[super setSelectedSegmentIndex:toValue];
}
#end
This works on both iOS 4 and 5:
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
int oldValue = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
if ( oldValue == self.selectedSegmentIndex )
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
The current solution presented still does not work, because setSelectedSegmentIndex is never called unless really a new segment is tapped. At least in my case this never worked, I do use iOS5 though, perhaps this solution did work in iOS4. Anyway, this is my solution.
It needs one extra subclass method, which is the following:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self setSelectedSegmentIndex:self.selectedSegmentIndex];
[super touchesEnded:touches withEvent:event];
}
Good luck!
This was a pretty old question so I thought I'd add the rather simple solution I used when I ran into this in an iOS 5+ app.
All I did was drag a Tap Gesture Recognizer onto the UISegmentedControl in my .xib file. Check the connections for your new gesture recognizer to make sure the segmented control is the only gestureRecognizer outlet and then just wire up it's action to the method you want to fire in your controller.
This method should fire any time you touch the segmented control, so just check the selected index with perhaps an instance variable to see if the user touched the already selected segment.
#implementation MyViewController
#synthesize selectedSegment;
#synthesize segmentedControl;
// connected to Tap Gesture Recognizer's action
- (IBAction)segmentedControlTouched:(id)sender
{
NSLog(#"Segmented Control Touched");
if (selectedSegment == [segmentedControl selectedSegmentIndex]) {
// Do some cool stuff
}
selectedSegment = [segmentedControl selectedSegmentIndex];
}
#end
On iOS8 every answer here seem to either not work or trigger change twice. I came up with very simple solution - so I'd like to share it.
In subclass I only have:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self setSelectedSegmentIndex:UISegmentedControlNoSegment];
}
What it does is to reset selected segment to -1 before changing segmented control segment. It will happen in touchesEnded method.
Swift 5
class ReselectableSegmentedControl: UISegmentedControl {
// Captures existing selected segment on touchesBegan.
var oldValue: Int!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.oldValue = self.selectedSegmentIndex
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if self.oldValue == self.selectedSegmentIndex {
self.sendActions(for: .valueChanged)
}
}
}
From here
This works:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
int oldValue = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
if (oldValue == self.selectedSegmentIndex)
{
[super setSelectedSegmentIndex:UISegmentedControlNoSegment];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
In Swift 3 I could imagine at least 2 solutions as follows. For a special case I posted also a third solution, where the selected segment works as toggle button.
Solution 1:
Hint: This solution only works on momentary controlSegments! If you need a solution for stationary controls, choose Solution 2
The idea is to register a second event that fires up on touch-up-inside:
// this solution works only for momentary segment control:
class Solution1ViewController : UIViewController {
var current:Int = UISegmentedControlNoSegment
#IBOutlet weak var mySegmentedControl: UISegmentedControl! {
didSet {
guard mySegmentedControl != nil else { return }
self.mySegmentedControl!.addTarget(
self,
action:#selector(changeSelection(sender:)),
for: .valueChanged)
self.mySegmentedControl!.addTarget(
self,
action:#selector(changeSelection(sender:)),
for: .touchUpInside)
}
}
func changeSelection(sender: UISegmentedControl) {
if current == sender.selectedSegmentIndex {
// user hit the same button again!
print("user hit the same button again!")
}
else {
current = sender.selectedSegmentIndex
// user selected a new index
print("user selected a new index")
}
}
}
Solution 2:
The other way is to override the touch functions in UISegmentedControl and to fire the valueChanged even if the segment index has not changed. Therefore you could override the UISegmentedControl as follows:
// override UISegmentControl to fire event on second hit:
class Solution2SegmentControl : UISegmentedControl
{
private var current:Int = UISegmentedControlNoSegment
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
current = self.selectedSegmentIndex
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if current == self.selectedSegmentIndex {
self.sendActions(for: .valueChanged)
}
}
}
// solution2 works with stationary (default) segment controls
class Solution2ViewController : UIViewController {
var current:Int = UISegmentedControlNoSegment
#IBOutlet weak var mySegmentedControl: UISegmentedControl! {
didSet {
guard mySegmentedControl != nil else { return }
self.mySegmentedControl!.addTarget(
self,
action:#selector(changeSelection(sender:)),
for: .valueChanged)
}
}
func changeSelection(sender: UISegmentedControl) {
if current == sender.selectedSegmentIndex {
// user hit the same button again!
print("user hit the same button again!")
}
else {
current = sender.selectedSegmentIndex
// user selected a new index
print("user selected a new index")
}
}
}
Solution 3:
If your approach was to have the selected segment be a toggle button than Solution 2 could be changed to clean up the code like this:
class MyToggleSegmentControl : UISegmentedControl {
/// you could enable or disable the toggle behaviour here
var shouldToggle:Bool = true
private var current:Int = UISegmentedControlNoSegment
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
current = self.selectedSegmentIndex
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if shouldToggle, current == self.selectedSegmentIndex {
self.selectedSegmentIndex = UISegmentedControlNoSegment
}
}
}
Now we could clean up the changeSelection function as follows:
func changeSelection(sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case UISegmentedControlNoSegment:
print("none")
default:
print("selected: \(sender.selectedSegmentIndex)")
}
}
The first idea I had was wire up the Touch Up Inside or Touch Down actions to your sort method, but this doesn't seem to work.
The second idea is more of a work around, set the Momentary property on the segmented control. This will then fire a Value Did Change action every time it is tapped.
Big help! What I want to do is have the option of one or no buttons set - and when a button is set, a second tap unsets it. This is my modification:
- (void)setSelectedSegmentIndex:(NSInteger)toValue
{
// Trigger UIControlEventValueChanged even when re-tapping the selected segment.
if (toValue==self.selectedSegmentIndex) {
[super setSelectedSegmentIndex:UISegmentedControlNoSegment]; // notify first
[self sendActionsForControlEvents:UIControlEventValueChanged]; // then unset
} else {
[super setSelectedSegmentIndex:toValue];
}
}
Notify first lets you read the control to find the current setting before it's reset.
Here is my solution. The most elegant I think if you want the ValueChanged event to fire on every touches...
.h
#interface UISegmentedControlAllChanges : UISegmentedControl
#end
.m
#implementation UISegmentedControlAllChanges
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self sendActionsForControlEvents:UIControlEventValueChanged];
[super touchesEnded:touches withEvent:event];
}
#end
Based on Bob de Graaf's answer:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self sendActionsForControlEvents:UIControlEventAllTouchEvents];
[super touchesEnded:touches withEvent:event];
}
Note the use of UIControlEventAllTouchEvents instead of UIControlEventValueChanged.
Also there's no need to call -(void)setSelectedSegmentIndex:.
Below the piece of code that did work for me. Repeated tap on the same segment will deselect it.
#implementation WUUnselectableSegmentedControl
{
NSUInteger _selectedIndexBeforeTouches;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
_selectedIndexBeforeTouches = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
if (_selectedIndexBeforeTouches == self.selectedSegmentIndex)
{
// Selection didn't change after touch - deselect
self.selectedSegmentIndex = UISegmentedControlNoSegment;
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
#end
iOS 9 solution. Override UISegmentedControl with such class:
#interface SegmentedControl()
#property (nonatomic) NSInteger previousCurrent;
#end
#implementation SegmentedControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.previousCurrent = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
if (self.selectedSegmentIndex == self.previousCurrent) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
self.previousCurrent = NSNotFound;
}
#end
Seems like a fun question to answer. This scheme expands upon Steve E's and yershuachu's solutions. This version uses a UITapGestureRecognizer to capture all touches and which sets the selectedSegmentIndex to -1; but it also passes on all touches to the UISegmentedControl so it can handle any normal touches. No subclassing is required.
UISegmentedControl *mySegControl;
- (void)viewDidLoad
{
[super viewDidLoad];
[mySegControl addTarget:self action:#selector(segmentAction:)
forControlEvents:UIControlEventValueChanged];
// allow the a seg button tap to be seen even if already selected
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(unitsSegTap:)];
tapGesture.cancelsTouchesInView = NO; // pass touches through for normal taps
[mySegControl addGestureRecognizer:tapGesture];
// continue setup ...
}
- (void)segTap:(UITapGestureRecognizer *)sender
{
mySegControl.selectedSegmentIndex = -1;
}
// called for UIControlEventValueChanged
- (void)segmentAction:(UISegmentedControl *)sender
{
NSInteger index = sender.selectedSegmentIndex;
NSLog(#"unitsSegmentAction %d",(int)index);
// process the segment
}
I'm using KVO to invert already selected segment for iOS8.
#import "QCSegmentedControl.h"
static void *QCSegmentedControlContext = &QCSegmentedControlContext;
#implementation QCSegmentedControl
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self registerKVO];
}
return self;
}
- (void)dealloc {
[self removeObserver:self forKeyPath:#"selectedSegmentIndex"];
}
#pragma mark -
- (void)registerKVO {
[self addObserver:self
forKeyPath:#"selectedSegmentIndex"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:QCSegmentedControlContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == QCSegmentedControlContext) {
NSNumber *new = change[NSKeyValueChangeNewKey];
NSNumber *old = change[NSKeyValueChangeOldKey];
if (new.integerValue == old.integerValue) {
self.selectedSegmentIndex = UISegmentedControlNoSegment;
}
}
}
#end
The selected answer's solution did not work for me as of current iOS v8.x. But #Andy offer a solution and I will complete:
Subclass the UISegmentedControl, and overwrite touchesEnded method in the subclass:
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self sendActionsForControlEvents:UIControlEventAllTouchEvents];
[super touchesEnded:touches withEvent:event];
}
In the class where you intend to use the UISegmentedControl, create an iVar currentSelectedSegIndex. Add the UIControlEventAllTouchEvents beside your UIControlEventValueChanged action methods:
NSInteger currentSelectedSegIndex;
[aSegmentedController addTarget:self action:#selector(aSegmentedControllerSelection:) forControlEvents:UIControlEventValueChanged];
[aSegmentedController addTarget:self action:#selector(aSegmentedControllerAllTouchEvent:) forControlEvents:UIControlEventAllTouchEvents];
Implement the two action methods:
-(void)aSegmentedControllerAllTouchEvent:(MyUISegmentedControl *)seg
{
seg.selectedSegmentIndex = UISegmentedControlNoSegment;
}
-(void)aSegmentedControllerSelection:(MyUISegmentedControl *)seg
{
if (currentSelectedSegIndex == seg.selectedSegmentIndex)
{
seg.selectedSegmentIndex = UISegmentedControlNoSegment;
currentSelectedSegIndex = NSIntegerMax;
NSLog(#"%s Deselected.", __PRETTY_FUNCTION__);
// do your stuff if deselected
return;
}
NSLog(#"%s Selected index:%u", __PRETTY_FUNCTION__, seg.selectedSegmentIndex);
currentSelectedSegIndex = seg.selectedSegmentIndex;
//do your stuff if selected index
}
Because the UISegmentedControl is in charge of setting the segment, it should only reset the state. I modify a little the suggestions of other guys for swift 5:
//MARK: Custom segmentedControl
/// Every element works like a flipflop [see](http://stackoverflow.com/questions/17652773/how-to-deselect-a-segment-in-segmented-control-button-permanently-till-its-click)
#IBDesignable class GRSegmentedControl: UISegmentedControl {
private let url = Bundle.main.url(forResource: "PlopChoiceCntrl", withExtension: "aiff")!
private var soundID:SystemSoundID = 0
override init(items: [Any]?) {
AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
super.init(items: items)
}
required init?(coder: NSCoder) {
AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
super.init(coder: coder)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
AudioServicesPlaySystemSound(soundID)
let previousSelectedSegmentIndex = self.selectedSegmentIndex
super.touchesEnded(touches, with: event)
if previousSelectedSegmentIndex == self.selectedSegmentIndex {
if let touch = touches.first{
let touchLocation = touch.location(in: self)
if bounds.contains(touchLocation) {
self.selectedSegmentIndex = UISegmentedControl.noSegment
}
}
}
}
}

Shake Detection iPhone 3.0 not working

I have a ViewController that works perfectly with a button that trigger an action. I would like to replace the button with a shake event so I've googled it around and created a ShakeDetector class that ineherits from UIView
and my implementation is as follow:
#implementation ShakeDetector
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake )
{
// User was shaking the device. Post a notification named "shake".
//[[NSNotificationCenter defaultCenter] postNotificationName:#"spin" object:self];
NSLog(#"sss");
}
}
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
}
#end
But I can't make it work... any help?
Thanks
Put :
-(BOOL)canBecomeFirstResponder
{
return YES;
}
and for your view :
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
- (void)viewDidDisappear:(BOOL)animated {
[self resignFirstResponder];
[super viewDidDisappear:animated];
}
You can also write it in viewWillAppear and viewWillDisappear

Scrolling with two fingers with a UIScrollView

I have an app where my main view accepts both touchesBegan and touchesMoved, and therefore takes in single finger touches, and drags. I want to implement a UIScrollView, and I have it working, but it overrides the drags, and therefore my contentView never receives them. I'd like to implement a UIScrollview, where a two finger drag indicates a scroll, and a one finger drag event gets passed to my content view, so it performs normally. Do I need create my own subclass of UIScrollView?
Here's my code from my appDelegate where I implement the UIScrollView.
#implementation MusicGridAppDelegate
#synthesize window;
#synthesize viewController;
#synthesize scrollView;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after app launch
//[application setStatusBarHidden:YES animated:NO];
//[window addSubview:viewController.view];
scrollView.contentSize = CGSizeMake(720, 480);
scrollView.showsHorizontalScrollIndicator = YES;
scrollView.showsVerticalScrollIndicator = YES;
scrollView.delegate = self;
[scrollView addSubview:viewController.view];
[window makeKeyAndVisible];
}
- (void)dealloc {
[viewController release];
[scrollView release];
[window release];
[super dealloc];
}
In SDK 3.2 the touch handling for UIScrollView is handled using Gesture Recognizers.
If you want to do two-finger panning instead of the default one-finger panning, you can use the following code:
for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
}
}
For iOS 5+, setting this property has the same effect as the answer by Mike Laurence:
self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;
One finger dragging is ignored by panGestureRecognizer and so the one finger drag event gets passed to the content view.
In iOS 3.2+ you can now achieve two-finger scrolling quite easily. Just add a pan gesture recognizer to the scroll view and set its maximumNumberOfTouches to 1. It will claim all single-finger scrolls, but allow 2+ finger scrolls to pass up the chain to the scroll view's built-in pan gesture recognizer (and thus allow normal scrolling behavior).
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
You need to subclass UIScrollView (of course!). Then you need to:
make single-finger events to go to your content view (easy), and
make two-finger events scroll the scroll view (may be easy, may be hard, may be impossible).
Patrick's suggestion is generally fine: let your UIScrollView subclass know about your content view, then in touch event handlers check the number of fingers and forward the event accordingly. Just be sure that (1) the events you send to content view don't bubble back to UIScrollView through the responder chain (i.e. make sure to handle them all), (2) respect the usual flow of touch events (i.e. touchesBegan, than some number of {touchesBegan, touchesMoved, touchesEnded}, finished with touchesEnded or touchesCancelled), especially when dealing with UIScrollView. #2 can be tricky.
If you decide the event is for UIScrollView, another trick is to make UIScrollView believe your two-finger gesture is actually a one-finger gesture (because UIScrollView cannot be scrolled with two fingers). Try passing only the data for one finger to super (by filtering the (NSSet *)touches argument — note that it only contains the changed touches — and ignoring events for the wrong finger altogether).
If that does not work, you are in trouble. Theoretically you can try to create artificial touches to feed to UIScrollView by creating a class that looks similar to UITouch. Underlying C code does not check types, so maybe casting (YourTouch *) into (UITouch *) will work, and you will be able to trick UIScrollView into handling the touches that did not really happen.
You probably want to read my article on advanced UIScrollView tricks (and see some totally unrelated UIScrollView sample code there).
Of course, if you can't get it to work, there's always an option of either controlling UIScrollView's movement manually, or use an entirely custom-written scroll view. There's TTScrollView class in Three20 library; it does not feel good to the user, but does feel good to programmer.
This answers are a mess since you can only find the correct answer by reading all the other answers and the comments (closest answer got the question backwards). The accepted answer is too vague to be useful, and suggests a different method.
Synthesizing, this works
// makes it so that only two finger scrolls go
for (id gestureRecognizer in self.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *panGR = gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
panGR.maximumNumberOfTouches = 2;
}
}
This requires two fingers for a scroll. I've done this in a subclass, but if not, just replace self.gestureRecognizers with myScrollView.gestureRecognizers and you're good to go.
The only thing that I added is using id to avoid an ugly cast :)
This works but can get quite messy if you want your UIScrollView to do zoom too... the gestures don't work correctly, since pinch-to-zoom and scroll fight it out. I'll update this if I find a suitable answer.
we managed to implement similar functionality in our iPhone drawing app by subclassing UIScrollView and filtering events depending on number of touches in simple and rude way:
//OCRScroller.h
#interface OCRUIScrollView: UIScrollView
{
double pass2scroller;
}
#end
//OCRScroller.mm
#implementation OCRUIScrollView
- (id)initWithFrame:(CGRect)aRect {
pass2scroller = 0;
UIScrollView* newv = [super initWithFrame:aRect];
return newv;
}
- (void)setupPassOnEvent:(UIEvent *)event {
int touch_cnt = [[event allTouches] count];
if(touch_cnt<=1){
pass2scroller = 0;
}else{
double timems = double(CACurrentMediaTime()*1000);
pass2scroller = timems+200;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
pass2scroller = 0;
[super touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
return YES;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
double timems = double(CACurrentMediaTime()*1000);
if (pass2scroller == 0 || timems> pass2scroller){
return NO;
}
return YES;
}
#end
ScrollView setuped as follows:
scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect];
scroll_view.contentSize = img_size;
scroll_view.contentOffset = CGPointMake(0,0);
scroll_view.canCancelContentTouches = YES;
scroll_view.delaysContentTouches = NO;
scroll_view.scrollEnabled = YES;
scroll_view.bounces = NO;
scroll_view.bouncesZoom = YES;
scroll_view.maximumZoomScale = 10.0f;
scroll_view.minimumZoomScale = 0.1f;
scroll_view.delegate = self;
self.view = scroll_view;
simple tap does nothing (you can handle it in the way you need), tap with two fingers scrolls/zooms view as expected. no GestureRecognizer is used, so works from iOS 3.1
I've got a further improvement to the code above. The problem was, that even after we set setCanCancelContentTouches:NO We have the problem, that a zoom gesture will interrupt with the content. It won't cancel the content touch but allow zooming in the meantime. TO prevent this i lock the zooming by setting the minimumZoomScale and maximumZoomScale to the same values everytime, the timer fires.
A quite strange behavior is that when a one finger event gets canceled by a two finger gesture within the allowed time period, the timer will be delayed. It gets fired after the touchCanceled Event gets called. So we have the problem, that we try to lock the zooming although the event is already canceled and therefore disable zooming for the next event.
To handle this behavior the timer callback method checks against if touchesCanceled was called before.
#implementation JWTwoFingerScrollView
#pragma mark -
#pragma mark Event Passing
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
}
timerWasDelayed = NO;
}
}
return self;
}
-(void)lockZoomScale {
zoomScale[0] = self.minimumZoomScale;
zoomScale[1] = self.maximumZoomScale;
[self setMinimumZoomScale:self.zoomScale];
[self setMaximumZoomScale:self.zoomScale];
NSLog(#"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
-(void)unlockZoomScale {
if (zoomScale[0] != -1 && zoomScale[1] != -1) {
[self setMinimumZoomScale:zoomScale[0]];
[self setMaximumZoomScale:zoomScale[1]];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
NSLog(#"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"began %i",[event allTouches].count);
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
//if one finger touch gets canceled by two finger touch, this timer gets delayed
// so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming
-(void)firstTouchTimerFired:(NSTimer*)timer {
NSLog(#"fired");
[self setCanCancelContentTouches:NO];
//if already locked: unlock
//this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock!
if (timerWasDelayed) {
[self unlockZoomScale];
}
else {
[self lockZoomScale];
}
timerWasDelayed = NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(#"moved %i",[event allTouches].count);
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"ended %i",[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
[self unlockZoomScale];
}
//[self setCanCancelContentTouches:NO];
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"canceled %i",[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
[self unlockZoomScale];
timerWasDelayed = YES;
}
#end
Bad news: iPhone SDK 3.0 and up, don't pass touches to -touchesBegan: and -touchesEnded: **UIScrollview**subclass methods anymore. You can use the touchesShouldBegin and touchesShouldCancelInContentView methods that is not the same.
If you really want to get this touches, have one hack that allow this.
In your subclass of UIScrollView override the hitTest method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
This will pass to you subclass this touches, however you can't cancel the touches to UIScrollView super class.
What I do is have my view controller set up the scroll view:
[scrollView setCanCancelContentTouches:NO];
[scrollView setDelaysContentTouches:NO];
And in my child view I have a timer because two-finger touches usually start out as one finger followed quickly by two fingers.:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Hand tool or two or more touches means a pan or zoom gesture.
if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) {
[[self parentScrollView] setCanCancelContentTouches:YES];
[firstTouchTimer invalidate];
firstTouchTimer = nil;
return;
}
// Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch.
[[self parentScrollView] setCanCancelContentTouches:NO];
anchorPoint = [[touches anyObject] locationInView:self];
firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:#selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
firstTouchTimeStamp = event.timestamp;
}
If a second touchesBegan: event comes in with more than one finger, the scroll view is allowed to cancel touches. So if the user pans using two fingers, this view would get a touchesCanceled: message.
This seems to be the best resource for this question on the internet. Another close solution can be found here.
I have solved this issue in a very satisfactory manner in a different way, essentially by supplanting my own gesture recognizer into the equation. I strongly recommend that anyone who is trying to achieve the effect requested by the original poster consider this alternative over aggressive subclassing of UIScrollView.
The following process will provide:
A UIScrollView containing your custom view
Zoom and Pan with two fingers (via UIPinchGestureRecognizer)
Your view's event processing for all other touches
First, let's assume you have a view controller and its view. In IB, make the view a subview of a scrollView and adjust the resize rules of your view so that it does not resize. In the attributes of the scrollview, turn on anything that says "bounce" and turn off "delaysContentTouches". Also you must set the zoom min and max to other than the default of 1.0 for, as Apple's docs say, this is required for zooming to work.
Create a custom subclass of UIScrollView, and make this scrollview that custom subclass. Add an outlet to your view controller for the scrollview and connect them up. You're now totally configured.
You will need to add the following code to the UIScrollView subclass so that it transparently passes touch events (I suspect this could be done more elegantly, perhaps even bypassing the subclass altogether):
#pragma mark -
#pragma mark Event Passing
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return NO;
}
Add this code to your view controller:
- (void)setupGestures {
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinchGesture:)];
[self.view addGestureRecognizer:pinchGesture];
[pinchGesture release];
}
- (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
if ( sender.state == UIGestureRecognizerStateBegan ) {
//Hold values
previousLocation = [sender locationInView:self.view];
previousOffset = self.scrollView.contentOffset;
previousScale = self.scrollView.zoomScale;
} else if ( sender.state == UIGestureRecognizerStateChanged ) {
//Zoom
[self.scrollView setZoomScale:previousScale*sender.scale animated:NO];
//Move
location = [sender locationInView:self.view];
CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y));
[self.scrollView setContentOffset:offset animated:NO];
} else {
if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 )
[self.scrollView setZoomScale:1.0 animated:YES];
}
}
Please note that in this method there are references to a number of properties you must define in your view controller's class files:
CGFloat previousScale;
CGPoint previousOffset;
CGPoint previousLocation;
CGPoint location;
Ok that's it!
Unfortunately I could not get the scrollView to show its scrollers during the gesture. I tried all of these strategies:
//Scroll indicators
self.scrollView.showsVerticalScrollIndicator = YES;
self.scrollView.showsVerticalScrollIndicator = YES;
[self.scrollView flashScrollIndicators];
[self.scrollView setNeedsDisplay];
One thing I really enjoyed is if you'll look at the last line you'll note that it grabs any final zooming that's around 100% and just rounds it to that. You can adjust your tolerance level; I had seen this in Pages' zoom behavior and thought it would be a nice touch.
I put this in the viewDidLoad method and this accomplishes the scroll view handling the two touch pan behavior and another pan gesture handler handling the one touch pan behavior -->
scrollView.panGestureRecognizer.minimumNumberOfTouches = 2
let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGR.minimumNumberOfTouches = 1
panGR.maximumNumberOfTouches = 1
scrollView.gestureRecognizers?.append(panGR)
and in the handlePan method which is a function attached to the ViewController there is simply a print statement to verify that the method is being entered -->
#IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)")
}
HTH
Check out my solution:
#import “JWTwoFingerScrollView.h”
#implementation JWTwoFingerScrollView
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
NSLog(#“%#”,[r class]);
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
}
}
}
return self;
}
-(void)firstTouchTimerFired:(NSTimer*)timer {
[self setCanCancelContentTouches:NO];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(firstTouchTimerFired:) userInfo: nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#“ended %i”,[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#“canceled %i”,[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
}
#end
It does not delays the first touch and does not stop when the user touches with two fingers after using one. Still it allows to cancel a just started one touch event using a timer.
Yes, you'll need to subclass UIScrollView and override its -touchesBegan: and -touchesEnded: methods to pass touches "up". This will probably also involve the subclass having a UIView member variable so that it knows what it's meant to pass the touches up to.
Kenshi's answer in Swift 4
for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
if (gestureRecognizer is UIPanGestureRecognizer) {
let panGR = gestureRecognizer as? UIPanGestureRecognizer
panGR?.minimumNumberOfTouches = 2
}
}