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.
Related
How would I go about disabling a UIButton if the UIScrollView has scrolled more than a certain amount?
this is what I've been trying. Perhaps it's the wrong scrollViewDidScroll: delegate method.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (_scrollView.contentOffset.y >= 100) {
mapLaunchButton.enabled = NO;
}
}
thanks for any help
Simple! You'll need to create a variable to store the starting position of the scroll view though. It should be a CGPoint. Set it to the scroll view's content offset in scrollViewWillBeginDragging: (where the scroll view starts moving) and then do comparison in scrollViewDidScroll similarly to how you were doing it before.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
startingPoint = scrollView.contentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y >= startingPoint.y + 100.0f) {
mapLaunchButton.enabled = NO;
}
}
Keep in mind you may need to modify the values I've provided slightly depending on the starting position of the scroll view, and the direction in which you'd like to monitor the changes.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (_scrollView.contentOffset.y >= 100) {
mapLaunchButton.enabled = NO;
}
else {
mapLaunchButton.enabled = YES;
}
}
The code is OK, but you have to add the delegate for the scrollView
- (void)viewDidLoad {
[super viewDidLoad];
// do whatever
...
// Add the delegate for the scrollview
[_scrollView setDelegate:self];
}
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 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;
}
I am trying to add two UIScrollView's into one UIView, Both the scrollView's show up properly, the problem that I am having is how to determine which scrollView is scrolled because based on that I have to populate the images. This is what I am doing:
I have a ViewController with UIScrollViewDelegate.
In the loadView method of my ViewConroller, I do the following:
CGRect scrollViewFrame1;
CGPoint scrollViewPoint1;
scrollViewPoint1.x = 0;
scrollViewPoint1.y = 57;
CGSize scrollViewSize1;
scrollViewSize1.width = 320;
scrollViewSize1.height = 154;
scrollViewFrame1.size = scrollViewSize1;
scrollViewFrame1.origin = scrollViewPoint1;
CGRect scrollViewFrame2;
CGPoint scrollViewPoint2;
scrollViewPoint2.x = 0;
scrollViewPoint2.y = 258;
CGSize scrollViewSize2;
scrollViewSize2.width = 320;
scrollViewSize2.height = 154;
scrollViewFrame2.size = scrollViewSize2;
scrollViewFrame2.origin = scrollViewPoint2;
scrollView1 = [[UIScrollView alloc] initWithFrame:scrollViewFrame1];
scrollView2 = [[UIScrollView alloc] initWithFrame:scrollViewFrame2];
And then:
scrollView1.delegate = self;
scrollView2.delegate = self;
And then:
[self.view addSubView:scrollView1];
[self.view addSubView:scrollView2];
I have one scrollViewDidScroll: method, how do I determine which scrollView this method got invoked by, because based on that i need to populate different images for my scrollView.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//Code to populate the next or previous images for scrollView
// If it was one i am able to show the images
}
Thanks for the help.
Delegate methods send with it the object that sent the message (the UIScrollView in this case). So, all you have to do is check that against your instance variables of scrollView1 and scrollView2.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == scrollView1) {
//do stuff with scrollView1
} else if (scrollView == scrollView2) {
//do stuff with scrollView2
}
}
FYI
You can also set a tag for both of the scrollView to differentiate
scrollView1.tag=10;
scrollView2.tag=11;
[self.view addSubView:scrollView1];
[self.view addSubView:scrollView2];
In your delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView1.tag==10) {
//do stuff with scrollView1
} else if (scrollView2.tag==11) {
//do stuff with scrollView2
}
}
I'm using a UITextView to roughly replicate the SMS text box above the keyboard. I'm using a UITextView instead of a field so that it can expand with multiple lines.
The problem is that, in my UITextView, the correction suggestions pop up below the text, causing them to be partially obscured by the keyboard.
In the SMS app, the suggestions pop up above the text. The placement does not appear to be a property of UITextView, or UITextInputTraits.
Any idea how to replicate this behavior? Thanks!
The problem is that the Keyboard is implemented as a separate UIWindow, rather than as a view within the main UIWindow, so layout with it is tricky. Here are some pointers in the right direction:
Hunt through the application's -windows property to find the private UITextEffectsWindow window and figure out its frame. This is the keyboard
Hunt through the TextView's subviews to find the private UIAutocorrectInlinePrompt view. This is the autocorrect bubble.
Move that subview into a separate wrapper view (added to the TextView) and then move that wrapper view so it's above the above-mentioned keyboard window.
You'll notice two mentions of "private" above. That carries all the relevant caveats. I have no idea why Apple has allowed the problem to persist when even their apps have had to work around it.
By doing the search for the UIAutocorrectInlinePrompt in an overridden or swizzled layoutSubViews it is possible to alter the layout of the correction so that it appears above. You can do this without calling any private APIs by looking for the subs views of particular classes positioned in a way you'd expect them. This example works out which view is which, checks to see that the correction is not already above the text and moves the correction above, and draws it on the window so that it is not bounded by the UITextView itself. Obviously if apple change the underlying implementation then this will fail to move correction. Add this to your overriden or swizzled layoutSubViews implementation.
- (void) moveSpellingCorrection {
for (UIView *view in self.subviews)
{
if ([[[view class] description] isEqualToString:#"UIAutocorrectInlinePrompt"])
{
UIView *correctionShadowView = nil; // [view correctionShadowView];
for (UIView *subview in view.subviews)
{
if ([[[subview class] description] isEqualToString:#"UIAutocorrectShadowView"])
{
correctionShadowView = subview;
break;
}
}
if (correctionShadowView)
{
UIView *typedTextView = nil; //[view typedTextView];
UIView *correctionView = nil; //[view correctionView];
for (UIView *subview in view.subviews)
{
if ([[[subview class] description] isEqualToString:#"UIAutocorrectTextView"])
{
if (CGRectContainsRect(correctionShadowView.frame,subview.frame))
{
correctionView = subview;
}
else
{
typedTextView = subview;
}
}
}
if (correctionView && typedTextView)
{
CGRect textRect = [typedTextView frame];
CGRect correctionRect = [correctionView frame];
if (textRect.origin.y < correctionRect.origin.y)
{
CGAffineTransform moveUp = CGAffineTransformMakeTranslation(0,-50.0);
[correctionView setTransform: moveUp];
[correctionShadowView setTransform: moveUp];
CGRect windowPos = [self convertRect: view.frame toView: nil ];
[view removeFromSuperview];
[self.window addSubview: view];
view.frame = windowPos;
}
}
}
}
}
}
Actually doing
textview.scrollEnabled = NO;
will set the bubble on top of the text... the caveat is that you lose scrolling, in my case it wasn't a problem due to havinng a textfield only for input purposes with character limit
Actually, the keyboard simply uses the result of -[UITextInput textInputView] to determine where to put the correction view (and to ask if your view supports correction). So all you need to do is this:
- (UIView *)textInputView {
for (UIWindow *window in [UIApplication sharedApplication].windows) {
if ([window isKindOfClass:NSClassFromString(#"UITextEffectsWindow")] &&
window != self.window) {
return window;
}
}
// Fallback just in case the UITextEffectsWindow has not yet been created.
return self;
}
Note that you'll likely also need to update -[UITextInput firstRectForRange:] to use the coordinate system of the window / device, so you can do this:
- (CGRect)firstRectForRange:(CoreTextTokenTextRange *)range {
CGRect firstRect = [self firstRectForRangeInternal:range];
return [self convertRect:firstRect toView:[self textInputView]];
}
(In the above context, self is a class that implements UITextInput).
If the bottom of your UITextView clears the keyboard, you should be able to just resize your UITextView to be tall enough to see the corrections. The corrections themselves don't display outside of the UITextView's frame.
If you want to mimic what you are getting in the SMS app (corrections above), you'll probably have to roll your own.
Putting the below method, adjustAutocorrectPromptView in layoutSubviews worked for me in portrait and landscape. I have a category that provides the bottom and top methods on view but you get the idea.
NSArray * subviewsWithDescription(UIView *view, NSString *description)
{
return [view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"class.description == '%#'", description]]];
}
- (void) adjustAutocorrectPromptView;
{
UIView *autocorrectPromptView = [subviewsWithDescription(self, #"UIAutocorrectInlinePrompt") lastObject];
if (! autocorrectPromptView)
{
return;
}
UIView *correctionShadowView = [subviewsWithDescription(autocorrectPromptView, #"UIAutocorrectShadowView") lastObject];
if (! correctionShadowView)
{
return;
}
UIView *typedTextView = nil; //[view typedTextView];
UIView *correctionView = nil; //[view correctionView];
for (UIView *subview in subviewsWithDescription(autocorrectPromptView, #"UIAutocorrectTextView"))
{
if (CGRectContainsRect(correctionShadowView.frame,subview.frame))
{
correctionView = subview;
}
else
{
typedTextView = subview;
}
}
if (correctionView && typedTextView)
{
if (typedTextView.top < correctionView.top)
{
correctionView.bottom = typedTextView.top;
correctionShadowView.center = correctionView.center;
}
}
}
Make sure your view controller delegate is listening to the notification when the keyboard pops up so that you resize your UITextView so that the keyboard doesn't obscure the UITextView. Then your correction won't be obscured by the keyboard. See:
http://www.iphonedevsdk.com/forum/iphone-sdk-development/12641-uitextview-scroll-while-editing.html
Here is a copy of the code from that page in case the original link is broken:
// the amount of vertical shift upwards keep the Notes text view visible as the keyboard appears
#define kOFFSET_FOR_KEYBOARD 140.0
// the duration of the animation for the view shift
#define kVerticalOffsetAnimationDuration 0.50
- (IBAction)textFieldDoneEditing:(id)sender
{
[sender resignFirstResponder];
}
- (IBAction)backgroundClick:(id)sender
{
[latitudeField resignFirstResponder];
[longitudeField resignFirstResponder];
[notesField resignFirstResponder];
if (viewShifted)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:kVerticalOffsetAnimationDuration];
CGRect rect = self.view.frame;
rect.origin.y += kOFFSET_FOR_KEYBOARD;
rect.size.height -= kOFFSET_FOR_KEYBOARD;
self.view.frame = rect;
[UIView commitAnimations];
viewShifted = FALSE;
}
}
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
if (!viewShifted) { // don't shift if it's already shifted
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:kVerticalOffsetAnimationDuration];
CGRect rect = self.view.frame;
rect.origin.y -= kOFFSET_FOR_KEYBOARD;
rect.size.height += kOFFSET_FOR_KEYBOARD;
self.view.frame = rect;
[UIView commitAnimations];
viewShifted = TRUE;
}
return YES;
}