iOS 7.0 User interaction disabled for controls inside UITableView backgroundView - ios

I had custom view set as background view to UITableview.Which is utilise to perform some actions when tableview is blank.
I had some buttons on view & a action is associated with each button
For iOS < 7 , action are called properly on buttons inside background view
But for iOS > 7 , actions set on buttons in backgroundView arent getting called.This seems interaction is disabled on backgroundView
Is this an issue with iOS 7.Anyone else face same issue?

There is a UITableViewWrapperView view sitting in front of the background view intercepting interactions. Can you not just use the table's tableHeaderView property instead?

Firstly this is a bad approach to add touchable items to tableView background view.
Secondly I'm not sure my fix will work for your case.
Try to implement following methods in your BackgroundView class:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
[self.superview bringSubviewToFront:self];
}
return hitView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
CGRect rect = self.bounds;
BOOL isInside = CGRectContainsPoint(rect, point);
if(!isInside)
{
for (UIView *view in self.subviews)
{
isInside = CGRectContainsPoint(view.frame, point);
if(isInside)
break;
}
}
return isInside;
}
By overriding those methods you are making all subviews of backgroundView as well as backgroundView itself touch sensitive.
Cheers!
UPDATE:
Sorry, this will not work. BackgroundView is located behind view with cells itself, so it will not receive touch.

As specified in my comment, this is a known issue in iOS7, see the radar (http://openradar.appspot.com/14707569).
But my solution or workaround to this was to implement a 'proxy' view on top of the table which offers a protocol to forward the hitTest to a Delegate who implements that protocol.
EventFixBackrgoundView.h
#protocol EventFixBackrgoundViewDelegate <NSObject>
- (UIView *)eventFixHitTest:(CGPoint)point withEvent:(UIEvent *)event;
#end
#interface EventFixBackrgoundView:UIView
#property (nonatomic, weak) id <EventFixBackrgoundViewDelegate> delegate;
#end
EventFixBackrgoundView.m
#import "EventFixBackrgoundView.h"
#implementation EventFixBackrgoundView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.delegate && [self.delegate respondsToSelector:#selector(eventFixHitTest:withEvent:)])
{
return [self.delegate eventFixHitTest:point withEvent:event];
}
return [super hitTest:point withEvent:event];
}
#end

Related

Enable UserInteraction to some part of UIViewController

I have been researching about it since last 3 to 4 hours but I didn't get any information. My issue is I want to enable userInteraction to some part of the UIViewController.
Description:
I have a UIViewController. I have added 30 tableviews. I have stored one value in the application. If that value is 1 then I have to enable user interaction for tableview1 only and if the value is 2 then tableview2 only ........etc
. Please let me know if I am not clear. Thank you for spending your valuable time. Thanks in advance
A simple way to do it is to subclass UIView and override - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event.
Return NO for the part of the UIView (represented as ignoreRect in the sample) you want the subviews to ignore touches.
#interface InteractionView ()
#property (nonatomic) CGRect ignoreRect;
#end
#implementation InteractionView
- (void)awakeFromNib {
self.ignoreRect = CGRectMake(0.0f, 0.0f, 300.0f, 300.0f);
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(self.ignoreRect, point)) {
return NO;
}
return [super pointInside:point withEvent:event];
}
#end
If you need more tweaking for an expected behavior : for exemple return a specific view for a specific zone, return the top view of a specific zone, ... you may use
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(self.ignoreRect, point)) {
return nil; // Edit that part if you want to return a chosen view
}
return [super hitTest:point withEvent:event];
}
Another solution without - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event. is to adding UIButton as a subview to the part of UIView you want to close interaction.
for example if you want to close interaction of the bottom half of view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, self.view.frame.size.height*0.5f, self.view.frame.size.width, self.view.frame.size.height*0.5);
[self.view addSubview:button];
since it will get the touch events, half of the view will be closed to user interaction.
EDIT
IBOutletCollection(UITableView) NSArray *allTableViews;// get all your tableviews reference to this array. set tag in interface builder for each array to reach later.
then when you want to enable/disable interaction of related tableview
int tagToOpenInteraction = 1;//or whatever it is
for(UITableView *t in allTableViews)
{
if(t.tag == tagToOpenInteraction)
[t setUserInteractionEnabled:YES];
else
[t setUserInteractionEnabled:NO];
}

Detecting a touch anywhere on the screen

I am wanting to know when a user has touched anywhere on the screen of my app.
I have looked into using -(UIResponder *)nextResponder but unfortunately this will not work, as I am also reloaded a table automatically, so this gets trigged when that occurs.
I have also tried a gesture recognizer, with the following code. But this will only recognise touches on the view. Where as I have many buttons the user will be using to operate the app. I would like to avoid adding a gesture recogniser or code for this in every button and segment control I have on the screen
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapOnView:)];
[self.mainView addGestureRecognizer:tap];
- (void)tapOnView:(UITapGestureRecognizer *)sender
{
//do something
}
I have also tried -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event , but this has the same issue as the gesture recognizer.
I was wondering if there is any way I could achieve this task. I was hoping that I may be able to recognise the type of event from within the nextResponder, and then I could detect if it is button for example.
EDIT: The reason I am working on this is that my app needs to stay active and the screen cannot be locked (so I have disabled screen locking). To avoid excessive use of power, I need to dim the screen, but then return the brightness back to the original level once the app is touched. I need this feature to only occur on 1 of my viewcontrollers.
As mentioned by Ian MacDonald, using hitTest:: is a great solution to detect user interaction on an app wide scale, including when buttons, textfields, etc, are selected.
My solution was to subclass UIWindow and implement the hitTest method.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// do your stuff here
// return nil if you want to prevent interaction with UI elements
return [super hitTest:point withEvent:event];
}
You could attach your UITapGestureRecognizer to your [[UIApplication sharedApplication] keyWindow].
Alternatively, you could override hitTest: of your root UIView.
Is there a particular task you are hoping to accomplish? There may be a better way than assigning an "anywhere" gesture.
Edit: Use hitTest:.
#interface PassthroughView : UIView
#property (readonly) id target;
#property (readonly) SEL selector;
#end
#implementation PassthroughView
- (void)setTarget:(id)target selector:(SEL)selector {
_target = target;
_selector = selector;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
[_target performSelector:_selector];
return nil;
}
#end
#implementation YourUIViewController {
PassthroughView *anytouchView;
}
- (void)viewDidLoad {
// Add this at the end so it's above all other views.
anytouchView = [[PassthroughView alloc] initWithFrame:self.view.bounds];
[anytouchView setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[anytouchView setTarget:self selector:#selector(undim)];
[anytouchView setHidden:YES];
[self.view addSubview:anytouchView];
}
- (void)undim {
[anytouchView setHidden:YES];
}
- (void)dim {
[anytouchView setHidden:NO];
}
#end
Your edit adds more clarity to your question.
The reason I am working on this is that my app needs to stay active
and the screen cannot be locked (so I have disabled screen locking).
To avoid excessive use of power, I need to dim the screen, but then
return the brightness back to the original level once the app is
touched.
Since you are controlling the screen brightness, you can add one transparent view controller before dimming screen on top of your root controller which does only one job, listen to tap using Tap gesture. And on tap you can dismiss the view controller and adjust brightness to previous state.
By doing so you dont have to worry about buttons being clicked as they will be below the transparent view controller. Since its a whole new view controller sitting on top of stack you dont have to modify your existing code as well.
Ok I have had a similar problem before.
As I remember I subclassed the UIWindow for full screen detection and made it First responder.
Than I overridden the touch to handle from subclasses.
You can also use code to identify the control that is been touched.
#import <QuartzCore/QuartzCore.h>
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view setMultipleTouchEnabled:YES];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Enumerate over all the touches
[touches enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
// Get a single touch and it's location
UITouch *touch = obj;
CGPoint touchPoint = [touch locationInView:self.view];
...
}];
}
To disable the locking of screen I used below code:
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
I used following functions to dim or increase the screen brightness
[[UIScreen mainScreen] setBrightness:0.0f]; //and
[[UIScreen mainScreen] setBrightness:1.0f];

hitTest to dismiss keyboard causing weird behaviour

I've googled on how to dismiss keyboard when touching a blank area in UITableView in iOS, and there're several ways to solve this. Like using delegate, UITapGestureRecognizer, - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event and - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event.
I decide to take hitTest by subclassing the corresponding UIView class and override this method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *result = [super hitTest:point withEvent:event];
[self endEditing:YES];
return result;
}
This really works, will dismiss the virtual keyboard when I touch / scroll / swipe / pinch ... somewhere else, but another problem shows up.
The keyboard is active or shown when I touch one UITextField object, then I touch the same UITextField object, here is the problem, the keyboard try to dismiss but not completely, in the middle of somewhere it begin to show up, doing this kind of weird animation. Most of the case in our APPs, the keyboard should stay still when we touch the same UITextField object. Is there a nice and simple way to solve this problem?
Solved:
Finally, I figure it out myself. Thanks to #Wain, thanks to #Wain's hint. I do check the result before I invoke [self endEditing:YES];. Here is the modified code:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *result = [super hitTest:point withEvent:event];
// You can change the following condition to meet your own needs
if (![result isMemberOfClass:[UITextField class]] && ![result isMemberOfClass:[UITextView class]]) {
[self endEditing:YES];
}
return result;
}
As per #iBCode answer, if I select same text field multiple times then keyboard automatically hide and show immediately, to avoid that I have added one more condition and added code below in swift languagge
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let resultView = super.hitTest(point, with: event) {
if resultView.isMember(of: UITextField.self) ||
resultView.isKind(of: UITextField.self) ||
resultView.isMember(of: UITextView.self) ||
resultView.isKind(of: UITextView.self) {
return resultView
}
if !resultView.isMember(of: UITextField.self) && !resultView.isMember(of: UITextView.self) {
endEditing(true)
}
return resultView
}
return nil
}
Check the class of the result so that you can limit when you end editing. If the hit target was an editable class (like a text field, or a switch) then don't end the editing session.

Handling touchesBegan in the main view - how to get ahold of the touched subview?

In an single view iPhone app I have the following structure: scrollView -> contentView -> imageView and underneath (at the main view) there are 7 draggable custom views - which can be dragged from/to the contenView.
Initially dragging was handled in the custom views (see Tile.m#2eb672523a) and while this worked well it required sending notifications to the main view.
So I'm trying to move touchesBegan and friends to the main app view (see ViewController.m).
My problem is:
Initially I had this code in the contentView (see GameBoard.m) to enable subview dragging at the scrollView:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : result;
}
However when trying to drag a tile back from contentView to the main view (in the direction shown in the below screenshot) this wouldn't deliver touchesBegan to the main view.
So I have changed this method to return the main view instead of the tiles:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : self.window.rootViewController.view;
}
Now touchesBegan is called on the main view (as I can see in the debugger), but the touches.view is pointing to the main view too.
My question is:
How to find the touched tile in the new touchesBegan method?
Nevermind, I've solved my problem.
In ViewController.m I've added a _draggedTile ivar and set it in touchesBegan by using the following method to find the touched tile:
- (SmallTile*) findTileAtPoint:(CGPoint)point withEvent:(UIEvent*)event
{
NSArray* children = [self.view.subviews arrayByAddingObjectsFromArray:_contentView.subviews];
for (UIView* child in children) {
CGPoint localPoint = [child convertPoint:point fromView:self.view];
if ([child isKindOfClass:[SmallTile class]] &&
[child pointInside:localPoint withEvent:event]) {
return (SmallTile*)child;
}
}
return nil;
}
And in touchedMoved, touchesEnded, touchesCancelled I just use that ivar (initially I was using the above findTileAtPoint:withEvent: method there as well, but sometimes the touch wouldn't be exactly over the dragged tile and the movement was jerky or cancelled).

how to disregard touch events in topmost uiview when it is clear and a different uiview can handle them

I have a clear UIView which has gesture recognizers attached to it.
This clear uiview covers the entire super view to allow for the gestures to be invoked from anywhere on it.
Under this clear UIView sit different components such as tables,buttons,collectionview etc.
The clear UIView has no idea what is under it any time.
What I want - if a view which is under the clear uiview can handle a touch event (or any type of gesture) - the clear view should disregard that event - and the event will pass through to the underlying view which could handle it.
I tried
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
but I don't know how to make sure the underlying view can handle it.
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
return nil;
}
else
{
return hitView;
}
}
Add this to your to clear view.
If the hit on clear view means just return nil.
You can override pointInside: withEvent: method. This method returns a boolean value indicating whether the receiver contains the specified point. So if we return NO then your upper clear view will become transparent for touch events and they will be passed to underlying views.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Clear UIView will now respond to touch events if return NO:
return NO;
}
use below code for your case->
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitTestView = [super hitTest:point withEvent:event];
if(hitTestView!=nil){
//check for gesture
if([hitTestView.gestureRecognizers count]>0)
return hitTestView;
//if it is subclass of UIControl like UIButton etc
else if([hitTestView isKindOfClass:[UIControl class]])
return hitTestView;
//if can handle touches
else if([hitTestView respondsToSelector:#selector(touchesBegan:withEvent:)])
return hitTestView;
else
return nil;
}
else{
return self;
}
}
In the above code if the subView which is hitView can anyway handle touch ,we return that object to handle that touch. If there is no such hitTest view, then we return the view itself.
I used some of these suggestions and used the following solution:
I added the gesture recognizer to the bottom most superview in the heirarchy (and not the top most)
Then in that class over rid
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
// if v is nil then touch wasn't in this view or its subviews
if (v == nil)
{
return nil;
}
// in any case if the topview was hidden than return the default value
if (self.myTopView.hidden)
{
return v;
}
// if the view isn't hidden but the touch returned a control - than we can pass the touch to the control
if ([v isKindOfClass:[UIControl class]])
{
return v;
}
// decide on what threshold to decide is a touch
CGFloat threshHold = 40;
// if the touch wasn't on a control but could initiate a gesture than that view should get the touch
if (v.gestureRecognizers)
{
threshHold = 30;
// return v;
}
// check if the threshold should be bigger
if ([self someCondition])
{
threshHold = 100;
}
// threshold according to its position - this is the dynamic part
if (point.y > (self.myTopView.frame.origin.y - threshold))
{
return self.handleBarView;
}
return v;
}

Resources