I have a point CGPoint in coordinates of the application window.
Is there a way to get a pointer to the view visible at that point?
E.g. I have 100 views or random sizes and placed at random locations - some partially/fully covering each other.
Pressing a button in the app will list the address of the view visible at (0,0), (100,100) and at (200,200).
The views may or may not have userInteraction enabled.
Also, what about the more complicated scenario where a view is covered by a fully transparent view?
Updated
hitTest will not work as it will not give you subtrees where a parent has userInteractionEnabled set to NO or a view which is outside of its parent's bounds.
visibleViewAtPoint as described below will parse the entire view tree and give the view with the highest subview index. This should give the correct view (does not take z-order into consideration...)
- (void) findView:(UIView**)visibleView atPoint:(CGPoint)pt fromParent:(UIView*)parentView
{
UIView *applicationWindowView = [[[[UIApplication sharedApplication] keyWindow] rootViewController] view];
if(parentView == nil) {
parentView = applicationWindowView;
}
for(UIView *view in parentView.subviews)
{
if((view.superview != nil) && (view.hidden == NO) && (view.alpha > 0))
{
CGPoint pointInView = [applicationWindowView convertPoint:pt toView:view];
if([view pointInside:pointInView withEvent:nil]) {
*visibleView = view;
}
[self findView:visibleView atPoint:pt fromParent:view];
}
}
}
- (UIView*) visibleViewAtPoint:(CGPoint)pt
{
UIView *visibleView = nil;
[self findView:&visibleView atPoint:pt fromParent:nil];
return visibleView;
}
Related
I'm trying to create a custom keyboard for a UITextField, the background of this inputView should be transparent, I have set the background color in the view's xib file to "clear color". It is working great on iOS 6 and earlier.. but on iOS 7 it not working
Any idea how can I make it work? I want it to be fully transparent
This will set the backdrops opacity to zero when displaying your custom keyboard and reset it back to 1 when the normal keyboard is shown.
+ (void)updateKeyboardBackground {
UIView *peripheralHostView = [[[[[UIApplication sharedApplication] windows] lastObject] subviews] lastObject];
UIView *backdropView;
CustomKeyboard *customKeyboard;
if ([peripheralHostView isKindOfClass:NSClassFromString(#"UIPeripheralHostView")]) {
for (UIView *view in [peripheralHostView subviews]) {
if ([view isKindOfClass:[CustomKeyboard class]]) {
customKeyboard = (CustomKeyboard *)view;
} else if ([view isKindOfClass:NSClassFromString(#"UIKBInputBackdropView")]) {
backdropView = view;
}
}
}
if (customKeyboard && backdropView) {
[[backdropView layer] setOpacity:0];
} else if (backdropView) {
[[backdropView layer] setOpacity:1];
}
}
+ (void)keyboardWillShow {
[self performSelector:#selector(updateKeyboardBackground) withObject:nil afterDelay:0];
}
+ (void)load {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(keyboardWillShow) name:UIKeyboardWillShowNotification object:nil];
}
I am chasing the same issue, as I have a numeric keypad which fills only the left half of the screen in landscape mode (and is basically unusable on iOS7 where the blur effect covers the entire width of the screen). I haven't quite figured out how to get what I want (blurred background only behind my actual inputView), but I have figured out how to disable the blur entirely:
Define a custom subclass of UIView and specify that in your xib file
In this class override willMoveToSuperview: as follows
- (void)willMoveToSuperview:(UIView *)newSuperview
{
if (UIDevice.currentDevice.systemVersion.floatValue >= 7 &&
newSuperview != nil)
{
CALayer *layer = newSuperview.layer;
NSArray *subls = layer.sublayers;
CALayer *blurLayer = [subls objectAtIndex:0];
[blurLayer setOpacity:0];
}
}
This appears to impact the background of every custom inputView I have (but not the system keyboard) so you might need to save/restore whatever the normal opacity value is when your inputView gets removed from the superview if you don't want that.
iOS 7 is doing some things under the hood that are not documented. However, you can examine the view hierarchy and adjust the relevant views by overriding -willMoveToSuperview in your custom input view. For instance, this code will make the backdrop transparent:
- (void)willMoveToSuperview:(UIView *)newSuperview {
NSLog(#"will move to superview of class: %# with sibling views: %#", [newSuperview class], newSuperview.subviews);
if ([newSuperview isKindOfClass:NSClassFromString(#"UIPeripheralHostView")]) {
UIView* aSiblingView;
for (aSiblingView in newSuperview.subviews) {
if ([aSiblingView isKindOfClass:NSClassFromString(#"UIKBInputBackdropView")]) {
aSiblingView.alpha = 0.0;
}
}
}
}
I have created custom navigation bar and added one UIButton over it.
Please take a look at image attached.
I have UIButton with background image added as subview to Custom NavigationBar so portion above GREEN line is clickable not the portion below green line.
I have tried using UITapGestureRecognizer and also the touches methods ie. touchesBegan to UIImageView to handle the touch, but no luck.
I think this is only because my UIButton nontouchable potion is outside of NavigationBar Frame.
Is there any way to click the subview's portion which is ouside its parent view.
The parent view size is less than the Child view . That's why it is non clickable. Asper my knowledge only option you have is try to increase the parent view (Custom Navigationbar) frame size .
Yo need subclass Navigation Bar and add method hitTest:withEvent:. For example:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGRect extraTapRect = ...; // Put here needed rect
if (CGRectContainsPoint(extraTapRect, point)) {
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subviewPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subviewPoint withEvent:event];
if (result != nil) {
return result;
}
}
}
return [super hitTest:point withEvent:event];
}
I got an gridview. Each cell within that grid is clickable. If a cell is clicked, another viewcontroller must be presented as a modal viewcontroller. The presentedviewcontroller must slide in fro the right to the left. After that, the modalviewcontroller can be dismissed with a slide. How do i achieve this? I got some images to show it :
Both views are separate viewcontrollers.
[Solution]
The answer from Matthew pointed me in the right direction. What i needed was a UIPanGestureRecognizer. Because UISwipeGestureRecognizer only registers one single swipe and i needed the view to follow the users finger. I did the following to accomplish it :
If i cell is tapped inside my UICollectionView, the extra view needs to pop up. So i implemented the following code first :
/* The next piece of code represents the action called when a touch event occours on
one of the UICollectionviewCells.
*/
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
NSString* release_id = releases[indexPath.row][0];
// Next boolean makes sure that only one new view can be seen. In the past, a user can click multiple cells and it allocs multiple instances of ReleaseViewController.
if(releaseViewDismissed) {
// Alloc UIViewController and initWithReleaseID does a request to a server to initialize some data.
ReleaseViewController *releaseViewController = [[ReleaseViewController alloc] initWithReleaseID: release_id];
// Create a new UIView and assign the height and width of the grid
UIView *releaseViewHolder = [[UIView alloc] initWithFrame:CGRectMake(gridSize.width, 0, gridSize.width, gridSize.height)];
// Add the view of the releaseViewController as a subview of the newly created view.
[releaseViewHolder addSubview:releaseViewController.view];
// Then add the UIView with the view of the releaseViewController to the current UIViewController's view.
[self.view addSubview:releaseViewHolder];
// Place the x coordinate of the new view to the same as width of the screen. Then after that get the x to 0 with an animation.
[UIView animateWithDuration:0.3 animations:^{
releaseViewHolder.frame = CGRectMake(0, 0, releaseViewHolder.frame.size.width, releaseViewHolder.frame.size.height);
// This is important. alloc an UIPanGestureRecognizer and set the method that handles those events to handleSwipes.
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipes:)];
// Add the UIPanGestureRecognizer to the created view.
[releaseViewHolder addGestureRecognizer:_panGestureRecognizer];
releaseViewDismissed = NO;
}];
}
}
Then my handleSwipes is as follows:
-(void)handleSwipes:(UIPanGestureRecognizer *)sender {
CGPoint translatedPoint = [(UIPanGestureRecognizer*)sender translationInView:self.view];
CGPoint translation = [sender translationInView:sender.view];
CGRect newFrame = [sender view].frame;
[sender setTranslation:CGPointZero inView:sender.view];
if (sender.state == UIGestureRecognizerStateChanged)
{
newFrame.origin.x = newFrame.origin.x + translation.x;
// Makes sure it can't go beyond the left of the screen.
if(newFrame.origin.x > 0) {
[sender view].frame = newFrame;
}
}
if(sender.state == UIGestureRecognizerStateEnded){
CGRect newFrame = [sender view].frame;
CGFloat velocityX = (0.3*[(UIPanGestureRecognizer*)sender velocityInView:self.view].x);
// If the user swipes less then half of the screen, it has to bounce back.
if(newFrame.origin.x < ([sender view].bounds.size.width/2)) {
newFrame.origin.x = 0;
}
// If a user swipes fast, the velocity is added to the new x of the frame.
if(newFrame.origin.x + velocityX > ([sender view].bounds.size.width/2)) {
newFrame.origin.x = [sender view].bounds.size.width + velocityX;
releaseViewDismissed = YES;
}
// Do it all with a animation.
[UIView animateWithDuration:0.25
animations:^{
[sender view].frame = newFrame;
}
completion:^(BOOL finished){
if(releaseViewDismissed) {
// Finally remove the new view from the superView.
[[sender view] removeFromSuperview];
}
}];
}
}
If you want the presented view controller to slide in from the right to the left, it cannot be a modal view. #Juan suggested one way to achieve the right to left and swipe back, but it would result in the grid view being pushed out of the way by the new view. If you would like the new view to cover the grid view when it slides in, you will either need to accept the vertical slide of modal views or write your own code to slide the view in from the right -- the latter would not actually be all that difficult*.
As for the swipe to get back, the easiest way to do that from either a modally presented view or a view you animate in yourself is to use a UISwipeGestureRecognizer. You create the recognizer, tell it what direction of swipe to look for, and you tell it what method to call when the swipe occurs.
*The gist of this approach is to create a UIView, add it as a subview of the grid view, and give it the same frame as your grid view but an x-position equal to the width of the view, and then use the following code to make the view animate in from the right.
[UIView animateWithDuration:0.3 animations:^{
slidingView.frame = CGRectMake(0, 0, slidingView.frame.size.width, slidingView.frame.size.height);
}];
I believe what you need is the following:
Create another controller that is going to handle navigation between these two (ContentViewController for example). This controller should have a ScrollView with paging enabled.
Here is a simple tutorial if you donĀ“t already know how to do this: click here
Once the cell is clicked you have to:
Create the new ViewController to be shown.
Enable paging and add this ViewController to the ContentViewController
Force paging to this newly created ViewController
Additionally you have to add some logic so that when the user swipes to change back to the first page, paging is disabled until a new cell is clicked to repeat the process.
I'm trying to detect a touch on a UISubview of a view being animated
Here's my detection code:
//simple but not very elegant way of getting correct frame
CGRect keepFrame = self.fishContainer.layer.frame;
self.fishContainer.layer.frame = [[self.fishContainer.layer presentationLayer] frame];
//get touch location, taking presentation layer into account. (See above)
CGPoint p = [sender locationInView:self.fishContainer];
CALayer *layer =[self.fishContainer.layer presentationLayer];
//apply relevant transform
p = CGPointApplyAffineTransform(p,layer.affineTransform);
EBLog(#"checking point %#",NSStringFromCGPoint(p));
UIView *vToRemove = nil;
//find topmost view containing touched point
for (UIView *v in self.parasites) {
EBLog(#"-BOUND %#",NSStringFromCGRect(v.frame));
if(CGRectContainsPoint(v.frame, p))
{
vToRemove = v;
}
}
//OK, we have a view. Let's remove it.
if(vToRemove)
{
EBLog(#"found one");
[vToRemove removeFromSuperview];
[self.parasites removeObject:vToRemove];
if ([self.parasites count] == 0) {
[self showWinnerScreen];
[self stopGame];
}
}
//restore view frame
self.fishContainer.layer.frame = keepFrame;
Everything works correctly as long as I don't animate parasiteArea parentview.
When I animate parasiteArea's parentview (A CAAnimation consisting of move of the view, scale of the view, and rotate of the view) , the touch is outside the bounds of the expected subview.
UPDATE
I manged to get the detection working in most cases (see code above), by using the presentationLayer property and CGPointApplyAffineTransform. There is however, still some cases where it dosnt work.
I guess I need to translate the touch point to the coordinate space of the CAAnimation.
Or something like that? any suggestions?
I ended up using UIView animateWithDuration instead of CAAninmation. For my purpose the limited animation possibles were enough.
A UIScrollView contains several UIView objects; how can I tell if a point on the screen not generated by touches is within a specific subview of the scrollview? so far atempts to determine if the point is in the subview always return the first subview in the subviews array of the parent scrollview, ie the coordinates are relative to the scrollview, not the window.
Here's what I tried (edited)
-(UIView *)viewVisibleInScrollView
{
CGPoint point = CGPointMake(512, 384);
for (UIView *myView in theScrollView.subviews)
{
if(CGRectContainsPoint([myView frame], point))
{
NSLog(#"In View");
return myView;
}
}
return nil;
}
It looks like you're point is relative to the window, and you want it relative to the current view. convertPoint:fromView: should help with this.
There are probably errors here, but it should look something like this:
-(UIView *)viewVisibleInScrollView
{
CGPoint point = CGPointMake(512, 384);
CGPoint relativePoint = [theScrollView convertPoint:point fromView:nil]; // Using nil converts from the window coordinates.
for (UIView *myView in theScrollView.subviews)
{
if(CGRectContainsPoint([myView frame], relativePoint))
{
NSLog(#"In View");
return myView;
}
}
return nil;
}
Here's what I do:
#implementation UIScrollView (FOO)
- (id)foo_subviewAtPoint:(CGPoint)point
{
point.x += self.contentOffset.x;
for (UIView *subview in self.subviews) {
if (CGRectContainsPoint(subview.frame, point)) {
return subview;
}
}
return nil;
}
#end
And here's how I use it:
CGPoint center = [scrollView convertPoint:scrollView.center fromView:scrollView.superview];
UIView *view = [scrollView foo_subviewAtPoint:center];
Some things to note:
The point passed to foo_subviewAtPoint: is expressed in the coordinate system of the scroll view. This is why in the code above I have to convert center to that coordinate system from that of its superview.
I'm using iOS 6.1+ with layout constraints. I've never tested this code with anything else, so YMMV.