UIButton not working correctly in iOS 7 - ios

I have a problem with a UIButton that works perfectly well in iOS6, but fails to respond to touch events in iOS7 up to a certain point.
To clarify please see below image:
The button that fails is the "Discard All" button that is in the UIView. (Please note, this button is only disabled temporarily and that is NOT the issue. I just don't have a screenshot of the newest test where the button is enabled")
This button ignores all touches, unless one first presses the "Discard" or "Retry" buttons in the UITableViewCell. (This does cause a reload of the view controller, which triggers the lifecycle methods like ViewDidLoad to be called again.)
After either the "Discard" or "Retry" buttons in the table view cell have been pressed, the "Discard All" button starts functioning correctly.
The view and the "Discard All" button are build on the Controller's XIB file and not in code. This only fails on iOS7, and starts working as soon as the taleview cell buttons are touched.
Anyone have any ideas?
Thanks!

I found the solution last night.
Okay, so what happens is that I put the above table view and UIView elements onto a target frame.
I'm not 100% sure, but it seems that in iOS6 the buttons respond to events irrespective of where they are placed.
For some reason in iOS7 when the button sits outside of the frame it is supposed to be in, it ignores touch events, even though it does get displayed.
I solved the problem by positioning the view's frame in the correct place so it overlays the button.
If I can find any documentation around this, I will post here.
Thanks!

I have just faced to this problem. According to #Guntis Treulands advice, I decided to check what happens if I override hitTest:withEvent: method in my custom header view. This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content and now, after it has been overridden, receives touches outside the bounds. It did the trick for me. Hope it helps you, guys.
swift
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !isHidden, alpha > 0 else {
return super.hitTest(point, with: event)
}
for subview in subviews {
let subpoint = subview.convert(point, from: self)
let result = subview.hitTest(subpoint, with: event)
if result != nil {
return result
}
}
return super.hitTest(point, with: event)
}
Objective-C
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.clipsToBounds && !self.hidden && self.alpha > 0) {
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result != nil) {
return result;
}
}
}
// use this to pass the 'touch' onward in case no subviews trigger the touch
return [super hitTest:point withEvent:event];
}

Just as an alternative answer - Actually for me, this has never worked (if uibutton is outside it's parent view, then it will not receive touch.) - But in some cases, it is required to have the button outside it's boundaries.
For that reason, there is a hittest function, which can be overridden - to simply check parent view all subviews to find uibutton that is placed outside parent views boundaries. (You would then check each subview, if it is uibutton type, and if touched coordinates are inside uibuttons frame.
By default hittest function skips checking views, that are outside boundaries.
Hittest example: https://stackoverflow.com/questions/17246488/best-way-to-perform-the-hit-test-objective-c .

Not sure if this is the same case, but it might be related:
When the UIButton is in a view with a UIGestureRecognizer which did NOT set delaysTouchesEnded to NO (default is YES), the button won't get TouchUpInside events anymore in iOS 7. It DID get the TouchUpInside events in iOS 6.
If possible, set the delaysTouchesEnded property of the UIGestureRecognizer to NO to solve the problem.

This is weird but I was developing a new app for iOS 7 and setting [cell.contentView setUserInteractionEnabled: YES]; actually worked for me.

I had this same issue - buttons did work on iOS6 and didn't on iOS7, problem was with Container UIView to which one I was adding UIButtons, I add constrains to container UIView and it started working. Code that I had to add in my case:
[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V: [_controllersView]-|" options:0 metrics:nil views:views]];
[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_controllersView(70)]" options:0 metrics:nil views:views]];
[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_controllersView]|" options:0 metrics:nil views:views]];

I also got same problem but after adding below code button actions working properly cell.contentView.userInteractionEnabled = NO;

If it's using auto layout, then try adding a log statement in -viewDidAppear:
NSLog(#"Height of UIView: %f", self.<name of UIView>.frame.size.height);
if the result is 0 (or not large enough), then set the height of UIView large than the height of 'Discard All' button.

Check that the button in inside parent frame

Okay, so seems like buttons programatically created in iOS7 won't call their target.
I don't know why, but I've found the following solution:
In the InterFace Builder (or nib) add a button and customize and HIDE it. ( theButtonInTheNib);
Make it IBOutlet property: #property (strong) IBOutlet UIButton *theButtonInTheNib
Make the target IBAction connection: (IBAction)theTargetAction:(id)sender;
Here comes the trick. We will make a copy/or copies of this button:
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:theButtonInTheNib];
UIButton *theCopyButton = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
[theCopyButton setHidden:NO];
[theCopyButton setFrame:CGRectMake(x, y, width, height)];
[theCopyButton addTarget:self action:#selector(theTargetAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:theCopyButton];
I have added again the target action, but any other action will work.

Related

Animated uiview switches back to its original state when rotating device

I try to make a tiny animation under IOS7. My view controller has 2 views: a tableview and a mapview. I have a button, that switches between 'show map full screen' and 'show map and tableview' state.
Here is my experimental code:
float mapNewX = self.mapView.frame.origin.x == 0 ? 375 : 0;
float mapNewWidth = mapNewX == 0 ? self.view.frame.size.width : self.view.frame.size.width - mapNewX;
[UIView animateKeyframesWithDuration:0.1 delay:0.0 options:UIViewAnimationCurveEaseOut animations:^
{
CGRect mapViewFrame = self.mapView.frame;
mapViewFrame.origin.x = (mapNewX);
mapViewFrame.size.width = mapNewWidth;
self.mapView.frame = mapViewFrame;
}
completion:^(BOOL finished)
{
NSLog(#"OK");
}];
It works, but when I rotate my device, it switches back to the original state. So, when I have full map display, when rotating device, it switches back to tableview + mapview on the screen.
The same happens, if I come back from another view controller by navigation controller. If I tap detail disclosure of a map pin, it switches back to the original layout.
What should I do? Thanks very much!
Update: here are the screenshots before/after rotation (some sensitive data is masked out)
If you loose frame changes after rotation or screen navigation, it probably relates to view controller's layout methods viewWillLayoutSubviews, viewDidLayoutSubviews or life cycle methods viewWillAppear:, viewDidAppear:. Check implementations of these methods if you override them.
Because you use auto layout, it's better to animate its constraints instead of frames.
As far as I understood, your task is to move left side of a map to reveal a table view underneath (opened/closed states). With autolayout your screen might look as simple as this:
(UIImageView view plays a role of a map)
UITableView constraints
constant width constraint
top, leading and bottom space to superview constraints set to zero
UIImageView constraints
top, bottom and trailing space to superview constraints set to zero
leading space to superview set to table width (opened state, animate this constraint)
To implement desired effect we need to animate leading space to superview constraint (selected on a the screenshot). We create an outlet to this constraint and change its constant property to opened/closed state.
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *mapLeadingSpace;
Set initial state of the menu before it will appear on screen. We set a default value, but you can restore an appropriate boolean setting from NSUserDefaults.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.mapLeadingSpace.constant = 0; // default is closed
}
Next, toggle animation on bar button press.
- (IBAction)toggle:(id)sender
{
self.mapLeadingSpace.constant = self.mapLeadingSpace.constant == 0
? self.tableView.frame.size.width // opened
: 0; // closed
[UIView animateWithDuration:.2 animations:^{
[self.imageView layoutIfNeeded];
}];
}
Using
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
Create a variable to indicate whether it is full screen map or split view, then run the routine once rotation is completed? Not sure how hi-tech of a solution that is, but it's what i'd do! :)
I had a similar split views (2 UItableViews), here is how i managed to fix it.
First, have a global static variable lets call it static BOOL _isViewSplitted
its static so when you push or pop viewContorller is stays in memory.
Override didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
check your variable here and if YES, split them if NO, just return.
Also, you have to check (do the same as above) in viewWillAppear when the viewController is popped.
You could probably fix this by coding your constraints and then animating them.
Something like this (the values in this code doesn't match the ones in your example):
[subview setTranslatesAutoresizingMaskIntoConstraints:NO];
[parentview setTranslatesAutoresizingMaskIntoConstraints:NO];
[parentview addSubview:subview];
NSDictionary *views = NSDictionaryOfVariableBindings(subview);
[parentview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[subview]"
options:0
metrics:nil
views:views]];
NSArray *firstConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[subview]"
options:0
metrics:nil
views:views];
[parentview addConstraints:firstConstraints];
[parentview layoutSubtreeIfNeeded];
[parentview removeConstraints:firstConstraints];
NSArray *secondConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-100-[subview]-100-|"
options:0
metrics:nil
views:views];
[parentview addConstraints:secondConstraints];
subview.superview.wantsLayer = YES;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext* context) {
[context setDuration:.3];
[context setAllowsImplicitAnimation:YES];
[context setTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[subview layoutSubtreeIfNeeded];
} completionHandler:^{
[[self delegate] createOptionMenuForPDFViewWithInvoice:invoice andSuperView:parentview];
}];
Tom, your big problem (IMHO) is that you are calling this code that you've provided in your question too often. Every time its going to toggle the map from full screen to split because it makes this decision from where it is now. Why don't you just have a boolean ivar you check? you can flip this in your button action to toggle the maps full screen state, then it won't matter if you repeatedly call this code on reorient etc, just alter the condition in your turnery operators at the start there to this new boolean instead of querying whether the map is full screen or not
This question stood a greater chance of being answered relatively quickly without the need for a bounty had the following information been provided in the original post (in general, this might be a good set of guidelines for any post related to layout in iOS):
(1.) is Auto Layout enabled in the storyboard?
(2.) are you using a mixed (hybrid) approach or pure Auto Layout? in other words, which views have their translatesAutoresizingMaskIntoConstraints set to YES?
(3.) if any layout methods or rotation callback methods are overridden, provide that implementation
(4.) show us your constraints
I was able to figure out the answer to #1 from the comments.
My answer given the little I know about the problem:
If you are using Auto Layout on the map view, then setting its frame in the animation block won't permanently re-position the view because the map view's constraints haven't been changed. Instead, update the map view's constraints before the animation block, then send layoutIfNeeded to self.view inside the animation block.
// update constraints…
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}];
Also give a try to this in viewDidload of your Viewcontroller class
if ([self respondsToSelector:#selector(edgesForExtendedLayout)]) {
self.edgesForExtendedLayout = UIRectEdgeNone;
}

Custom UIView widget with UIScrollView not scrolling

I am trying to develop a new custom UIView (to allow for horizontal date selection). I want to do all the UI design in XIB files.
The custom UI view contains a scrollview and then two 'week' views. The idea is that as the scrolling occurs, I will move the two 'week' views in place and reconfigure them to the right dates to create an 'infinite' scroll for date selections.
I can load the UIView, which then loads scrollview and week views (all designed in a XIB).
My DatePickerView class, derived from the UIView class does an addSubview of the scroll view (which contains the two week views). The scroll view is 320 wide and the contentSize is set to 640 wide. UserInteraction is enabled. Horizonal Scrolling is enabled.
This all works and displays on the screen. The week views each contain 7 buttons. I can press them and they get the touch. However, the scrollview does not seem to want to scroll.
I set my custom view to be a UIScrollViewDelegate. No calls occur to scrollViewDidScroll.
For each of the week views, I have a 'container' view and then the buttons. I added the following to the container view (again derived from a UIView).
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
DDLogInfo(#"Began. Next Responder: %#", self.nextResponder);
[self.nextResponder touchesBegan:touches withEvent:event];
}
(and comparable ones for the other touch events, touchesMoved, touchesEnded, touchesCancelled),
I print out the nextResponder, which is the UIScrollView, so I know that I am sending the touch to the view, but I never see the scrollview want to scroll.
Is my method of passing the touchEvents up the responder chain correct?
Is there anything else I need to configure to get the scrolling to work?
Any help is appreciated.
Charlie
If I understand correctly, you want infinite scroll with just three pages of scroll view. I achieved it with similar effects in my calendar view project.
You can checkout from here DPCalendar
In a nutshell, I created a view like
#interface DPCalendarMonthlyView : UIScrollView
And initial it like this
self.showsHorizontalScrollIndicator = NO;
self.clipsToBounds = YES;
self.contentInset = UIEdgeInsetsZero;
self.pagingEnabled = YES;
self.delegate = self;
I create three views like this
[self.pagingViews addObject:[self singleMonthViewInFrame:self.bounds]];
[self.pagingViews addObject:[self singleMonthViewInFrame:CGRectMake(self.bounds.size.width, 0, self.bounds.size.width, self.bounds.size.height)]];
[self.pagingViews addObject:[self singleMonthViewInFrame:CGRectMake(self.bounds.size.width * 2, 0, self.bounds.size.width, self.bounds.size.height)]];
Then I set the content size and also scroll it to the middle
[self setContentSize:CGSizeMake(self.bounds.size.width * 3, self.bounds.size.height)];
[self scrollRectToVisible:((UIView *)[self.pagingViews objectAtIndex:1]).frame animated:NO];
In the scrollview delegate function, i need to do
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
//If scroll right
if(self.contentOffset.x > self.frame.size.width)
{
//do something if scroll right
} else if(self.contentOffset.x < self.frame.size.width)
{
//do something else if scroll left
} else {
return;
}
//scroll back to the middle
[self scrollRectToVisible:((UICollectionView *)[self.pagingViews objectAtIndex:1]).frame animated:NO];
}
Hopefully it is useful to you.
For those that follow down this path, I figured this out and it was a silly error. I forgot to turn off AutoLayout. I keep forgetting that Apple put autoLayout as an enable/disable option under the 'document'-level of a NIB (so I forget to look there).
Turned it off and it works as designed. Looks like autoLayout was causing the views to be rearranged to not need to be scrolled, or something equivalent.

Button click not responding in ScrollView when I create outlet of ScrollView in iOS

I have a Button1 outside ScrollView and another Button2 inside scrollview. I am using storyboard. I have used drag drop segue from both buttons to a different view. Button1 works fine, the problem is with Button2, it doesnt work no matter how many times I click, it only works when I click and drag (strange!!). When I troubleshooted I found that whenever I create an outlet of scrollview it behaves this way, if I remove the connection of scrollview to the outlet it works fine, but I need the outlet for the scrollview to work. Any solution on how I can get this done? This is a sample code of my view did load if it helps
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"bg.png"]];
self.scrollView.contentSize =CGSizeMake(320, 500);
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(resignKeyboard)];
[self.scrollView addGestureRecognizer:singleTap];
singleTap.numberOfTapsRequired = 1;
}
-(void) resignKeyboard
{
[self.view endEditing:YES];
}
This is how I defined the outlet
#property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
Note that Button2 Click does work but on click and drag/swipe not on single or multiple clicks.
EDIT:I checked and found that it works fine for simulator 6.1 but not for 5.0
Check out that your button 2 is inside the bounds of scrollview. If button's any part will lie outside the parent container's frame, it will not respond to any events. Test it by changing color of background of the scrollview. Also you can set button2.clipToBounds = YES. If button will be out of bounds, that part will be clipped.
I figured out the problem was because of adding tap gesture, because when your button is inside scrollview the first tap it works for scrollview and not button, so all I had to do is check if touch is for button or not. Here is the code that fixed this issue.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (self.scrollView.superview != nil) {
if ([touch.view isKindOfClass:[UIButton class]])
{
return NO; // ignore the touch
}
}
return YES; // handle the touch
}
Add delegate UIGestureRecognizerDelegate and in viewdidload add this line in the end
singleTap.delegate = self;
I had similar issue. What I did was remove entire hierarchy involving problematic view (in this case Button 2 and scroll view), then recreate them. Then recreate outlets and IBActions.
This works because storyboard is an xml document and sometimes xcode messes things up in writing it.

UIScrollview delaysContentTouches issue

I have UIScrollView loaded with UIButtons and on UIButton action I have highlighted UIImage of each UIButton.
If I don't set delaysContentTouches as NO then highlighted UIImage of UIButton will not shown if I touch up UIButton very fast. After I set delaysContentTouches property as NO then only UIButton highlighted UIImage is shown.
Now after setting delaysContentTouches property as NO for UIScrollView. I can not scroll my UIScrollView by dragging on the UIButtons. Now how can I resolve this issue.
Please give me an advise.
Thanks in advance.
Here's what works for me. Subclass UIScrollView, and implement only this method:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return YES;
}
Then set delaysContentTouches = NO;
Voila! Works just like the home screen: Highlights buttons immediately, but still allows scrolling :)
I found that in iOS 8, the UIScrollView's underlying UIPanGestureRecognizer is not respecting the UIScrollView's delaysContentTouches property. I consider this an iOS 8 bug. Here's my workaround:
theScrollView.panGestureRecognizer.delaysTouchesBegan = theScrollView.delaysContentTouches
OK I have resolved by implementing below method :
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
NSLog(#"touchesShouldCancelInContentView");
if ([view isKindOfClass:[UIButton class]])
return NO;
else
return YES;
}
Unable to find a satisfactory solution online so far (and it seems to be that Apple is ignoring the issue). Found a thread on Apple's developer forum with some suggestions in there that may help: UIScrollView: 'delaysContentTouches' ignored
I was able to use the workaround from this link. To summarize the workaround (I'm para-quoting here):
UIEvent objects contain a time stamp.
You can record the time stamp at the time of touchesBegan on your
embedded subview.
In touchesMoved of scrollView's subview, look at the time stamp and
location again.
If the touch has not moved very far and more than, say, 0.1 seconds
have passed, you can assume the user touched the subview and then
delayed movement.
In this case, the UIScrollView will have decided, independently, that
this is NOT a scrolling action even though it will never tell you
that.
So, you can have a local state variable to flag that this condition of
delayed movement occurred and process events received by the subview.
Here's my code:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// store the timestamp
_beginDragTimeStamp = event.timestamp;
// your embedded subview's touches begin code
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// compare and ignore drag if time passed between tap and drag is less than 0.5s
if(event.timestamp - _beginDragTimeStamp < 0.5) return;
// your drag code
}
I had same issue & same hierarchy of the views, With latest sdk , just use it :
Setting delaysContentTouches to NO for UIButton in the same UITableViewCell.
self.scrollview.delaysContentTouches = NO
Create a subclass of the UIScrollView (or UITableView, or UICollectionView, or any other UIScrollView subclass that you use).
Implement the below method:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
if ([view isKindOfClass:UIButton.class]) {
return YES;
}
return [super touchesShouldCancelInContentView:view];
}
Set this subclass at xib/storyboard as a "Custom Class" class if you use the interface builder.
Unselect Delay Touch Down in a xib or set delaysContentTouches = NO in code.

iOS: setting Exclusive Touch to all buttons in a view

My app has many buttons in a Window and I want to set Exclusive Touch all of them together. Do you have any suggestion about this? Thanks
There is a way to set exclusive touch to all buttons in your app, may be helpful.
#import </usr/include/objc/objc-class.h>
static IMP gOringinalWillMoveToSuperview = nil;
static id newMoveToSuperviewPlusSettingExclusiveTouch(id self,SEL selector,...)
{
va_list arg_list;
va_start( arg_list,selector);
gOringinalWillMoveToSuperview(self,selector,arg_list);
[self setExclusiveTouch:YES];
return nil;
}
-(void)addSettingExclusiveTouchToAllUIViewMethodWillMoveToSuperview
{
gOringinalWillMoveToSuperview = class_getMethodImplementation([UIButton class], #selector(willMoveToSuperview:));
class_replaceMethod([UIButton class], #selector(willMoveToSuperview:), &newMoveToSuperviewPlusSettingExclusiveTouch, "v#:");
}
if you don't understand this, you can refer to this and this.
Are you just looking for an easy way to set them all at once?
If you have all the buttons in an array (e.g. they're all connected to the same IBOutletCollection) you can use key value coding to set the exclusiveTouch property of the array:
[buttonArray setValue:[NSNumber numberWithBool:YES] forKey:#"exclusiveTouch"];
NSArray will then invoke the same method on every item in the array.
-(void)setExclusiveTouchForButtons:(UIView *)myView
{
for (UIView * v in [myView subviews]) {
if([v isKindOfClass:[UIButton class]])
[((UIButton *)v) setExclusiveTouch:YES];
else if ([v isKindOfClass:[UIView class]]){
[self setExclusiveTouchForButtons:v];
}
}
}
then call this function at viewDidAppear
If these buttons are all in the same view, you can loop through the view's subviews, test for whether the particular subview is a button (or test for a tag if you have one set) and set exclusiveTouch on each.
I just found an answer for this:
#pragma mark Set Buttons Exclusive Touch Yes
-(void)setExclusiveTouchForButtons:(UIView *)myView
{
for (UIView * button in [myView subviews]) {
if([button isKindOfClass:[UIButton class]])
[((UIButton *)button) setExclusiveTouch:YES];
}
}
Source
If you want to set exclusiveTouch for ALL UIButtons in your whole application method swizzling will be the perfect solution for you.
This answer explains the way very well : https://stackoverflow.com/a/24534814/976246 , and it works perfectly for me.
Also go through this article to know how this (http://nshipster.com/method-swizzling/) tecknique can be used for various purposes.
If you are adding buttons pragmatically, then send a message to the button [button setExclusiveTouch:YES]; for each buttons before adding to its super view. Else if you are using xib, you have to send the same message to the button in viewDidLoad or in loadView.
Here is some code in swift that will set exclusive touch to all buttons in your viewcontroller's view
for button in self.view.subviews {
if(button.isKindOfClass(UIButton)){
(button as! UIButton).exclusiveTouch = true
}
}
Even though the question was asked many years ago, here is a simple way to set isExclusiveTouch for all subviews that are buttons. This example shows code for subviews of self.view. Change self.view to your view of interest.
Seems like cleaner code for anyone with questions on this in the present.
for subview in self.view.subviews
{
if subview is UIButton {
subview.isExclusiveTouch = true
}
}

Resources