Okay I tested the following with the tabbed application template on Xcode 4.5/iOS 6.
Created a tabbed application.
Created a UIButton subclass called SampleButton and implemented
the following mothods:
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
}
- (void) touchesMoved:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
}
Added this SampleButton to the first tab.
Added breakpoints to all the touch methods.
Run on device.
Tested that all the touch methods are firing as expected.
Now touch the SampleButton and then also press the second tab.
RESULT: View switches to second tab but touchesCancelled and/or touchesEnded are never called in SampleButton. Shouldn't one or the other of those fire if the view changes while I'm touching that button? This is proving to be a huge issue because, in my app I'm playing a sound while that button is down and it never stops playing if the user switches tabs while pressing it. Seems like this used to work fine in iOS3 and iOS4.
It appears that when a view is removed from its window, it dissociates itself from any touches that were associated with it. So when the touch finally ends, the system doesn't send touchesEnded:… or touchesCancelled:… to the view.
Workaround by disabling tab switching
If you want to just disable tab switching while the button is pressed, you can do that by giving the tab bar controller a delegate and having the delegate return NO from tabBarController:shouldSelectViewController:. For example, in the your test app, you can have FirstViewController make itself the tab bar controller's delegate:
- (void)viewWillAppear:(BOOL)animated {
self.tabBarController.delegate = self;
}
And the view controller can allow the tab bar controller to select a tab only when the button is not pressed (highlighted):
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
return !_button.highlighted;
}
Workaround by detecting button highlight resetting to NO
When the button is removed from its window, it resets its highlighted property to NO. So one generic way to work around this problem is by using key-value observing (KVO) to monitor the button's state (instead of relying on the button to send you actions). Set yourself up as an observer of the button's highlighted property like this:
static int kObserveButtonHighlightContext;
- (void)viewDidLoad {
[super viewDidLoad];
[_button addObserver:self forKeyPath:#"highlighted"
options:NSKeyValueObservingOptionOld
context:&kObserveButtonHighlightContext];
}
- (void)dealloc {
[_button removeObserver:self forKeyPath:#"highlighted"
context:&kObserveButtonHighlightContext];
}
I discovered in testing that the button sends an extra KVO notification when it's removed from the window, before it resets its highlighted property back to NO. So when handling the KVO notification, check that the value has actually changed:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == &kObserveButtonHighlightContext) {
if ([change[NSKeyValueChangeOldKey] boolValue] != _button.highlighted) {
[self updatePlaybackForButtonState];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Finally, start or stop playback according to the highlighted property of the button:
- (void)updatePlaybackForButtonState {
if (_button.highlighted) {
NSLog(#"start playback");
} else {
NSLog(#"end playback");
}
}
Subclassing the button seems like the hard way to do this. Just tell the audio player to stop playing in the viewDidDisappear:animated: method, as R.A. suggested.
I worked around this by setting targets for the actions instead of using the touch methods:
[self addTarget:self action:#selector(handleKeyPressed) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:#selector(handleKeyReleased) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:#selector(handleKeyReleased) forControlEvents:UIControlEventTouchCancel];
These seems to get fired correctly even when the view swaps out.
Related
My custom button touchupinside event not fire when I put some code in my custom button
Note: When I tap on button this method is fire but touchUpInside method not fire
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(#"touchBegan");
[Singleton playSoundForEvent:Sound_ButtonTap];
}
If I remove above code then working fine.
Above code reason : I don't put tap sound everywhere on touchupinside.
I would like only One line code in whole project.
You forgot the call to super
[super touchesBegan:touches withEvent:event];
The reason it doesn't work without the call to super, is that a button needs to register touch events (touchesBegan, touchesEnded, touchesMoved, touchesCanceled) to be able to map them to UIControlEvent, and only then fire actions with sendAction.
Full code
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
NSLog(#"touchBegan");
[Singleton playSoundForEvent:Sound_ButtonTap];
}
Please check this code and test again I hope It will working fine.
[super touchesBegan:touches withEvent:event];
I have a custom view which is not being deallocated. I dismiss controller on close button pressed. Now if I only press the button the view is deallocated alright. But If press button with one finger with other finger touching the view its not deallocated on dismiss but on the next touch event.
Its UITouch which is keeping the reference of my view and not releasing it. How can I fix this?
Here is my code for my close action:
- (IBAction)closePressed:(UIButton *)sender {
NSLog(#"Close pressed");
if (self.loader)
[self.loader cancelJsonLoading];
[self.plView quit];
[self dismissViewControllerAnimated:YES completion:nil];
}
Did you try to call:
[self.view resignFirstResponder];
That should cancel all pending UITouches.
If this doesn't work, you can keep trace of your touches:
define a NSMutableSet where you store current touches:
NSMutableSet *_currentTouches;
in your init():
_currentTouches = [[NSMutableSet alloc] init];
And implement:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super.touchesBegan:touches withEvent:event];
[_currentTouches unionSet:touches]; // record new touches
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super.touchesEnded:touches withEvent:event];
[_currentTouches minusSet:touches]; // remove ended touches
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super.touchesEnded:touches withEvent:event];
[_currentTouches minusSet:touches]; // remove cancelled touches
}
Then, when you need to clean your touches (when you release your view for instance):
- (void)cleanCurrentTouches {
self touchesCancelled:_currentTouches withEvent:nil];
_currentTouchesremoveAllObjects];
}
It is, I think, a bit hacky, but the doc says:
When an object receives a touchesCancelled:withEvent: message it
should clean up any state information that was established in its
touchesBegan:withEvent: implementation.
I have set this UIViewController to be the delegate for the UITextField in the viewDidLoad with this line: self.nameInputTextField.delegate = self;.
I have set the delegate on the class as well by adding <UITextFieldDelegate> to the #interface declaration.
When I select the nextButton, in the method that is called, I have tried [self.nameInputTextField resignFirstResponder] as well as [self.view endEditing:YES] one line before I push the new view controller.
The rest of the class does not manipulate the firstResponder.
I've also implemented the UITextField delegate method
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[self.nameInputTextField resignFirstResponder];
return NO;
}
I haven't found any related questions after extensive searching. There are many similar ones about resigning keyboards, but not regarding the timing of the keyboard resignation being postponed until after the view transition is complete. Note- if you reload this url in your browser, you'll see the gif again from the beginning.
Hide keyboard anywhere in ios :
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UIView * txt in self.view.subviews){
if ([txt isKindOfClass:[UITextField class]] && [txt isFirstResponder]) {
[txt resignFirstResponder];
}
}
}
OR
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
Resign your keyboard on viewWillDisappear and the problem should be solved.
Edit
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
self.nameInputTextField.text = #"";
[self.nameInputTextField resignFirstResponder];
}
I am making a custom iOS keyboard and have a UIControl subclass to represent my button. I am trying to get the same behaviour as the normal iOS keyboard:
User begins touch on one button
User drags over other buttons (need to detect this so they can highlight/dehighlight accordingly)
Register the actual keyboard "press" when the user lifts their finger; that is, the touch ends
I am testing using the touch tracking methods like this:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[super beginTrackingWithTouch:touch withEvent:event];
NSLog(#"Begin for %#", [self label]);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[super continueTrackingWithTouch:touch withEvent:event];
NSLog(#"Continue for %#", [self label]);
return YES;
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
[super endTrackingWithTouch:touch withEvent:event];
NSLog(#"End for %#", [self label]);
}
These methods are all called, except they are only ever called on the UIControl where the touch began.
What is the best way to recognise touches coming and going across all my buttons? Do I have to do it all via the parent view?
I'll select a better answer if offered... but in case anybody finds this by search, I've managed to get what I need this way:
Set userInteractionEnabled to NO for the button class (UIControl subclass)
Don't override any touch methods in the button class
Implement touchesBegan:withEvent:, touchesMoved:withEvent: and touchesEnded:withEvent: in the view controller
On each event, extract the location from the UITouch object
Iterate over all of the button subviews and find the one containing the touch location:
- (MyButton *)buttonForTouch:(UITouch *)touch
{
CGPoint windowLocation = [touch locationInView:keyboardView];
for (MyButton *button in buttons) {
if (CGRectContainsPoint([button frame], windowLocation)) {
return button;
}
}
return nil;
}
Having determined which button the user is interacting with, make the view controller send messages to the relevant buttons to adjust their appearance
If appropriate, keep a reference to the UITouch instance in touchesBegan:withEvent: so you can be sure that you're tracking the same one in the other methods
I think that you should have a single big UIControl which has different subviews (like UIButton) but tracks touches by itself like you did already but finds out which subview to highlight depending on the touch position.
So let me clarify first off with showing what code i have implemented
-(BOOL)canBecomeFirstResponder
{
return YES;
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self becomeFirstResponder];
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self resignFirstResponder];
[super viewWillDisappear:animated];
}
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake)
{
NSLog(#"FUUU");
}
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake)
{
NSLog(#"FUUU");
}
}
this is all in a UIViewController class
and in - (void) applicationDidFinishLaunching:(UIApplication*)application i have set
[application setApplicationSupportsShakeToEdit:YES];
Yet it does nothing, not nary single motion detected. I'm not sure what else to do. This seems to have worked for many other people, so i am baffled as to why i am different...Could it be because it's through a UINavigationController? or because i load the main application via a plist and my main looks like so
retVal = UIApplicationMain(argc, argv, nil, nil);
I am quite thoroughly stumped.
In order to detect shake gesture you need to subclass the UIView and implement the following methods (Do not implement these methods on UIViewController):
(BOOL)canBecomeFirstResponder
(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
Then add the subclassed view to UIViewController and make it first responder by calling -becomeFirstResponder on it.
You don't need to set [application setApplicationSupportsShakeToEdit:YES]; because this is the default value and iOS use this only for Shake to Undo/Redo.
If you want to capture the motion in your view controller you need to set this property to NO [application setApplicationSupportsShakeToEdit:NO]; and handle it by yourself.
As described in Event Handling Programming Guide.
Hope this helps.
PS: Just in case, you call the wrong super in your viewDidAppear method. This should be :
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
Thank you all for your responses but i ended up using a different method i accessed the accelerometer in the app delegate and then send a nsnotifcation via the nsnotification center to the current ui navigation controller displayed
- (void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
[[NSNotificationCenter defaultCenter] postNotificationName:notificationName object:nil userInfo:nil];
}