QLPreviewController remove or add UIBarButtonItems - iphone

I have seen this kind of question a lot on the internet but it seems no one really knows the answer?
I am using QLPreviewController for displaying PDF documents. I first used a UIWebView but I was recommended to use QLPreviewController instead for performance reasons with bigger documents.
what I want is 4 custom UIBarButtonItem's in the top right (so where the print button is).
I managed to get a custom toolbar at the bottom, but that's not really what I want.
Considering that it is not possible to add custom button at the place of the print button, I still want to remove the printbutton and use the custom toolbar instead.
EDIT (Solution):
I found the solution a while ago but didn't update this post so here is how I solved the problem:
I add al the buttons manually:
// Create a toolbar to have the buttons at the right side of the navigationBar
UIToolbar* toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 180, 44.01)];
[toolbar setTranslucent:YES];
// Create the array to hold the buttons, which then gets added to the toolbar
NSMutableArray* buttons = [[NSMutableArray alloc] initWithCapacity:4];
// Create button 1
button1 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:#selector(button1Pressed)];
[buttons addObject:button1];
// Create button 2
button2 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose target:self action:#selector(button2Pressed)];
[buttons addObject:button2];
// Create button 3
button3 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemBookmarks target:self action:#selector(button3Pressed)];
[buttons addObject:button3];
// Create a action button
openButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:#selector(openWith)];
[buttons addObject:openButton];
// insert the buttons in the toolbar
[toolbar setItems:buttons animated:NO];
// and put the toolbar in the navigation bar
[[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:toolbar]];

I searched for a solution to this problem for months and finally found a way to customize the navigationbar of a QLPreviewController. Previously I was also using UIWebView to display documents as I'm not allowed to display the iOS-share button for certain confidential documents within my app and this is what the QLPreviewController does. However I wanted to have those nice features such as the table of contents with the little previews and stuff. So I looked for a reliable way to get rid of this button. Like you guys I was first looking into customizing the navigationbar of the QLPreviewController. However, as others already pointed out this is absolutely not possible since iOS6. So instead of customizing the existing navigation bar what we need to do is creating an own one and placing it in front of the QL-navigationbar, thus hiding it.
So how to do this?
First of all we need to subclass QLPreviewContoller and overwrite the viewDidAppear method and viewWillLayoutSubviews like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.qlNavigationBar = [self getNavigationBarFromView:self.view];
self.overlayNavigationBar = [[UINavigationBar alloc] initWithFrame:[self navigationBarFrameForOrientation:[[UIApplication sharedApplication] statusBarOrientation]]];
self.overlayNavigationBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[self.view addSubview:self.overlayNavigationBar];
NSAssert(self.qlNavigationBar, #"could not find navigation bar");
if (self.qlNavigationBar) {
[self.qlNavigationBar addObserver:self forKeyPath:#"hidden" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
}
// Now initialize your custom navigation bar with whatever items you like...
UINavigationItem *item = [[UINavigationItem alloc] initWithTitle:#"Your title goes here"];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(doneButtonTapped:)];
item.leftBarButtonItem = doneButton;
item.hidesBackButton = YES;
[self.overlayNavigationBar pushNavigationItem:item animated:NO];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
self.overlayNavigationBar.frame = [self navigationBarFrameForOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
}
qlNavigationBar is the default navigationbar owned by the QLPreviewController, overlayNavigationBar is our custom one which will hide the default one. We also add a key-value observation to the default QL navigationbar to get notified when the default navigation bar gets hidden / reappears. In the viewWillLayoutSubviews method we take care of our custom navigationbar frame.
The next thing we should do is listen for visibility changes of the quicklook navigationbar:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// Toggle visiblity of our custom navigation bar according to the ql navigationbar
self.overlayNavigationBar.hidden = self.qlNavigationBar.isHidden;
}
So now we need to implement methods we need to get the QL navigationbar and one that always gives us the current frame for our custom navigation bar:
- (UINavigationBar*)getNavigationBarFromView:(UIView *)view {
// Find the QL Navigationbar
for (UIView *v in view.subviews) {
if ([v isKindOfClass:[UINavigationBar class]]) {
return (UINavigationBar *)v;
} else {
UINavigationBar *navigationBar = [self getNavigationBarFromView:v];
if (navigationBar) {
return navigationBar;
}
}
}
return nil;
}
- (CGRect)navigationBarFrameForOrientation:(UIInterfaceOrientation)orientation {
// We cannot use the frame of qlNavigationBar as it changes position when hidden, also there seems to be a bug in iOS7 concerning qlNavigationBar height in landscape
return CGRectMake(0.0f, self.isIOS6 ? 20.0f : 0.0f, self.view.bounds.size.width, [self navigationBarHeight:orientation]);
}
- (CGFloat)navigationBarHeight:(UIInterfaceOrientation)orientation {
if([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if(UIInterfaceOrientationIsLandscape(orientation)) {
return self.isIOS6 ? 32.0f : 52.0f;
} else {
return self.isIOS6 ? 44.0f : 64.0f;
}
} else {
return self.isIOS6 ? 44.0f : 64.0f;
}
}
What else? Well of course you need to define properties, remove the observer in dealloc as well as define and set the iOS6 property (there are plenty of examples on the web...). Also you need to customize your navigationbar and listen to the button callbacks. That's it.
I know this is a bit hacky ... hiding / replacing the default QL action button by hiding it beneath another navigationbar ...but well at least it works reliable for me and you don't access private APIs etc.
I tested my solution on all available simulators for iOS 6.0 - 7.0 as well as on iPad 2 & 3, iPhone 4S & 5 (the latter with iOS 7.0 Beta 6 installed).

Update:
This no longer works in iOS 6. Quick Look runs in another process using XPC. See here for more details. I don't foresee any way to customize QLPreviewController. The following answer remains for anyone interested for pre-iOS 6.
I answered an almost identical question the other day here. The question pertained to removing the print button, which isn't too hard. One thing to note about QLPreviewController is that it's not meant to be customized. I have built a subclass of QLPreviewController that can be customized. I've put it here on Github. It's designed to easily remove the action button, among other features too. It wouldn't take much effort at all to replace the button with a custom one.
The biggest thing to watch out for is that the action button is re-added to the navigation bar anytime a new document is displayed. You should notice this in my code. Anytime RBFilePreviewer removes the action button, you just need to re-add your custom buttons. To add your custom buttons, you should create a UIBarButtonItem that holds a custom view with four buttons in it. Then set the right bar button item as the custom UIBarButtonItem you created.
Update:
I've updated RBFilePreviewer to allow you to set a custom right bar button item right out-of-the-box. Just call -setRightBarButtonItem: on RBFilePreviewer and it just works.

I took the response from Lukas Gross and applied it in Swift on iOS 8 and came up with this solution that worked for me:
NOTE: I have the QLPreviewController embedded in a UINavigationController!
Code:
var QLNavigationBar: UINavigationBar?
var overlayNavigationBar: UINavigationBar?
func getQLNavigationBar(fromView view: UIView) -> UINavigationBar? {
for v in view.subviews {
if v is UINavigationBar {
return v as? UINavigationBar
} else {
if let navigationBar = self.getQLNavigationBar(fromView: (v as! UIView)) {
return navigationBar
}
}
}
return nil
}
func handleNavigationBar() {
self.QLNavigationBar = self.getQLNavigationBar(fromView: self.navigationController!.view)
self.overlayNavigationBar = UINavigationBar(frame: CGRectMake(0, 0, self.view.bounds.size.width, 64.0))
self.overlayNavigationBar?.autoresizingMask = UIViewAutoresizing.FlexibleWidth
if let qln = self.QLNavigationBar {
qln.addObserver(self, forKeyPath: "hidden", options: (NSKeyValueObservingOptions.New | NSKeyValueObservingOptions.Old), context: nil)
qln.superview?.addSubview(self.overlayNavigationBar!)
}
var item = UINavigationItem(title: self.navigationItem.title!)
var doneBtn = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: "doneBtnPressed")
item.leftBarButtonItem = doneBtn
item.hidesBackButton = true
self.overlayNavigationBar?.pushNavigationItem(item, animated: false)
self.overlayNavigationBar?.tintColor = .whiteColor()
self.overlayNavigationBar?.barTintColor = .blackColor()
self.overlayNavigationBar?.titleTextAttributes = [
NSForegroundColorAttributeName : UIColor.whiteColor() ]
}
And applying this code like this:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if self.QLNavigationBar!.hidden {
self.overlayNavigationBar?.hidden = self.QLNavigationBar!.hidden
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
self.QLNavigationBar?.superview?.sendSubviewToBack(self.QLNavigationBar!)
if !self.QLNavigationBar!.hidden {
self.overlayNavigationBar?.hidden = self.QLNavigationBar!.hidden
}
})
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.handleNavigationBar()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.overlayNavigationBar?.frame = CGRectMake(0, 0, self.view.bounds.size.width, 64.0)
}

I understand that this answer is a little late for this.
But I really do find a solution for this.
#import "UINavigationItem+Custome.h"
#import <QuickLook/QuickLook.h>
#import <objc/runtime.h>
#implementation UINavigationItem (Custome)
void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL);
- (void) override_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated{
if (item && [item.target isKindOfClass:[QLPreviewController class]] && item.action == #selector(actionButtonTapped:)){
QLPreviewController* qlpc = (QLPreviewController*)item.target;
[self override_setRightBarButtonItem:qlpc.navigationItem.rightBarButtonItem animated: animated];
}else{
[self override_setRightBarButtonItem:item animated: animated];
}
}
+ (void)load {
MethodSwizzle(self, #selector(setRightBarButtonItem:animated:), #selector(override_setRightBarButtonItem:animated:));
}
void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL) {
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod = class_getInstanceMethod(c, overrideSEL);
if (class_addMethod(c, origSEL, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(c, overrideSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}else{
method_exchangeImplementations(origMethod, overrideMethod);
}
}
#end
Add this as a Category and import this into your QLPreviewController's subclass and just call the
self.navigationItem.rightBarButtonItem = nil;//something you want
It works for me.
I learn this from http://nshipster.com/method-swizzling/
and thoughts from http://codego.net/507056/
Good luck, guys.

By mixing a bit out of the existing answers/comments I was able to get this working for my use case: I needed to display files inside a UINavigationController and keep the ability of hiding/showing the UINavigationBar when the file content is tapped
Based on the answer from Lukas Gross and the comment from nacross here's what I ended up doing:
Add a (subclass of) QLPreviewController as a child view controller. This will show two navigation bars: one for the main navigation controller and one from the QLPreviewController
Set up a top constraint from the container view to the top layout guide (named containerTop in the code)
Set this constraint to a negative value, equal to the UINavigationBar plus the status bar, so that the QLPreviewController's UINavigationBar remains hidden under the main UINavigationBar.
Using KVO, monitor the hidden property of the UINavigationBar so that we can (1) hide/show our main UINavigationBar and (2) reset the top constraint
I ended up with something like this:
var qlNavigationBar: UINavigationBar?
func getQLNavigationBar(fromView view: UIView) -> UINavigationBar? {
for v in view.subviews {
if v is UINavigationBar {
return v as? UINavigationBar
} else {
if let navigationBar = self.getQLNavigationBar(fromView: v) {
return navigationBar
}
}
}
return nil
}
func setObserverForNavigationBar() {
self.qlNavigationBar = self.getQLNavigationBar(fromView: self.view)
if let qln = self.qlNavigationBar {
qln.addObserver(self, forKeyPath: "hidden", options: [NSKeyValueObservingOptions.New, NSKeyValueObservingOptions.Old], context: nil)
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
self.navigationController?.setNavigationBarHidden(self.qlNavigationBar!.hidden, animated: true)
self.containerTop.constant = self.qlNavigationBar!.hidden ? self.getStatusBarHeight() * -1 : self.getFullNavigationBarHeight() * -1
UIView.animateWithDuration(0.5) {
self.view.layoutIfNeeded()
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated);
self.setObserverForNavigationBar()
self.containerTop.constant = self.getFullNavigationBarHeight() * -1
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated);
if let qln = self.qlNavigationBar {
qln.removeObserver(self, forKeyPath: "hidden")
}
}
func getFullNavigationBarHeight() -> CGFloat {
if let nav = self.navigationController {
return nav.navigationBar.frame.origin.y + nav.navigationBar.frame.size.height
}
return 0
}
func getStatusBarHeight() -> CGFloat {
return UIApplication.sharedApplication().statusBarFrame.size.height
}
The animations might need a little tweaking and it is hacky, but it's better than not having this possibility.
It should be possible to adapt this strategy to other scenarios without the UINavigationController
Note: If you have a crash when implementing the container view for the QLPreviewController from a storyboard, subclass the QLPreviewController and implement the initializer:
class MyPreviewController: QLPreviewController {
required init?(coder aDecoder: NSCoder) {
super.init(nibName: nil, bundle: 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.

How to detect touches in status bar

I have custom view in my application which can be scrolled by the user. This view, however, does not inherit from UIScrollView. Now I want the user to be able to scroll this view to the top, just as any other scrollable view allows. I figured that there is no direct way to do so.
Google turned up one solution: http://cocoawithlove.com/2009/05/intercepting-status-bar-touches-on.html This no longer works on iOS 4.x. That's a no-go.
I had the idea of creating a scrollview and keeping it around somewhere, just to catch it's notifications and then forward them to my control. This is not a nice way to solve my problem, so I am looking for "cleaner" solutions. I like the general approach of the aforementioned link to subclass UIApplication. But what API can give me reliable info?
Are there any thoughts, help, etc...?
Edit: Another thing I don't like about my current solution is that it only works as long as the current view does not have any scroll views. The scroll-to-top gesture works only if exactly one scroll view is around. As soon as the dummy is added (see my answer below for details) to a view with another scrollview, the gesture is completely disabled. Another reason to look for a better solution...
Finally, i've assembled the working solution from answers here. Thank you guys.
Declare notification name somewhere (e.g. AppDelegate.h):
static NSString * const kStatusBarTappedNotification = #"statusBarTappedNotification";
Add following lines to your AppDelegate.m:
#pragma mark - Status bar touch tracking
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
if (CGRectContainsPoint(statusBarFrame, location)) {
[self statusBarTouchedAction];
}
}
- (void)statusBarTouchedAction {
[[NSNotificationCenter defaultCenter] postNotificationName:kStatusBarTappedNotification
object:nil];
}
Observe notification in the needed controller (e.g. in viewWillAppear):
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(statusBarTappedAction:)
name:kStatusBarTappedNotification
object:nil];
Remove observer properly (e.g. in viewDidDisappear):
[[NSNotificationCenter defaultCenter] removeObserver:self name:kStatusBarTappedNotification object:nil];
Implement notification-handling callback:
- (void)statusBarTappedAction:(NSNotification*)notification {
NSLog(#"StatusBar tapped");
//handle StatusBar tap here.
}
Hope it will help.
Swift 3 update
Tested and works on iOS 9+.
Declare notification name somewhere:
let statusBarTappedNotification = Notification(name: Notification.Name(rawValue: "statusBarTappedNotification"))
Track status bar touches and post notification. Add following lines to your AppDelegate.swift:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let statusBarRect = UIApplication.shared.statusBarFrame
guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }
if statusBarRect.contains(touchPoint) {
NotificationCenter.default.post(statusBarTappedNotification)
}
}
Observe notification where necessary:
NotificationCenter.default.addObserver(forName: statusBarTappedNotification.name, object: .none, queue: .none) { _ in
print("status bar tapped")
}
So this is my current solution, which works amazingly well. But please come with other ideas, as I don't really like it...
Add a scrollview somewhere in your view. Maybe hide it or place it below some other view etc.
Set its contentSize to be larger than the bounds
Set a non-zero contentOffset
In your controller implement a delegate of the scrollview like shown below.
By always returning NO, the scroll view never scrolls up and one gets a notification whenever the user hits the status bar. The problem is, however, that this does not work with a "real" content scroll view around. (see question)
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
// Do your action here
return NO;
}
Adding this to your AppDelegate.swift will do what you want:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
let events = event!.allTouches()
let touch = events!.first
let location = touch!.locationInView(self.window)
let statusBarFrame = UIApplication.sharedApplication().statusBarFrame
if CGRectContainsPoint(statusBarFrame, location) {
NSNotificationCenter.defaultCenter().postNotificationName("statusBarSelected", object: nil)
}
}
Now you can subscribe to the event where ever you need:
NSNotificationCenter.defaultCenter().addObserverForName("statusBarSelected", object: nil, queue: nil) { event in
// scroll to top of a table view
self.tableView!.setContentOffset(CGPointZero, animated: true)
}
Thanks Max, your solution worked for me after spending ages looking.
For information :
dummyScrollView = [[UIScrollView alloc] init];
dummyScrollView.delegate = self;
[view addSubview:dummyScrollView];
[view sendSubviewToBack:dummyScrollView];
then
dummyScrollView.contentSize = CGSizeMake(view.frame.size.width, view.frame.size.height+200);
// scroll it a bit, otherwise scrollViewShouldScrollToTop not called
dummyScrollView.contentOffset = CGPointMake(0, 1);
//delegate :
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
// DETECTED! - do what you need to
NSLog(#"scrollViewShouldScrollToTop");
return NO;
}
Note that I had a UIWebView also which I had to hack a bit with a solution I found somewhere :
- (void)webViewDidFinishLoad:(UIWebView *)wv
{
[super webViewDidFinishLoad:wv];
UIScrollView *scroller = (UIScrollView *)[[webView subviews] objectAtIndex:0];
if ([scroller respondsToSelector:#selector(setScrollEnabled:)])
scroller.scrollEnabled = NO;
}
Found a much better solution which is iOS7 compatible here :http://ruiaureliano.tumblr.com/post/37260346960/uitableview-tap-status-bar-to-scroll-up
Add this method to your AppDelegate:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
if (CGRectContainsPoint([UIApplication sharedApplication].statusBarFrame, location)) {
NSLog(#"STATUS BAR TAPPED!");
}
}
I implemented this by adding a clear UIView over the status bar and then intercepting the touch events
First in your Application delegate application:didFinishLaunchingWithOptions: add these 2 lines of code:
self.window.backgroundColor = [UIColor clearColor];
self.window.windowLevel = UIWindowLevelStatusBar+1.f;
Then in the view controller you wish to intercept status bar taps (or in the application delegate) add the following code
UIView* statusBarInterceptView = [[[UIView alloc] initWithFrame:[UIApplication sharedApplication].statusBarFrame] autorelease];
statusBarInterceptView.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer* tapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(statusBarClicked)] autorelease];
[statusBarInterceptView addGestureRecognizer:tapRecognizer];
[[[UIApplication sharedApplication].delegate window] addSubview:statusBarInterceptView];
In the statusBarClicked selector, do what you need to do, in my case I posted a notification to the notification center so that other view controllers can respond to the status bar tap.
Use an invisible UIScrollView. Tested at iOS 10 & Swift 3.
override func viewDidLoad() {
let scrollView = UIScrollView()
scrollView.bounds = view.bounds
scrollView.contentOffset.y = 1
scrollView.contentSize.height = view.bounds.height + 1
scrollView.delegate = self
view.addSubview(scrollView)
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
debugPrint("status bar tapped")
return false
}
You can track status bar tap by using following code:
[[NSNotificationCenter defaultCenter] addObserverForName:#"_UIApplicationSystemGestureStateChangedNotification"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSLog(#"Status bar pressed!");
}];
One way, might not be the best, could be to put a clear UIView on top of the status bar and intercept the touches of the UIView, might help you out if nothing else comes up...
If you're just trying to have a UIScrollView scroll to the top when the status bar is tapped, it's worth noting that this is the default behavior IF your view controller has exactly one UIScrollView in its subviews that has scrollsToTop set to YES.
So I just had to go and find any other UIScrollView (or subclasses: UITableView, UICollectionView, and set scrollsToTop to be NO.
To be fair, I found this info in the post that was linked to in the original question, but it's also dismissed as no longer working so I skipped it and only found the relevant piece on a subsequent search.
For iOS 13 this has worked for me, Objective-C category of UIStatusBarManager
#implementation UIStatusBarManager (CAPHandleTapAction)
-(void)handleTapAction:(id)arg1 {
// Your code here
}
#end

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.