My App calls scrollViewDidScroll 19 times - objective-c

I have an app based on Apple's PageControl sample. The first time the view loads, the scroll view is loaded with page 0 and page 1. Whenever a scroll is initiated, the scrollViewDidScroll method should get called by UIKit correct?
When initiating a scroll from page 0 to page 1, the app should load page-1, page and page+1, (to prevent flashes during scrolling).
My app seems to call scrollViewDidScroll 19 times and my loadScrollViewWithPage: method 19 times each with page 0 and page 1, before it finally gets to page 1 and 2, then it crashes.
These are the methods:
- (void)scrollViewDidScroll:(UIScrollView *)sender {
NSLog(#"scrollviewdidscroll");
// We don't want a "feedback loop" between the UIPageControl and the scroll delegate in
// which a scroll event generated from the user hitting the page control triggers updates from
// the delegate method. We use a boolean to disable the delegate logic when the page control is used.
if (pageControlUsed) {
// do nothing - the scroll was initiated from the page control, not the user dragging
return;
}
// Switch the indicator when more than 50% of the previous/next page is visible
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
pageControl.currentPage = page;
// load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
// A possible optimization would be to unload the views+controllers which are no longer visible
}
- (void)loadScrollViewWithPage:(int)page {
if (page < 0) return;
if (page >= kNumberOfPages) return;
NSLog(#"page: %i", page);
// replace the placeholder if necessary
KeyboardViewController *controller = [viewControllers objectAtIndex:page];
if ((NSNull *)controller == [NSNull null]) {
controller = [[KeyboardViewController alloc] initWithPageNumber:page];
[viewControllers replaceObjectAtIndex:page withObject:controller];
[controller release];
}
// add the controller's view to the scroll view
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
frame.size.height = scrollView.frame.size.height;
controller.view.frame = frame;
[scrollView setAutoresizesSubviews:YES];
[scrollView setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[scrollView addSubview:controller.view];
}
Why would scrollViewDidScroll get called so many times?
Thanks

scrollViewDidScroll: gets called every time the scroll bounds change. This means it gets called during the scroll, as well as when it starts. You may want to try scrollViewWillBeginDragging: instead.

Related

iOS 8 Form Sheet Modal Dialog Resizing After Display

I work on an application where it shows a modal popup by performing a segue using the form sheet modal style and then a horizontal flip animation. In iOS 7 made the form take up the correct screen size by calling:
self.view.superview.bounds = CGRectMake(0,0,300,300);
for example and this set the modal dialogs dimensions to the correct size. There is also a button in this modal dialog that allows the user to see 'more detail' and for this the view grows to show some additional content. We did this again by animating the view and make a call to set the bounds to something a little bigger on the superview.
For Example:
self.view.superview.bounds = CGRectMake(0,0,600,600);
In iOS 7 this worked just great. In iOS 8 however it has stopped working. Instead of modifying the super view's bounds we now have to call:
self.preferredContentSize = CGSizeMake(300, 300);
This resolves the forms appearance after opening but i can't find a way to manipulate the size of the view there after. Setting the super views bounds has no effect.
I can see in iOS8 Apple has made a change to form sheets to have a 3 view hierarchy (your view -> shadow view -> transition view -> window) where as before in iOS 7 it was just a 2 view hierarchy (your view -> shadow view -> window).
I've tried manipulating the super view's super view but that did not have the desired effect.
Is anyone else seeing this / found a way around it? Here is an example project:
https://github.com/ingybing/FormSheet
Run it in iOS 7 Simulator and you will see the desired behaviour. That is what I'm struggling to get working in iOS 8
View Controller Example:
#import "ModalWindowViewController.h"
#interface ModalWindowViewController ()
#property bool minimised;
#property bool isInitialViewLoadLayout;
#end
#define is_iOS8 ([[[UIDevice currentDevice] systemVersion]floatValue] >= 8)
#implementation ModalWindowViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.minimised = YES;
self.isInitialViewLoadLayout = YES;
if(is_iOS8)
{
// Set initial size in iOS 8 etc.
self.preferredContentSize = CGSizeMake(300, 300);
}
}
- (void) viewWillLayoutSubviews
{
if (self.isInitialViewLoadLayout)
{
self.isInitialViewLoadLayout = NO;
if(!is_iOS8)
{
// Set initial size in iOS 6,7 etc.
self.view.superview.bounds = CGRectMake(0, 0, 300, 300);
}
}
}
- (IBAction)minMaxTouched:(id)sender
{
CGRect newSize = CGRectMake(0, 0, 300, 300);
if (self.minimised == YES)
{
newSize = CGRectMake(0, 0, 600, 600);
}
if(is_iOS8)
{
// Doesn't Work
// self.view.superview.bounds = newSize;
// Doesn't seem to do anything either.
self.preferredContentSize = newSize.size;
}
else
{
// Works a treat on ios7
self.view.superview.bounds = newSize;
}
self.minimised = !self.minimised;
}
- (IBAction)closeTouched:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
After much messing around.. if you update the view.superview's bounds to something and then call layoutIfNeeded on the superview. This will cause the willLayoutSubviews method to be invoked. If you set the superview bounds in there, it will update the modal dialog size but not if you do it anywhere else.
Example view controller solution:
#import "ModalWindowViewController.h"
#interface ModalWindowViewController ()
#property bool minimised;
#property CGRect windowBounds;
#end
#define is_iOS8 ([[[UIDevice currentDevice] systemVersion]floatValue] >= 8)
#implementation ModalWindowViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Start with a minimised window size.
self.minimised = YES;
self.windowBounds = CGRectMake(0, 0, 300, 300);
if(is_iOS8)
{
// Set initial size in iOS 8 etc.
self.preferredContentSize = self.windowBounds.size;
}
}
- (void) viewWillLayoutSubviews
{
// Resize the window based on it's current required size.
// The supeview bounds only seems to actually changed on screen
// if changed inside this method.
self.view.superview.bounds = self.windowBounds;
}
- (IBAction)minMaxTouched:(id)sender
{
// Toggle the dimensions of the window bases on minimised / maximised state.
if (self.minimised == YES)
{
self.windowBounds = CGRectMake(0, 0, 600, 600);
}
else
{
self.windowBounds = CGRectMake(0, 0, 300, 300);
}
// Set some value that will get overridden in viewWillLayoutSubviews
// You need to change the view bounds or it won't actually invoke a layout.
self.view.superview.bounds = CGRectMake(0, 0, 100, 100);
// Toggle minimised state before we layout.
self.minimised = !self.minimised;
// Manually request a layout. Since the superview bounds have been changed
// to a temporary value it should cause a layout where we set the real
// desired size.
[self.view.superview layoutIfNeeded];
}
- (IBAction)closeTouched:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
#end

UINavigationController Custom Interactive Transition nested push animation can result in corrupted navigation bar

I am implementing a custom interactive navigation controller transition to "push" a new view controller by panning downwards on the navigation controller's view.
Everything works great, except in situations where (in efforts to break the app), I pan down again on the navigation controller's view to initiate a new interactive pushing of a view controller. If I do it quick enough after just finishing the previous one, I get a warning
"nested push animation can result in corrupted navigation bar"
And eventually as I continue to play with the app, I will get:
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
I understand the for whatever reason, the context is not back in correct state, and there is something weird probably happening in the viewDidAppear/viewWillAppear methods in the pushed controller, but I'm not able to narrow down where exactly to stop the pushing of a new view controller before making sure the last view controller has FOR SURE finished being pushed.
I've played around with transition coordinator's notifyWhenInteractionEndsUsingBlock, by disabling the pan gesture until this block gets called, but this has not helped me.
I've searched on SO for these warnings, but it doesn't seem to apply to my situation - whereas I feel like its something to do with the context not being managed properly.
Here is the code I'm using:
I set a delegate to handle the pan gesture's begin state by pushing the view controller onto the stack:
Application.h
-(void)transitionManagerDidBeginDraggingDown:(TransitionManager *)transitionManager{
MenuViewController *menu = [[MenuViewController alloc] initWithNibName:#"MenuViewController" bundle:nil];
[self.navigationController pushViewController:menu animated:YES];
}
This method below is what initiates the delegate's pushing of the view controller onto the stack:
TransitionManage.h
- (void)panned:(UIPanGestureRecognizer*)gesture{
UIViewController *toVc = [self.context viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVc = [self.context viewControllerForKey:UITransitionContextFromViewControllerKey];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:{
if (self.interactiveTransitionUnderway == NO) {
self.interactive = YES;
CGPoint velocity = [gesture velocityInView:gesture.view];
if (velocity.y < 0) { // we are pulling upwards on the visible view
self.presenting = YES;
[self.delegate transitionManagerDidBeginDraggingUp:self];
}
else{ // we are pulling downwards on the visible view
self.presenting = NO;
[self.delegate transitionManagerDidBeginDraggingDown:self];
}
}
break;
}
case UIGestureRecognizerStateChanged:{
CGPoint touchLocation = [gesture locationInView:gesture.view];
CGPoint translation = [gesture translationInView:gesture.view];
CGPoint updatedCenter = CGPointMake(self.beginPoint.x, self.beginPoint.y + translation.y);
toVc.view.center = updatedCenter;
CGFloat d = fabs(touchLocation.y / CGRectGetHeight(self.parentViewController.view.bounds)) ;
[self.context updateInteractiveTransition:d];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:{
CGPoint velocity = [gesture velocityInView:gesture.view];
CGRect frame;
if (velocity.y < 0)
frame = CGRectMake(0, -toVc.view.frame.size.height, toVc.view.frame.size.width, toVc.view.frame.size.height); // pulling up
else
frame = CGRectMake(0, 0, toVc.view.frame.size.width, toVc.view.frame.size.height); // pulling down
[UIView animateWithDuration:0.75 delay:0.0 usingSpringWithDamping:0.8 initialSpringVelocity:0.0
options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveEaseOut animations:^{
toVc.view.frame = frame;
} completion:^(BOOL finished) {
if (velocity.y < 0){
[self cancelInteractiveTransition];
}
else{
[self finishInteractiveTransition];
UIView *snapshotView = [fromVc.view snapshotViewAfterScreenUpdates:YES];
[toVc.view addSubview:snapshotView];
[toVc.view sendSubviewToBack:snapshotView];
}
[self completeTransition];
}];
break;
}
default:
break;
}
}
- (void)completeTransition{
BOOL finished = ![self.context transitionWasCancelled];
[self.context completeTransition:finished];
}
- (void)animationEnded:(BOOL)transitionCompleted {
// Reset to our default state
self.interactive = NO;
self.presenting = NO;
self.context = nil;
self.interactiveTransitionUnderway = NO;
}
I do not have a full answer to your issue but at least you should prevent your code from pushing a new viewcontroller with animation before the previous view controller is fully pushed (animation finished).

iOS: TapGestureRecognizer Issues

So I have an app that behaves like a photo gallery and I'm implementing the ability for the user to delete the images. Here is the setup: I have 9 UIImageViews, imageView, imageView2, etc. I also have an "Edit" button, and a tapGesture action method. I dragged a Tap Gesture Recognizer over onto my view in IB, and attached it to each one of the UIImageViews. I also attached the tapGesture action method to each of the UIImageViews. Ideally, I would like the method to only become active when the "Edit" button is pressed. When the user taps Edit, then taps on the picture they want to delete, I would like a UIAlertView to appear, asking if they are sure they want to delete it. Here is the code I'm using:
- (IBAction)editButtonPressed:(id)sender {
editButton.hidden = YES;
backToGalleryButton.hidden = NO;
tapToDeleteLabel.hidden = NO;
}
- (IBAction)tapGesture:(UITapGestureRecognizer*)gesture
{
UIAlertView *deleteAlertView = [[UIAlertView alloc] initWithTitle:#"Delete"
message:#"Are you sure you want to delete this photo?"
delegate:self
cancelButtonTitle:#"No"
otherButtonTitles:#"Yes", nil];
[deleteAlertView show];
if (buttonIndex != [alertView cancelButtonIndex]) {
UIImageView *view = [self.UIImageView];
if (view) {
[self.array removeObject:view];
}
CGPoint tapLocation = [gesture locationInView: self.view];
for (UIImageView *imageView in self.view.subviews) {
if (CGRectContainsPoint(self.UIImageView.frame, tapLocation)) {
((UIImageView *)[self.view]).image =nil;
}
}
[self.user setObject:self.array forKey:#"images"];
}
}
This code is obviously riddled with errors:
"Use of undeclared identifier button index" on this line: if (buttonIndex != [alertView cancelButtonIndex])
"Property UIImageView not found on object of type PhotoViewController" on this line UIImageView *view = [self.UIImageView];
And "Expected identifier" on this line ((UIImageView *)[self.view]).image =nil;
I'm very new to programming, and I'm surprised that I even made it this far. So, I'm just trying to figure out how I need to edit my code so that the errors go away, and that it can be used whenever one of the 9 image views is tapped, and also so that this method only fires when the Edit button is pushed first. I was using tags earlier, and it worked great, but I save the images via NSData, so I can't use tags anymore. Any help is much appreciated, thanks!
First, you don't want to attach the tap gesture to the image views. Also, if you are going to have more than 9 images, you may want a scroll view, or handle scrolling separately. First, remove that gesture recognizer and all its connections.
Next, determine what type of view you will use as your gallery canvas. A simple View, or a ScrollView, or whatever... it doesn't really matter right now, just to get it working. You want to ctrl-drag that view into your interface definition, so it drops an IBOutlet for the view (that way you can reference it in code).
You will place your ImageViews onto the view I just mentioned.
You can have an internal flag for the gesture recognizer, but it also has a property that use can enable/disable it whenever you want. Thus, you can have it active/inactive fairly easily.
All you want to do is drop a single tap-gesture-recognizer onto your controller, and connect it to the implementation section of the controller. It will generate a stub for handling the recognizer. It will interpret taps "generally" for the controller, and call your code whenever a tap is made on the view.
Some more code...
Creates a frame for the "new" image in the scroll view.
- (CGRect)frameForData:(MyData*)data atIndex:(NSUInteger)idx
{
CGPoint topLeft;
int row = idx / 4;
int col = idx % 4;
topLeft.x = (col+1)*HORIZ_SPACING + THUMBNAIL_WIDTH * (col);
topLeft.y = (row+1)*VERT_SPACING + THUMBNAIL_HEIGHT * (row);
return CGRectMake(topLeft.x, topLeft.y, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
}
Creates an image view for each piece of metadata, and a small border.
- (UIImageView*)createImageViewFor:(MetaData*)metadata
{
UIImageView *imageView = [[UIImageView alloc] initWithImage:metadata.lastImage];
imageView.frame = CGRectMake(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);;
imageView.layer.borderColor = [[UIColor blackColor] CGColor];
imageView.layer.borderWidth = 2.0;
imageView.userInteractionEnabled = YES;
return imageView;
}
This is where the views are created and added to the parent...
imageView = [self createImageViewFor:metadata];
//[data.imageView sizeToFit];
// Make sure the scrollView contentSize represents the data
CGRect lastFrame = [self frameForData:data atIndex:self.data.count-1];
CGFloat newHeight = lastFrame.origin.y + lastFrame.size.height;
if (self.bookshelfScrollView.contentSize.height < newHeight) {
CGSize newSize = self.bookshelfScrollView.contentSize;
newSize.height = newHeight;
self.bookshelfScrollView.contentSize = newSize;
}
[self.bookshelfScrollView addSubview:data.imageView];
So, you just create each frame, add them to the view, and the only thing you have to do is enable user interaction on them, because otherwise the scroll view does not allow the gesture through.
OK... Looking at the code you posted... since you didn't say what was wrong with it... hard to say... The below is your code... My comments are, well, Comments...
- (IBAction)editButtonPressed:(id)sender {
editButton.hidden = YES;
backToGalleryButton.hidden = NO;
tapToDeleteLabel.hidden = NO;
}
- (IBAction)tapGesture:(UITapGestureRecognizer*)gesture
{
// I don't think I'd do this here, but it shouldn't cause "problems."
UIAlertView *deleteAlertView = [[UIAlertView alloc] initWithTitle:#"Delete"
message:#"Are you sure you want to delete this photo?"
delegate:self
cancelButtonTitle:#"No"
otherButtonTitles:#"Yes", nil];
[deleteAlertView show];
// In your controller, you have the main view, which is the view
// on which you added your UIViews. You need that view. Add it as an IBOutlet
// You should know how to do that... ctrl-drag to the class INTERFACE source.
// Assuming you name it "galleryView"
// Take the tap location from the gesture, and make sure it is in the
// coordinate space of the view. Loop through all the imageViews and
// find the one that contains the point where the finger was taped.
// Then, "remove" that one from its superview...
CGPoint tapLocation = [gesture locationInView: self.galleryView];
for (UIImageView *imageView in self.galleryView.subviews) {
if (CGRectContainsPoint(imageView.frame, tapLocation)) {
[imageView removeFromSuperview];
}
}
}
Personally, in my little photo gallery, I bypassed all of the gesture recognizer stuff by replacing the UIImageView controls with UIButton controls, setting the image property for the button like you would for the UIImageView. It looks identical, but then you get the functionality of tapping on a thumbnail for free, with no gestures needed at all.
So, my view just has a UIScrollView, which which I programmatically add my images as buttons with the image set for the button control, e.g.:
- (void)loadImages
{
listOfImages = [ImageData newArrayOfImagesForGallery:nil];
// some variables to control where I put my thumbnails (which happen to be 76 x 76px)
int const imageWidth = 76;
int const imageHeight = imageWidth;
NSInteger imagesPerRow;
NSInteger imagePadding;
NSInteger cellWidth;
NSInteger cellHeight;
// some variables to keep track of where I am as I load in my images
int row = 0;
int column = 0;
int index = 0;
// add images to navigation bar
for (ImageData *item in listOfImages)
{
// figure out what row and column I'm on
imagesPerRow = self.view.frame.size.width / (imageWidth + 2);
imagePadding = (self.view.frame.size.width - imageWidth*imagesPerRow) / (imagesPerRow + 1);
cellWidth = imageWidth + imagePadding;
cellHeight = imageHeight + imagePadding;
// this is how I happen to grab my UIImage ... this will vary by implementation
UIImage *thumb = [item imageThumbnail];
// assuming I found it...
if (thumb)
{
// create my button and put my thumbnail image in it
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(column * cellWidth + imagePadding,
row * cellHeight + imagePadding,
imageWidth,
imageHeight);
[button setImage:thumb forState:UIControlStateNormal];
[button addTarget:self
action:#selector(buttonClicked:)
forControlEvents:UIControlEventTouchUpInside];
button.tag = index++;
[[button imageView] setContentMode:UIViewContentModeScaleAspectFit];
// add it to my view
[scrollView addSubview:button];
// increment my column (and if necessary row) counters, making my scrollview larger if I need to)
if (++column == imagesPerRow)
{
column = 0;
row++;
[scrollView setContentSize:CGSizeMake(self.view.frame.size.width, (row+1) * cellHeight + imagePadding)];
}
}
}
}
// I also have this in case the user changes orientation, so I'll move my images around if I need to
- (void)rearrangeImages
{
if (!listOfImages)
{
[self loadImages];
return;
}
// a few varibles to keep track of where I am
int const imageWidth = 76;
int const imagesPerRow = self.view.frame.size.width / (imageWidth + 2);
int const imageHeight = imageWidth;
int const imagePadding = (self.view.frame.size.width - imageWidth*imagesPerRow) / (imagesPerRow + 1);
int const cellWidth = imageWidth + imagePadding;
int const cellHeight = imageHeight + imagePadding;
NSArray *buttons = [[NSArray alloc] initWithArray:[scrollView subviews]];
int row;
int column;
int index;
CGRect newFrame;
// iterate through the buttons
for (UIView *button in buttons)
{
index = [button tag];
if ([button isKindOfClass:[UIButton class]] && index < [listOfImages count])
{
// figure out where the button should go
row = floor(index / imagesPerRow);
column = index % imagesPerRow;
newFrame = CGRectMake(column * cellWidth + imagePadding,
row * cellHeight + imagePadding,
imageWidth,
imageHeight);
// if we need to move it, then animation the moving
if (button.frame.origin.x != newFrame.origin.x || button.frame.origin.y != newFrame.origin.y)
{
[UIView animateWithDuration:0.33 animations:^{
[button setFrame:newFrame];
}];
}
}
}
NSInteger numberOfRows = floor(([listOfImages count] - 1) / imagesPerRow) + 1;
[scrollView setContentSize:CGSizeMake(self.view.frame.size.width, numberOfRows * cellHeight + imagePadding)];
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self rearrangeImages];
}
Hopefully this gives you an idea of how you can programmatically add a UIButton to act as an image in a gallery. I've tried to edit this on the fly, so I apologize in advance if I introduced any errors in the process, but it should give you a sense of what you could conceivably do ... I removed my code to do this in a separate GCD queue (which I do because I have close to 100 images, and doing this on the main queue is too slow.)
Anyway, you can then create your buttonClicked method to do your display of your UIAlertView.
- (void)buttonClicked:(UIButton *)sender
{
if (inEditMode)
{
// create your UIAlterView using [sender tag] to know which image you tapped on
}
}
Finally, you have two buttons on your UIAlertView, but you don't seem to check to see what button the user clicked on. Your view controller should be a UIAlterViewDelegate, in which you have defined your alertView:clickedButtonAtIndex which will do your actual edit steps.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
// ok, will do what I need to do with whatever the user tapped in my alertview
}
The easiest thing I can think of off the top of my head is to have the gesture disabled by default, and then have the gesture enabled once the edit button is hit. This way, the picture will only respond to the tap gesture recognizer if it is in "Edit" mode.

How to pass scrolling input to a different view

This site really is awesome.
I have what is hopefully a simple question this time. I would like to pass any scrolling input from the user (could be wheel, touchpad, etc) to an NSScrollView which contains my own subviews.
At the moment if the user scrolls just on the documentView (outside of my subviews' frames) the scroll works normally but if they scroll while the cursor is over a subview nothing happens. So basically I'd like to have the subview recognise the scroll event and pass it back to the scroll view.
Any help is greatly appreciated.
Cheers.
EDIT:
Here is the code I'm using to add the subviews to the documentView
_milestoneView and _activityView are both NSView subclasses which have a corresponding nib (created with instantiateNibWithOwner and objects hooked up accordingly) they contain a NSTextField, PXListView and some have a NSProgressIndicator.
-(void)useProject:(NSNumber *)projectId
{
[self resetView];
NSRect bounds = [[self view] bounds];
NSRect defaultFrame = NSMakeRect(20, NSMaxY(bounds)-93, NSMaxX(bounds)-20, 50);
//Prepare the milestone view
if (_milestoneView == nil)
_milestoneView = [MilestoneView milestoneViewFromNibWithFrame:defaultFrame withProject:[BCLocalFetch projectForId:projectId]];
[_milestoneView reloadList];
//Prepare the activity view
if (_activityView == nil)
_activityView = [ActivityView activityViewFromNibWithFrame:defaultFrame withProject:[BCLocalFetch projectForId:projectId]];
[self refresh];
}
I then use the refresh method to reposition them as the content sizes vary so I wanted to have a separate method.
-(void)refresh
{
//Position the milestones first
[_milestoneView setFrameOrigin:NSMakePoint(20, NSMaxY([[self view] bounds])-[_milestoneView frame].size.height-60)];
if ([[_milestoneView milestones] count] > 0)
[[self view] addSubview:_milestoneView];
//Now the activity view
[_activityView setFrameOrigin:NSMakePoint(20, [_milestoneView frame].origin.y-[_activityView frame].size.height-20)];
[[self view] addSubview:_activityView];
[self autosizeView];
}
-(void)autosizeView
{
//Resize the view to accommodate all subviews
NSRect oldFrame = [[self view] frame];
CGFloat lastY = [_activityView frame].origin.y;
if (lastY < 0) {
CGFloat newHeight = oldFrame.size.height + (-lastY);
[[self view] setFrameSize:NSMakeSize(oldFrame.size.width, newHeight)];
}
[[NSNotificationCenter defaultCenter] postNotificationName:#"BBContentDidResizeNotification" object:self];
}
Ok so I came back to the issue and finally got it worked out. The implementation was actually quite simple.
I added a property to PXListView to point to the NSScrollView that is to be scrolled.
I then implemented NSResponder's scrollWheel: method like this:
-(void)scrollWheel:(NSEvent *)theEvent
{
//Pass scrolling to the superview
[_scrollHandler scrollWheel:theEvent];
}
And all is well!

How programmatically move a UIScrollView to focus in a control above keyboard?

I have 6 UITextFields on my UIScrollView. Now, I can scroll by user request. But when the keyboard appear, some textfields are hidden.
That is not user-friendly.
How scroll programmatically the view so I get sure the keyboard not hide the textfield?
Here's what worked for me. Having an instance variable that holds the value of the UIScrollView's offset before the view is adjusted for the keyboard so you can restore the previous state after the UITextField returns:
//header
#interface TheViewController : UIViewController <UITextFieldDelegate> {
CGPoint svos;
}
//implementation
- (void)textFieldDidBeginEditing:(UITextField *)textField {
svos = scrollView.contentOffset;
CGPoint pt;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:scrollView];
pt = rc.origin;
pt.x = 0;
pt.y -= 60;
[scrollView setContentOffset:pt animated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[scrollView setContentOffset:svos animated:YES];
[textField resignFirstResponder];
return YES;
}
Finally, a simple fix:
UIScrollView* v = (UIScrollView*) self.view ;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:v];
rc.origin.x = 0 ;
rc.origin.y -= 60 ;
rc.size.height = 400;
[self.scroll scrollRectToVisible:rc animated:YES];
Now I think is only combine this with the link above and is set!
I've put together a universal, drop-in UIScrollView and UITableView subclass that takes care of moving all text fields within it out of the way of the keyboard.
When the keyboard is about to appear, the subclass will find the subview that's about to be edited, and adjust its frame and content offset to make sure that view is visible, with an animation to match the keyboard pop-up. When the keyboard disappears, it restores its prior size.
It should work with basically any setup, either a UITableView-based interface, or one consisting of views placed manually.
Here it is.
(For google: TPKeyboardAvoiding, TPKeyboardAvoidingScrollView, TPKeyboardAvoidingCollectionView.)
Editor's note: TPKeyboardAvoiding seems to be continually updated and fresh, as of 2014.
If you set the delegate of your text fields to a controller object in your program, you can have that object implement the textFieldDidBeginEditing: and textFieldShouldReturn: methods. The first method can then be used to scroll to your text field and the second method can be used to scroll back.
You can find code I have used for this in my blog: Sliding UITextViews around to avoid the keyboard. I didn't test this code for text views in a UIScrollView but it should work.
simple and best
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
// self.scrlViewUI.contentOffset = CGPointMake(0, textField.frame.origin.y);
[_scrlViewUI setContentOffset:CGPointMake(0,textField.center.y-90) animated:YES];
tes=YES;
[self viewDidLayoutSubviews];
}
The answers posted so far didn't work for me as I've a quite deep nested structure of UIViews. Also, the I had the problem that some of those answers were working only on certain device orientations.
Here's my solution, which will hopefully make you waste some less time on this.
My UIViewTextView derives from UIView, is a UITextView delegate and adds a UITextView after having read some parameters from an XML file for that UITextView (that XML part is left out here for clarity).
Here's the private interface definition:
#import "UIViewTextView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreGraphics/CGColor.h>
#interface UIViewTextView (/**/) {
#private
UITextView *tf;
/*
* Current content scroll view
* position and frame
*/
CGFloat currentScrollViewPosition;
CGFloat currentScrollViewHeight;
CGFloat kbHeight;
CGFloat kbTop;
/*
* contentScrollView is the UIScrollView
* that contains ourselves.
*/
UIScrollView contentScrollView;
}
#end
In the init method I have to register the event handlers:
#implementation UIViewTextView
- (id) initWithScrollView:(UIScrollView*)scrollView {
self = [super init];
if (self) {
contentScrollView = scrollView;
// ...
tf = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 241, 31)];
// ... configure tf and fetch data for it ...
tf.delegate = self;
// ...
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(keyboardWasShown:) name: UIKeyboardWillShowNotification object:nil];
[nc addObserver:self selector:#selector(keyboardWasHidden:) name: UIKeyboardWillHideNotification object:nil];
[self addSubview:tf];
}
return(self);
}
Once that's done, we need to handle the keyboard show event. This gets called before the textViewBeginEditing is called, so we can use it to find out some properties of the keyboard. In essence, we want to know the height of the keyboard. This, unfortunately, needs to be taken from its width property in landscape mode:
-(void)keyboardWasShown:(NSNotification*)aNotification {
NSDictionary* info = [aNotification userInfo];
CGRect kbRect = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGSize kbSize = kbRect.size;
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat sWidth = screenRect.size.width;
CGFloat sHeight = screenRect.size.height;
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if ((orientation == UIDeviceOrientationPortrait)
||(orientation == UIDeviceOrientationPortraitUpsideDown)) {
kbHeight = kbSize.height;
kbTop = sHeight - kbHeight;
} else {
//Note that the keyboard size is not oriented
//so use width property instead
kbHeight = kbSize.width;
kbTop = sWidth - kbHeight;
}
Next, we need to actually scroll around when we start editing. We do this here:
- (void) textViewDidBeginEditing:(UITextView *)textView {
/*
* Memorize the current scroll position
*/
currentScrollViewPosition = contentScrollView.contentOffset.y;
/*
* Memorize the current scroll view height
*/
currentScrollViewHeight = contentScrollView.frame.size.height;
// My top position
CGFloat myTop = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow.rootViewController.view].y;
// My height
CGFloat myHeight = self.frame.size.height;
// My bottom
CGFloat myBottom = myTop + myHeight;
// Eventual overlap
CGFloat overlap = myBottom - kbTop;
/*
* If there's no overlap, there's nothing to do.
*/
if (overlap < 0) {
return;
}
/*
* Calculate the new height
*/
CGRect crect = contentScrollView.frame;
CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight + overlap);
/*
* Set the new height
*/
[contentScrollView setFrame:nrect];
/*
* Set the new scroll position
*/
CGPoint npos;
npos.x = contentScrollView.contentOffset.x;
npos.y = contentScrollView.contentOffset.y + overlap;
[contentScrollView setContentOffset:npos animated:NO];
}
When we end editing, we do this to reset the scroll position:
- (void) textViewDidEndEditing:(UITextView *)textView {
/*
* Reset the scroll view position
*/
CGRect crect = contentScrollView.frame;
CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight);
[contentScrollView setFrame:nrect];
/*
* Reset the scroll view height
*/
CGPoint npos;
npos.x = contentScrollView.contentOffset.x;
npos.y = currentScrollViewPosition;
[contentScrollView setContentOffset:npos animated:YES];
[tf resignFirstResponder];
// ... do something with your data ...
}
There's nothing left to do in the keyboard was hidden event handler; we leave it in anyway:
-(void)keyboardWasHidden:(NSNotification*)aNotification {
}
And that's it.
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end
I know this is old, but still none of the solutions above had all the fancy positioning stuff required for that "perfect" bug-free, backwards compatible and flicker-free animation.
Let me share my solution (assuming you have set up UIKeyboardWill(Show|Hide)Notification):
// Called when UIKeyboardWillShowNotification is sent
- (void)keyboardWillShow:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardFrameInWindow;
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameInWindow];
// the keyboard frame is specified in window-level coordinates. this calculates the frame as if it were a subview of our view, making it a sibling of the scroll view
CGRect keyboardFrameInView = [self.view convertRect:keyboardFrameInWindow fromView:nil];
CGRect scrollViewKeyboardIntersection = CGRectIntersection(_scrollView.frame, keyboardFrameInView);
UIEdgeInsets newContentInsets = UIEdgeInsetsMake(0, 0, scrollViewKeyboardIntersection.size.height, 0);
// this is an old animation method, but the only one that retains compaitiblity between parameters (duration, curve) and the values contained in the userInfo-Dictionary.
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
_scrollView.contentInset = newContentInsets;
_scrollView.scrollIndicatorInsets = newContentInsets;
/*
* Depending on visual layout, _focusedControl should either be the input field (UITextField,..) or another element
* that should be visible, e.g. a purchase button below an amount text field
* it makes sense to set _focusedControl in delegates like -textFieldShouldBeginEditing: if you have multiple input fields
*/
if (_focusedControl) {
CGRect controlFrameInScrollView = [_scrollView convertRect:_focusedControl.bounds fromView:_focusedControl]; // if the control is a deep in the hierarchy below the scroll view, this will calculate the frame as if it were a direct subview
controlFrameInScrollView = CGRectInset(controlFrameInScrollView, 0, -10); // replace 10 with any nice visual offset between control and keyboard or control and top of the scroll view.
CGFloat controlVisualOffsetToTopOfScrollview = controlFrameInScrollView.origin.y - _scrollView.contentOffset.y;
CGFloat controlVisualBottom = controlVisualOffsetToTopOfScrollview + controlFrameInScrollView.size.height;
// this is the visible part of the scroll view that is not hidden by the keyboard
CGFloat scrollViewVisibleHeight = _scrollView.frame.size.height - scrollViewKeyboardIntersection.size.height;
if (controlVisualBottom > scrollViewVisibleHeight) { // check if the keyboard will hide the control in question
// scroll up until the control is in place
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y += (controlVisualBottom - scrollViewVisibleHeight);
// make sure we don't set an impossible offset caused by the "nice visual offset"
// if a control is at the bottom of the scroll view, it will end up just above the keyboard to eliminate scrolling inconsistencies
newContentOffset.y = MIN(newContentOffset.y, _scrollView.contentSize.height - scrollViewVisibleHeight);
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
} else if (controlFrameInScrollView.origin.y < _scrollView.contentOffset.y) {
// if the control is not fully visible, make it so (useful if the user taps on a partially visible input field
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y = controlFrameInScrollView.origin.y;
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
}
}
[UIView commitAnimations];
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillHide:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = notification.userInfo;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
// undo all that keyboardWillShow-magic
// the scroll view will adjust its contentOffset apropriately
_scrollView.contentInset = UIEdgeInsetsZero;
_scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
[UIView commitAnimations];
}
You may check it out: https://github.com/michaeltyson/TPKeyboardAvoiding (I used that sample for my apps). It is working so well. I hope that helps you.
Actually, here's a full tutorial on using TPKeyboardAvoiding, which may help someone
(1) download the zip file from the github link. add these four files to your Xcode project:
(2) build your beautiful form in IB. add a UIScrollView. sit the form items INSIDE the scroll view. (Note - extremely useful tip regarding interface builder: https://stackoverflow.com/a/16952902/294884)
(3) click on the scroll view. then at the top right, third button, you'll see the word "UIScrollView". using copy and paste, change it to "TPKeyboardAvoidingScrollView"
(4) that's it. put the app in the app store, and bill your client.
(Also, just click on the Inspector tab of the scroll view. You may prefer to turn on or off bouncing and the scroll bars - your preference.)
Personal comment - I strongly recommend using scroll view (or collection view) for input forms, in almost all cases. do not use a table view. it's problematic for many reasons. and quite simply, it's incredibly easier to use a scroll view. just lay it out any way you want. it is 100% wysiwyg in interface builder. hope it helps
This is my code, hope it will help you. It work ok in case you have many textfield
CGPoint contentOffset;
bool isScroll;
- (void)textFieldDidBeginEditing:(UITextField *)textField {
contentOffset = self.myScroll.contentOffset;
CGPoint newOffset;
newOffset.x = contentOffset.x;
newOffset.y = contentOffset.y;
//check push return in keyboar
if(!isScroll){
//180 is height of keyboar
newOffset.y += 180;
isScroll=YES;
}
[self.myScroll setContentOffset:newOffset animated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
//reset offset of content
isScroll = NO;
[self.myScroll setContentOffset:contentOffset animated:YES];
[textField endEditing:true];
return true;
}
we have a point contentOffset to save contentoffset of scrollview before keyboar show. Then we will scroll content for y about 180 (height of keyboar). when you touch return in keyboar, we will scroll content to old point(it is contentOffset). If you have many textfield, you don't touch return in keyboar but you touch another textfield, it will +180 . So we have check touch return
Use any of these,
CGPoint bottomOffset = CGPointMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height);
[self.MainScrollView setContentOffset:bottomOffset animated:YES];
or
[self.MainScrollView scrollRectToVisible:CGRectMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height-30, MainScrollView.frame.size.width, MainScrollView.frame.size.height) animated:YES];
I think it's better use keyboard notifications because you don't know if the first responder (the control with focus on) is a textField or a textView (or whatever). So juste create a category to find the first responder :
#import "UIResponder+FirstResponder.h"
static __weak id currentFirstResponder;
#implementation UIResponder (FirstResponder)
+(id)currentFirstResponder {
currentFirstResponder = nil;
[[UIApplication sharedApplication] sendAction:#selector(findFirstResponder:) to:nil from:nil forEvent:nil];
return currentFirstResponder;
}
-(void)findFirstResponder:(id)sender {
currentFirstResponder = self;
}
#end
then
-(void)keyboardWillShowNotification:(NSNotification*)aNotification{
contentScrollView.delegate=nil;
contentScrollView.scrollEnabled=NO;
contentScrollViewOriginalOffset = contentScrollView.contentOffset;
UIResponder *lc_firstResponder = [UIResponder currentFirstResponder];
if([lc_firstResponder isKindOfClass:[UIView class]]){
UIView *lc_view = (UIView *)lc_firstResponder;
CGRect lc_frame = [lc_view convertRect:lc_view.bounds toView:contentScrollView];
CGPoint lc_point = CGPointMake(0, lc_frame.origin.y-lc_frame.size.height);
[contentScrollView setContentOffset:lc_point animated:YES];
}
}
Eventually disable the scroll and set the delegate to nil then restore it to avoid some actions during the edition of the first responder. Like james_womack said, keep the original offset to restore it in a keyboardWillHideNotification method.
-(void)keyboardWillHideNotification:(NSNotification*)aNotification{
contentScrollView.delegate=self;
contentScrollView.scrollEnabled=YES;
[contentScrollView setContentOffset:contentScrollViewOriginalOffset animated:YES];
}
In Swift 1.2+ do something like this:
class YourViewController: UIViewController, UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
_yourTextField.delegate = self //make sure you have the delegate set to this view controller for each of your textFields so textFieldDidBeginEditing can be called for each one
...
}
func textFieldDidBeginEditing(textField: UITextField) {
var point = textField.convertPoint(textField.frame.origin, toView: _yourScrollView)
point.x = 0.0 //if your textField does not have an origin at 0 for x and you don't want your scrollView to shift left and right but rather just up and down
_yourScrollView.setContentOffset(point, animated: true)
}
func textFieldDidEndEditing(textField: UITextField) {
//Reset scrollview once done editing
scrollView.setContentOffset(CGPoint.zero, animated: true)
}
}