UIAlertController in iOS 8.x cannot response touch event, is the touch event sent to the key window object? - touch

I have a problem abount UIAlertController. It cannot receive any touch events and be dismissed.
I persent the alert in this way:
UIViewController *controller =[UIApplication sharedApplication].keyWindow.rootViewController
[controller presentViewController:alertController animated:YES completion:nil];
After this time, the alert view was presented.But when I try to click ‘OK’, there’s no response. So I click the blank area around the alert view, the button on the lower layer receive it and make a response.
In my application, there’s a third-party SDK which provides a suspension button and login/logout UI. The button will always be there, but UI wouldn’t be presented except I call it.
The SDK will add two windows into the application’s window array when it was initialized.
So there’re three objects in ‘[UIApplication sharedApplication].windows’.
Look like this:
[0]:”<TS_PlatformWindow: 0x16e15ac0; baseClass = UIWindow; frame = (0 0; 568 320); gestureRecognizers = <NSArray: 0x186a85b0>; layer = <UIWindowLayer: 0x186a8a20>>",
[1]:”<UIWindow: 0x16e1a060; frame = (0 0; 568 320); autoresize = W+H; tag = 10000000; gestureRecognizers = <NSArray: 0x16e19af0>; layer = <UIWindowLayer: 0x16e19f40>>",
[2]:“<UITextEffectsWindow: 0x16f576c0; frame = (0 0; 568 320); opaque = NO; gestureRecognizers = <NSArray: 0x16f57120>; layer = <UIWindowLayer: 0x16f574b0>>"
The window of my application at index 1, windows at index 0 and 2 are third-party SDK’s.
As the official document in iOS Developer Library, here is the quote:
‘An event travels along a specific path until it is delivered to an object that can handle it. First, the singleton UIApplication object takes an event from the top of the queue and dispatches it for handling.
Typically, it sends the event to the app’s key window object, which passes the event to an initial object for handling. The initial object depends on the type of event.’
from https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html
As my understanding, touch events should be sent to the key window of the app, but actually the window of my application isn’t.
The document use the word ’Typically’, so I’m not sure if the situation in my App is an exception. There’s no mention about how the event delivered if there’re multiply windows exist.
So I change my code like this:
UIViewController *controller =[UIApplication sharedApplication].keyWindow.rootViewController
for (UIWindow *win in wins.reverseObjectEnumerator) {
UIViewController *rootController = win.rootViewController;
if (rootController != nil) {
controller = rootController;
break;
}
}
[controller presentViewController:alertController animated:YES completion:nil];
The default value of ‘controller’ will be assigned to ‘keyWindow.rootViewController’
Travel the window array of the application in reverse order, and select the top-most object’s ‘rootViewController’ to present the alert.
It works!
This problem confuse me if this is the correct solution? Or there is another document could give an explanation?
PS:
Every operations are on the main thread

I cannot access to background view from UIAlertController, therefore using addGestureRecognizer for current keyWindow -
class MyAlertController: UIAlertController, UIGestureRecognizerDelegate {
var tapGesture : UITapGestureRecognizer?
override func viewDidAppear(animated: Bool)
{
tapGesture = UITapGestureRecognizer(target: self, action:Selector("tapGesture:"))
tapGesture!.delegate = self
UIApplication.sharedApplication().keyWindow?.addGestureRecognizer(tapGesture!)
}
func tapGesture(sender: UITapGestureRecognizer? = nil)
{
UIApplication.sharedApplication().keyWindow?.removeGestureRecognizer(tapGesture!)
self.dismissViewControllerAnimated(true, completion: nil)
}
}

Related

ECSlidingViewController Zoom Animation

I'm trying to set my sliding transition with Zoom Animation in ECSLidingViewController2.
Like in the TransitionFun example (https://github.com/ECSlidingViewController/ECSlidingViewController/blob/master/Examples/TransitionFun/TransitionFun/METransitionsViewController.m), I'm doing this in viewDidLoad of my topView NavigationController:
id<ECSlidingViewControllerDelegate> transition = [[MEZoomAnimationController alloc] init];
self.slidingViewController.delegate = transition;
self.slidingViewController.topViewAnchoredGesture = ECSlidingViewControllerAnchoredGestureTapping | ECSlidingViewControllerAnchoredGesturePanning;
self.slidingViewController.customAnchoredGestures = #[];
[self.navigationController.view removeGestureRecognizer:self.dynamicTransitionPanGesture];
[self.navigationController.view addGestureRecognizer:self.slidingViewController.panGesture];
I'm adopting the ECSlidingViewControllerDelegate protocol in the NavigationController header file.
But it crashes in the ECSlidingViewController.m, at the first if condition of this method I get an EXC_BAD_ACCESS(1) error:
- (CGRect)frameFromDelegateForViewController:(UIViewController *)viewController
topViewPosition:(ECSlidingViewControllerTopViewPosition)topViewPosition {
CGRect frame = CGRectInfinite;
if ([(NSObject *)self.delegate respondsToSelector:#selector(slidingViewController:layoutControllerForTopViewPosition:)]) {
id<ECSlidingViewControllerLayout> layoutController = [self.delegate slidingViewController:self
layoutControllerForTopViewPosition:topViewPosition];
if (layoutController) {
frame = [layoutController slidingViewController:self
frameForViewController:viewController
topViewPosition:topViewPosition];
}
}
return frame;
}
Anyone having the same issue?
thx
This is what i do in the viewDidLoad: method of my view controller and it works great:
id<ECSlidingViewControllerDelegate> transition = self.zoomAnimationController;
self.slidingViewController.delegate = transition;
self.slidingViewController.topViewAnchoredGesture = ECSlidingViewControllerAnchoredGestureTapping | ECSlidingViewControllerAnchoredGesturePanning;
self.slidingViewController.customAnchoredGestures = #[];
[self.navigationController.view addGestureRecognizer:self.slidingViewController.panGesture];
If, like me, you aren't trying to implement the dynamic transition, you should get rid of the line you have that says:
[self.navigationController.viewremoveGestureRecognizer:self.dynamicTransitionPanGesture];
IMHO its what could be causing your EXC_BAD_ACCESS(1) error since you haven't actually set the dynamicTransitionPanGesture gesture recognizer.

MFMessageComposeViewController: Can I present this with a custom animation?

Here's what I want to do. Not sure if it's possible, but if there's an answer with clean code that's app store acceptable I'm more than happy to give a bounty for it!
- Present an MFMessageComposeViewController with a custom animation. (It's
a modal view controller).
- I then want to animate this MFMessageComposeViewController off with a
custom animation, while at the same time animating on a new instance
of MFMessageComposeController. (Again, custom animation).
For the sake of this question, let's make it simple and say that the first MFMessageComposeViewController should slide in from the right, and then it should slide off to the left (when the send button is pressed) while the new instance slides on from the right (pretty much like the default push animation for a nav controller).
If this is impossible, an explanation of why there's no way to do this would be great :)
No. But you can do a trick, which will looks like you wish.
- (IBAction)showComposer:(id)sender {
// 1) get the prepared image of empty composer
UIImageView *composerView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"preparedImage"]];
composerView.frame = rightOffscreenFrame;
[self.view addSubview:composerView];
// 2) do any transitions, and transforms with it
[UIView animateWithDuration:0.33 animations:^{
composerView.frame = self.view.bounds;
} completion:^(BOOL finished) {
if (finished) {
// 3) when it is time, just add a real composer without animation
MFMailComposeViewController *composer = [[MFMailComposeViewController alloc] init];
composer.mailComposeDelegate = self;
[self presentViewController:composer animated:NO completion:^{
[composerView removeFromSuperview];
}];
}
}];
}
- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
// 4) when user will send message, render the new image with content of composer
UIGraphicsBeginImageContext(self.view.bounds.size);
[controller.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *composerView = [[UIImageView alloc] initWithImage:newImage];
composerView.frame = self.view.bounds;
// 5) show it below composer, close composer without animation.
[self.view addSubview:composerView];
[self dismissViewControllerAnimated:NO completion:^{
// 6) do any transitions, and transforms with image.
[UIView animateWithDuration:0.33 animations:^{
composerView.frame = leftOffscreenFrame
} completion:^(BOOL finished) {
if (finished) {
[composerView removeFromSuperview];
}
}];
}];
}
Well, I have to say you seriously caught my curiosity with this one. Now, as far as your question goes, it doesn't look like there's really that much you can do about this.
I took a couple of different approaches to trying to present the composer in a style other than the default with little success. The closest I was able to get was with this:
UIViewAnimationTransition trans = UIViewAnimationTransitionCurlDown;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationTransition:trans forView:[self view] cache:YES];
[self presentViewController:controller animated:NO completion:nil];
[UIView commitAnimations];
Using this method of presentation the animation effect happened, but it didn't actually seem to apply to the composer. It was just a blank page flipping. I also tried just manually adding transition effects such as alpha, and transform adjustments to the composers view property directly, but that didn't do much either.
Everything just kept boiling down to this:
Important: The message composition interface itself is not
customizable and must not be modified by your application. In
addition, after presenting the interface, your application is unable
to make further changes to the SMS content. The user can edit the
content using the interface, but programmatic changes are ignored.
Thus, you must set the values of content fields, if desired, before
presenting the interface
EDIT: Actually I think I may have found a way to make this work. It still seems unlikely that you'll be able to use custom transitions of any kind, and I can't promise that Apple will approve this, but this should allow you to present the composer navigation controller push style!
Instead of using:
[self presentViewController:controller animated:YES completion:nil];
Use:
[self.navigationController pushViewController:[[controller viewControllers] lastObject] animated:YES];
This actually allows you to push to the composer. By default this behavior isn't supported and causes an error stating that you can not push to and navigation controller (the composer).
Then to follow up, in - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result Simply use:
[self.navigationController popToRootViewControllerAnimated:YES];
Instead of:
[self dismissViewControllerAnimated:YES completion:nil];
EDIT 2: Sorry, looks like I forgot about one of the points of your question. If you want to push from one instance of the composer to another you can create iVars for each composer, set them up in viewDidLoad, and then handle daisy chaining them together in didFinishWithResult. However, this only partially solves the problem. As it stands, the code I've posted below will work fine going forward, but not as well backing up. I believe the reason for this is that the composer expects to be closed and made nil after a message has been successfully sent, and as a result the cancel but is automatically disabled.
Overall, if you mess around with it a little you should still be able to get this working.
- (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result {
switch (result) {
case MessageComposeResultCancelled:
if (controller == firstComposer) {
[self.navigationController popToRootViewControllerAnimated:YES];
}
else if (controller == secondComposer) {
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];
}
break;
case MessageComposeResultFailed:
NSLog(#"Failed");
break;
case MessageComposeResultSent:
if (controller == firstComposer) {
[self.navigationController pushViewController:[[secondComposer viewControllers] lastObject] animated:YES];
[secondComposer becomeFirstResponder];
}
break;
default:
break;
}
}
Link to download the project I made this in.
MFMailComposeViewController as a modal view is consistent with Apple's HIG. Pushing it onto a navigation stack is not. Use :
-presentModalViewController:animated:
-presentViewController:animated:completion` (if supporting iOS 5).
if you really want some deferent use the modalTransitionStyle
mail.modalTransitionStyle=UIModalTransitionStyleFlipHorizontal;
mail.modalTransitionStyle=UIModalTransitionStyleCoverVertical;
mail.modalTransitionStyle = UIModalTransitionStylePartialCurl;
mail.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
and also use the modalPresentationStyle.
FMailComposeViewController is a UINavigationController and pushing a navigation controller is not supported..
i don't think it is possible because it is a custom component given by apple
Recently faced this task. I needed to implement a transition similar push and pop of navigation stack.
Here is my implementation:
extension MFMailComposeViewController: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
convenience init(_ customTransition: Bool) {
self.init()
if customTransition { self.transitioningDelegate = self }
}
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else { return }
var start = transitionContext.initialFrame(for:fromVC)
var end = transitionContext.finalFrame(for:toVC)
if toVC is MFMailComposeViewController {
start.origin.x -= containerView.bounds.width
end.origin.x = 0.0
let v1 = transitionContext.view(forKey:.from)!
let v2 = transitionContext.view(forKey:.to)!
v2.frame.origin.x = containerView.bounds.width
containerView.addSubview(v2)
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
v1.frame.origin.x -= containerView.bounds.width/3
v2.frame = end
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
} else {
start.origin.x = containerView.bounds.width
end.origin.x = 0.0
let v1 = transitionContext.view(forKey:.from)!
let v2 = transitionContext.view(forKey:.to)!
v2.frame.origin.x = -containerView.bounds.width/3
containerView.insertSubview(v2, belowSubview: v1)
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
v2.frame = end
v1.frame = start
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
}
Above, we have implemented an extension for the MFMailComposeViewController, in which the key is an initialization with a line: self.transitioningDelegate = self
Next, we write the pseudo-code of the controller, where the MFMailComposeViewController will be initialized and present with the transition that we need:
class ViewController: UIViewController, MFMailComposeViewControllerDelegate {
#IBAction func testAction(_ sender: UIButton) {
let mailComposerVC = MFMailComposeViewController(true)
mailComposerVC.mailComposeDelegate = self
//then we configure the controller for our needs
self.present(mailComposerVC, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: {
//configure result
})
}
}
And voila, everything works like a charm!
Its possible, & u can use it as a normal ViewController, in one of my app i used, modalTransistion style as dissolve and its in store...
And one more thing, developer decides how to present the mail composer, and also how to dismiss it.
Presenting & dismissing handled by us not iOS/apple..

Prevent iOS from taking screen capture of app before going into background

You all might know that iOS takes a screen shot of your application before throwing it into the background. This is usually for a better User experience like quick animation to bring the app back and so on. I don't want my app screen shot to be stored on the device, but I want the multitasking to still exist.
I came out with a solution but I'm not sure if I'm heading in the right direction. So, when the applicationDidEnterBackground is called -- I put in an overlay image that will be captured by the OS, and once the app enters foreground, I will remove the overlay. I'm not sure if this is going to work but I'm on my way to implement this. Meanwhile, any other thoughts on this will help me figure out the optimal way of attacking this issue.
You are on the right track. This is Apple's recommended way to do this as noted in the iOS Application Programming Guide:
Remove sensitive information from views before moving to the background. When an application transitions to the background, the system takes a snapshot of the application’s main window, which it then presents briefly when transitioning your application back to the foreground. Before returning from your applicationDidEnterBackground: method, you should hide or obscure passwords and other sensitive personal information that might be captured as part of the snapshot.
Need to write the code in Application life cycle methods, here we are putting an imageView while the app animate to background :
-(void)applicationWillResignActive:(UIApplication *)application
{
imageView = [[UIImageView alloc]initWithFrame:[self.window frame]];
[imageView setImage:[UIImage imageNamed:#"Splash_Screen.png"]];
[self.window addSubview:imageView];
}
Here is the code to remove the imageView:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
if(imageView != nil) {
[imageView removeFromSuperview];
imageView = nil;
}
}
It is working and properly tested.
I came across the same issue, and my research has lead me to the following answers:
set a blurry screen overlay before the app goes in the background and once the app becomes active remove this overlay
if it is iOS 7 or later you can use the function
ignoreSnapshotOnNextApplicationLaunch
See in apple documentation:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/Reference/Reference.html#//apple_ref/occ/instm/UIApplication/ignoreSnapshotOnNextApplicationLaunch
I hope this helps somebody.
Working methods in AppDelegate, swift 4.2:
func blurScreen(style: UIBlurEffect.Style = UIBlurEffect.Style.regular) {
screen = UIScreen.main.snapshotView(afterScreenUpdates: false)
let blurEffect = UIBlurEffect(style: style)
let blurBackground = UIVisualEffectView(effect: blurEffect)
screen?.addSubview(blurBackground)
blurBackground.frame = (screen?.frame)!
window?.addSubview(screen!)
}
func removeBlurScreen() {
screen?.removeFromSuperview()
}
Where is:
weak var screen : UIView? = nil // property of the AppDelegate
Call these methods in needed delegate methods:
func applicationWillResignActive(_ application: UIApplication) {
blurScreen()
}
func applicationDidBecomeActive(_ application: UIApplication) {
removeBlurScreen()
}
Your approach is exactly the correct and only way to do it. Place an overlay view and remove it later. It is valid to do this if your app shows sensitive data that you don't want to be cached in image format anywhere.
Apple Doc
https://developer.apple.com/library/archive/qa/qa1838/_index.html
Note: Your implementation of -applicationDidEnterBackground: should not start any animations (pass NO to any animated: parameter). The snapshot of your application's window is captured immediately upon returning from this method. Animations will not complete before the snapshot is taken.
Converted Apple code in swift 4.2 App delegate i declared
func applicationDidEnterBackground(_ application: UIApplication) {
// Your application can present a full screen modal view controller to
// cover its contents when it moves into the background. If your
// application requires a password unlock when it retuns to the
// foreground, present your lock screen or authentication view controller here.
let blankViewController = UIViewController()
blankViewController.view.backgroundColor = UIColor.black
// Pass NO for the animated parameter. Any animation will not complete
// before the snapshot is taken.
window.rootViewController?.present(blankViewController, animated: false)
}
func applicationWillEnterForeground(_ application: UIApplication) {
// This should be omitted if your application presented a lock screen
// in -applicationDidEnterBackground:
window.rootViewController?.dismiss(animated: false) false
}
Improvement in Depak Kumar post :
Make a property UIImage *snapShotOfSplash;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[UIApplication sharedApplication] ignoreSnapshotOnNextApplicationLaunch];
snapShotOfSplash =[UIImage imageNamed:#"splash_logo"];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.overlayView = [[UIImageView alloc]initWithFrame:[self.window frame]];
self.overlayView.backgroundColor = [UIColor whiteColor];
[self.overlayView setImage:snapShotOfSplash];
[self.overlayView setContentMode:UIViewContentModeCenter];
[self.window addSubview:self.overlayView];
[self.window bringSubviewToFront:self.overlayView]; }
- (void)applicationDidBecomeActive:(UIApplication *)application {
if(self.overlayView != nil) {
[self.overlayView removeFromSuperview];
self.overlayView = nil;
}
}
Implementation with some animation while going in background and reverse action
- (void)applicationWillResignActive:(UIApplication *)application
{
// fill screen with our own colour
UIView *colourView = [[UIView alloc]initWithFrame:self.window.frame];
colourView.backgroundColor = [UIColor blackColor];
colourView.tag = 1111;
colourView.alpha = 0;
[self.window addSubview:colourView];
[self.window bringSubviewToFront:colourView];
// fade in the view
[UIView animateWithDuration:0.5 animations:^{
colourView.alpha = 1;
}];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// grab a reference to our coloured view
UIView *colourView = [self.window viewWithTag:1111];
// fade away colour view from main view
[UIView animateWithDuration:0.5 animations:^{
colourView.alpha = 0;
} completion:^(BOOL finished) {
// remove when finished fading
[colourView removeFromSuperview];
}];
}
swift 4.0 version.
for use custom icon
first add this line at top of AppDelegate
var imageView: UIImageView?
and add this:
func applicationDidEnterBackground(_ application: UIApplication) {
imageView = UIImageView(frame: window!.frame)
imageView?.image = UIImage(named: "AppIcon")
window?.addSubview(imageView!)
}
func applicationWillEnterForeground(_ application: UIApplication) {
if imageView != nil {
imageView?.removeFromSuperview()
imageView = nil
}
}
background with black color
func applicationDidEnterBackground(_ application: UIApplication) {
let blankViewController = UIViewController()
blankViewController.view.backgroundColor = UIColor.black
window?.rootViewController?.present(blankViewController, animated: false)
}
func applicationWillEnterForeground(_ application: UIApplication) {
window?.rootViewController?.dismiss(animated: false)
}
In iOS 7 you could use the allowScreenShot to stop the ability all together.
See: Apple Developer: Configuration Profile Reference:
allowScreenShot
Boolean
Optional. If set to false, users can’t save a screenshot of the display and are prevented from capturing a screen recording; it also prevents the Classroom app from observing remote screens. Defaults to true.
Availability: Updated in iOS 9.0 to include screen recordings.

iOS: AutoRotating between NIBs

My universal app is a single full screen view. Pressing a button flips to reveal a settings page:
- (void) showSettings
{
FlipsideViewController * flipsideVC = [FlipsideViewController alloc];
NSString * settingsNib;
if ( isIPad() )
settingsNib = isCurrentlyPortrait() ? #"settings_iPad_portrait" : #"settings_iPad_landscape";
else
settingsNib = #"settings_iPhone";
[flipsideVC initWithNibName: settingsNib
bundle: nil ];
flipsideVC.delegatePointingToMainVC = self;
flipsideVC.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController: flipsideVC
animated: YES ];
[flipsideVC release];
}
and the settings page invokes the delegate method: I recreate the main view in light of the changed settings, and flip back.
- (void) settingsDidQuit:(FlipsideViewController *) flipsideVC
{
[self createOrRecreateWheelView];
[self dismissModalViewControllerAnimated: YES];
}
But what if the user rotates the iPad on the settings page? Apple decrees that my app must handle this. But how to do this? can I dynamically load a new XIB for the settings page?
I can't see a way to do that, so my attempted solution is to catch the rotation within the settings view, ...
- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation) oldInterfaceOrientation
{
[self.delegatePointingToMainVC settingsOrientationChanged];
}
...and call back to the main view controller, which dissolves the settings view controller and recreates it in light of the current orientation.
- (void) settingsOrientationChanged
{
[self dismissModalViewControllerAnimated: YES];
[self showSettings];
}
There is a trivial problem straight away -- didRotateFromInterfaceOrientation gets triggered automatically when the settings page loads. I can prevent this by setting a boolean to false in init, and modifying thus:
- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation) oldInterfaceOrientation
{
if (initialized)
[self.delegatePointingToMainVC settingsOrientationChanged];
initialized = true;
}
problem with this approach is that I navigate to the settings page, rotate the device, and it momentarily shows the correct settings page, before flicking back to my main view.
I think there is a threading problem here. But maybe my whole approach is wrong. Can somebody suggest a better solution?
I'm not sure I understand the problem. You want the settings view (loaded from NIB) to autorotate? You should just return YES for the orientation you want the autorotation to be performed in the shouldAutorotateToInterfaceOrientation: and set the autoresizing mask of the views inside the XIB accordingly to your needs.
There's no need to call back the main view controller and tell him to push a new settings view controller. The rotation behavior of the views is determined by the autoresizing mask properties of each view and the implementation of shouldAutorotateToInterfaceOrientation: method of the associated view controller and just that. If want to do more advanced animations, though, you can set up and manage them in the willRotateToInterfaceOrientation:duration: and didRotateFromInterfaceOrientation: methods.

How to disable multitouch?

My app has several buttons which trigger different events. The user should NOT be able to hold down several buttons. Anyhow, holding down several buttons crashes the app.
And so, I'm trying to disable multi-touch in my app.
I've unchecked 'Multiple Touch' in all the xib files, and as far as I can work out, the properties 'multipleTouchEnabled' and 'exclusiveTouch' control whether the view uses multitouch. So in my applicationDidFinishLaunching I've put this:
self.mainViewController.view.multipleTouchEnabled = NO;
self.mainViewController.view.exclusiveTouch = YES;
And in each of my view controllers I've put this in the viewDidLoad
self.view.multipleTouchEnabled = NO;
self.view.exclusiveTouch = YES;
However, it still accepts multiple touches. I could do something like disable other buttons after getting a touch down event, but this would be an ugly hack. Surely there is a way to properly disable multi-touch?
If you want only one button to respond to touches at a time, you need to set exclusiveTouch for that button, rather than for the parent view. Alternatively, you could disable the other buttons when a button gets the "Touch Down" event.
Here's an example of the latter, which worked better in my testing. Setting exclusiveTouch for the buttons kind-of worked, but led to some interesting problems when you moved your finger off the edge of a button, rather than just clicking it.
You need to have outlets in your controller hooked up to each button, and have the "Touch Down", "Touch Up Inside", and "Touch Up Outside" events hooked to the proper methods in your controller.
#import "multibuttonsViewController.h"
#implementation multibuttonsViewController
// hook this up to "Touch Down" for each button
- (IBAction) pressed: (id) sender
{
if (sender == one)
{
two.enabled = false;
three.enabled = false;
[label setText: #"One"]; // or whatever you want to do
}
else if (sender == two)
{
one.enabled = false;
three.enabled = false;
[label setText: #"Two"]; // or whatever you want to do
}
else
{
one.enabled = false;
two.enabled = false;
[label setText: #"Three"]; // or whatever you want to do
}
}
// hook this up to "Touch Up Inside" and "Touch Up Outside"
- (IBAction) released: (id) sender
{
one.enabled = true;
two.enabled = true;
three.enabled = true;
}
#end
- (void)viewDidLoad {
[super viewDidLoad];
for(UIView* v in self.view.subviews)
{
if([v isKindOfClass:[UIButton class]])
{
UIButton* btn = (UIButton*)v;
[btn setExclusiveTouch:YES];
}
}
}
- (void)viewDidLoad {
[super viewDidLoad];
for(UIView* v in self.view.subviews)
{
if([v isKindOfClass:[UIButton class]])
{
UIButton* btn = (UIButton*)v;
[btn setExclusiveTouch:YES];
}
}
}
This code is tested and working perfectly for me.there is no app crash when pressing more than one button at a time.
Your app crashes for a reason. Investigate further, use the debugger, see what's wrong instead of trying to hide the bug.
Edit:
OK, ok, I have to admit I was a bit harsh. You have to set the exclusiveTouch property on each button. That's all. The multipleTouchEnabled property is irrelevant.
To disable multitouch in SWIFT:
You need first to have an outlet of every button and afterwards just set the exclusive touch to true.Therefore in you viewDidLoad() would have:
yourButton.exclusiveTouch = true.
// not really necessary but you could also add:
self.view.multipleTouchEnabled = false
If you want to disable multi touch throughout the application and don't want to write code for each button then you can simply use Appearance of button. Write below line in didFinishLaunchingWithOptions.
UIButton.appearance().isExclusiveTouch = true
Thats great!! UIAppearance
You can even use it for any of UIView class so if you want to disable multi touch for few buttons. Make a CustomClass of button and then
CustomButton.appearance().isExclusiveTouch = true
There is one more advantage which can help you. In case you want to disable multi touch of buttons in a particular ViewController
UIButton.appearance(whenContainedInInstancesOf: [ViewController2.self]).isExclusiveTouch = true
Based on neoevoke's answer, only improving it a bit so that it also checks subviews' children, I created this function and added it to my utils file:
// Set exclusive touch to all children
+ (void)setExclusiveTouchToChildrenOf:(NSArray *)subviews
{
for (UIView *v in subviews) {
[self setExclusiveTouchToChildrenOf:v.subviews];
if ([v isKindOfClass:[UIButton class]]) {
UIButton *btn = (UIButton *)v;
[btn setExclusiveTouch:YES];
}
}
}
Then, a simple call to:
[Utils setExclusiveTouchToChildrenOf:self.view.subviews];
... will do the trick.
This is quite often issue being reported by our testers. One of the approach that I'm using sometimes, although it should be used consciously, is to create category for UIView, like this one:
#implementation UIView (ExclusiveTouch)
- (BOOL)isExclusiveTouch
{
return YES;
}
Pretty much simple you can use make use of ExclusiveTouch property in this case
[youBtn setExclusiveTouch:YES];
This is a Boolean value that indicates whether the receiver handles touch events exclusively.
Setting this property to YES causes the receiver to block the delivery of touch events to other views in the same window. The default value of this property is NO.
For disabling global multitouch in Xamarin.iOS
Copy&Paste the code below:
[DllImport(ObjCRuntime.Constants.ObjectiveCLibrary, EntryPoint = "objc_msgSend")]
internal extern static IntPtr IntPtr_objc_msgSend(IntPtr receiver, IntPtr selector, bool isExclusiveTouch);
static void SetExclusiveTouch(bool isExclusiveTouch)
{
var selector = new ObjCRuntime.Selector("setExclusiveTouch:");
IntPtr_objc_msgSend(UIView.Appearance.Handle, selector.Handle, isExclusiveTouch);
}
And set it on AppDelegate:
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
...
SetExclusiveTouch(true); // setting exlusive to true disables the multitouch
...
}
My experience is that, by default, a new project doesn't even allow multitouch, you have to turn it on. But I suppose that depends on how you got started. Did you use a mutlitouch example as a template?
First of all, are you absolutely sure multitouch is on? It's possible to generate single touches in sequence pretty quickly. Multitouch is more about what you do with two or more fingers once they are on the surface. Perhaps you have single touch on but aren't correctly dealing with what happens if two buttons are pressed at nearly the same time.
I've just had exactly this problem.
The solution we came up with was simply to inherit a new class from UIButton that overrides the initWithCoder method, and use that where we needed one button push at a time (ie. everywhere):
#implementation ExclusiveButton
(id)initWithCoder: (NSCoder*)decoder
{
[self setExclusiveTouch:YES];
return [super initWithCoder:decoder]
}
#end
Note that this only works with buttons loaded from nib files.
I created UIView Class Extension and added this two functions. and when i want to disable view touch i just call [view makeExclusiveTouch];
- (void) makeExclusiveTouchForViews:(NSArray*)views {
for (UIView * view in views) {
[view makeExclusiveTouch];
}
}
- (void) makeExclusiveTouch {
self.multipleTouchEnabled = NO;
self.exclusiveTouch = YES;
[self makeExclusiveTouchForViews:self.subviews];
}
If you want to disable multitouch programmatically, or if you are using cocos2d (no multipleTouchEnabled option), you can use the following code on your ccTouches delegate:
- (BOOL)ccTouchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event {
NSSet *multiTouch = [event allTouches];
if( [multiTouch count] > 1) {
return;
}
else {
//else your rest of the code
}
Disable all the buttons on view in "Touch Down" event and enable them in "Touch Up Inside" event.
for example
- (void) handleTouchDown {
for (UIButton *btn in views) {
btn.enable = NO;
}
}
- (void) handleTouchUpInside {
for (UIButton *btn in views) {
btn.enable = Yes;
}
------
------
}
I decided this problem by this way:
NSTimeInterval intervalButtonPressed;
- (IBAction)buttonPicturePressed:(id)sender{
if (([[NSDate date] timeIntervalSince1970] - intervalButtonPressed) > 0.1f) {
intervalButtonPressed = [[NSDate date] timeIntervalSince1970];
//your code for button
}
}
I had struggled with some odd cases when dragging objects around a view, where if you touched another object at the same time it would fire the touchesBegan method. My work-around was to disable user interaction for the parent view until touchesEnded or touchesCancelled is called.
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
// whatever setup you need
self.view.userInteractionEnabled = false
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
// whatever setup you need
self.view.userInteractionEnabled = true
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
// whatever setup you need
self.view.userInteractionEnabled = true
}
A Gotcha:
If you are using isExclusiveTouch, be aware that overriding point(inside:) on the button can interfere, effectively making isExclusiveTouch useless.
(Sometimes you need to override point(inside:) for handling the "button not responsive at bottom of iPhone screen" bug/misfeature (which is caused by Apple installing swipe GestureRecognizers at the bottom of the screen, interfering with button highlighting.)
See: UIButton fails to properly register touch in bottom region of iPhone screen
Just set all relevant UIView's property exclusiveTouch to false do the trick.