Why does UIScrollView scroll violently when I quickly swipe twice in the same direction? - objective-c

when scrolling horizontally in a UIScrollview, if I quickly swipe twice in the same direction the scroll view jumps violently. Is there anyway to prevent this from happening? To explain in detail, here's an event log from the scrollview where in most delegate methods I just print the x coordinate.
scrollViewWillBeginDragging:
14:55:12.034 Will begin dragging!
14:55:12.037 - Position -0.000000
scrollViewWillBeginDeceleration:
14:55:12.129 Deceleration rate 0.998000
14:55:12.152 + Position 314.000000
scrollViewWillBeginDragging:
14:55:12.500 Will begin dragging!
14:55:12.522 - Position 1211.000000
scrollViewWillBeginDeceleration:
14:55:12.530 Deceleration rate 0.998000
14:55:12.533 + Position 1389.000000
scrollViewDidScroll: (printing values < 0 && > 6000 (bounds.size.width)
14:55:12.595 !!! Position 7819.000000
14:55:12.628 !!! Position 9643.000000
14:55:12.658 !!! Position 10213.000000
14:55:12.688 !!! Position 10121.000000
14:55:12.716 !!! Position 9930.000000
... contentoffset.x drops with around 400 each scrollviewdidscroll call ...
14:55:13.049 !!! Position 6508.000000
scrollViewDidEndDecelerating:
14:55:13.753 Will end deceleration
14:55:13.761 * Position 6144.000000
The most notable thing in the log is right after scrollViewWillBeginDeceleration when the contentoffset.x jumps with ~6000 points in a matter of milliseconds.
Implementation
The uiscrollview and uiscrollviewdelegate are in the same class, a subclass of uiscrollview which also implements the uiscrollviewdelegate protocol, nothing special is done to contentoffset, and the only properties set on the scrollview are:
self.showsHorizontalScrollIndicator = YES;
self.scrollsToTop = NO;
self.delegate = self;
The scrollview subviews are added once from a viewwillappear call in a uiviewcontroller which hosts the uiscrollview (and the contentSize is set appropriately). Scrolling, waiting a little while, and scrolling again works perfectly.

Please set the scroll view in your required direction and as well as co-ordinate for which you want the scroll and use the proper connection to XIB then it will work fine.

you can try it like this
- (void)enableScrollView
{
scrollView.userInteractionEnabled = YES;
}
- (void)delayScroll:(id)sender
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//delay userInteraction 0.1 second, prevent second swipe
[NSThread sleepForTimeInterval:0.1];
[self performSelectorOnMainThread:#selector(enableScrollView) withObject:nil waitUntilDone:NO];
[pool release];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = NO;
[NSThread detachNewThreadSelector:#selector(delayScroll:) toTarget:self withObject:nil];
}

May be you need to setup the bouncesZoom property?
scrollviewobject.bouncesZoom = NO;

If you want the UIScrollView to scroll a bit slower you could for example add a NSTimer when user scroll's and set the scrollers contentOffset a bit back with an animation like so:
Declare in .h File:
UIScrollView *myScrollView;
NSTimer *scrollCheckTimer;
BOOL delayScroll;
.m File:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (delayScroll == YES) {
[scrollCheckTimer invalidate];
delayScroll = NO;
[myScrollView setContentOffset:CGPointMake(myScrollView.contentOffset.x - 40, contentOffset.y) animated:YES];
}
delayScroll = YES;
scrollCheckTimer = [NSTimer scheduledTimerWithTimeInterval:0.9 target:self selector:#selector(removeDelayBecouseOfTime) userInfo:nil repeats:NO];
}
- (void)removeDelayBecouseOfTime { delayScroll = NO; }

Related

Objective C: Drawing with Fingers on UIScrollView

I am trying to make a sketch pad app.
I used UIScrollView for paging and UIImageView for the drawing.
I put the UIImageView on top of the scrollView but it's not added to UIScrollView so it will not scroll.
The issue now...
It's not writing when...
[scrollView setScrollEnable:YES];
[scrollView setUserInteractionEnabled:YES];
i need to set it to NO with the use of a button for it to write,
is there a way that i can scroll and write at the same time without using any button??
This is definitely not the correct way of building such an application.
A UIScrollView is meant for scrolling content not for drawing. And you don't need a UIImageView to draw content either, a simple UIView would be enough.
Here you're best bet would be to create one UIScrollView and disable it's scrolling because you'll be handling it with two fingers, while the drawing will be handled pan another gesture recognizer.
UIPanGestureRecognizer *twoFingerScrolling = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(onTwoFingerScroll:)] autorelease];
[twoFingerScrolling setMinimumNumberOfTouches:2];
[twoFingerScrolling setMaximumNumberOfTouches:2];
UIPanGestureRecognizer *oneFingerDraw = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(onOneFingerDraw:)] autorelease];
[oneFingerDraw setMinimumNumberOfTouches:1];
[oneFingerDraw setMaximumNumberOfTouches:1];
[yourScollView addGestureRecognizer:twoFingerScrolling];
[yourScollView addGestureRecognizer:oneFingerDraw];
And later on in your code you can easily process both events, the scrolling:
- (void)onTwoFingerScroll:(UIPanGestureRecognizer*)sender
{
// Calculate the content offset from the shifting that occured
//[yourScrollView setContentOffset:theContentOffset]
}
And the drawing (which can be done by the Quartz Tookit)
- (void)onOneFingerDraw:(UIPanGestureRecognizer*)sender
{
// Processing the drawing by using comparing:
if (sender.state == UIGestureRecognizerStateBegan)
{ /* drawing began */ }
else if (iRecognizer.state == UIGestureRecognizerStateChanged)
{ /* drawing occured */ }
else if (iRecognizer.state == UIGestureRecognizerStateEnded)
{ /* drawing ended /* }
}
Hope this helps.

Prevent uiscrollview from scrolling further then certain point [duplicate]

This question already has answers here:
Limiting the scrollable area in UIScrollView
(4 answers)
Closed 9 years ago.
I have a UIScrollView which has subviews that create a content with a width of about 7000.
Now I want the UIScrollView to not scroll any further then 2000.
I tried:
[scrollView setContentSize:CGSizeMake(768.0, 2000.0)];
[scrollView setClipsToBounds:YES];
But I can still scroll to the end, how do I prevent this?
It's seems that webView is changing its scrollView contentSize after page loading.
In your init set self as a delegate of WebView:
[webView setDelegate: self];
And add this method:
- (void)webViewDidFinishLoad: (UIWebView *)webView {
[webView.scrollView setContentSize: CGSizeMake(768.0, 2000.0)];
}
In my test case it's working fine.
UPDATED:
Calling javascript methods sometimes does not trigger webViewDidFinishLoad. So the most universal technique will be Key-Value Observing.
In init:
[webView.scrollView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionNew context:nil];
And somewhere this method:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
UIScrollView * scrlView = (UIScrollView *) object;
if ([keyPath isEqual:#"contentSize"]) {
NSLog(#"New contentSize: %f x %f",scrlView.contentSize.width,scrlView.contentSize.height);
if(scrlView.contentSize.width!=768.0||scrlView.contentSize.height!=2000.0)
[scrlView setContentSize:CGSizeMake(768.0, 2000.0)];
}
}
Do not forget to remove observer in case of dealloc:
[webView.scrollView removeObserver:self forKeyPath:#"contentSize"];
You can set content offset to 2000 if it is more than 2000 so it won't go beyond the bound.
Implement this method and set it to the delegate of that scrollview
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.x > 2000) {
CGPoint offset = scrollView.contentOffset;
offset.x = 2000;
[scrollView setContentOffset:offset animated:YES];
}
}
Edit: I just tried this, and it works fine. Maybe check why setContentSize: is not working?
Another approach, make a view with heigh of 2000, put your content view inside it, and add that view as subview of scrollview.
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 600, 7000)];
label.numberOfLines = 60;
label.font = [UIFont systemFontOfSize:100];
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
NSMutableString *str = [[NSMutableString alloc] initWithCapacity:300];
for (int i = 0; i < 60; i++)
[str appendFormat:#"%d\n", i];
label.text = str;
[scrollView addSubview:label];
[scrollView setContentSize:CGSizeMake(600, 2000)];
[self.view addSubview:scrollView];
I believe your code above should work fine. setContentSize will prevent the contentOffset from getting larger. You may have to start, though, by setting contentOffset yourself (once) if the user has already scrolled outside that area. (Try resetting it to (0, 0), for example.)
Also, are you permitting zooming? If you have a zoom != 1, the content size may not be what you think it is. Try setting zoom to 1 before modifying the content size.
Ok
So first step, your view controller must implement UISCrollViewDelegate,
and you must set your controller as a delegate for your UIScrollView.
Then, implement this delegate method in your controller:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
static float stopx = 2000.0f;
if (scrollView.contentOffset.x >= stopx) {
CGPoint stop = scrollView.contentOffset;
stop.x = stopx;
[scrollView setContentOffset:stop animated:NO];
}
}
Anytime you get pass 2000 / stops value, the scrollview should be brought back
As others have said, implement the uiscrollviewdelegate
implement these methods:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
and hopefully that will fix the jumping issue you were having
note, put in these methods similar code to the other posts like
static float stopx = 2000.0f;
if (scrollView.contentOffset.x >= stopx) {
CGPoint stop = scrollView.contentOffset;
stop.x = stopx;
[scrollView setContentOffset:stop animated:NO];
}
At first performance will possibly be compromised, if this is true, begin taking out certain delegate methods until you get the desired behavior
Unless I'm misunderstanding something in your question this is straightforward. Add this to your view controller containing the web view.
- (void)viewDidLoad
{
// other stuff
self.webView.delegate = self;
self.webView.scrollView.bounces = YES;
// load your webView here
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
CGSize contentSize = self.webView.scrollView.contentSize;
CGFloat maxContentWidth = contentSize.width;
if (maxContentWidth > 2000) {
maxContentWidth = 2000;
}
self.webView.scrollView.contentSize = CGSizeMake(maxContentWidth, contentSize.height);
}
Hard to give specific answer without seeing your html but I've had a similar problem before and i think its actually your UIWebview thats actually scrolling and not the scrollview. somewhere on the page in the UIWebview html you will most probably have oversize content. THe webview naturally wants to scroll. In my case it was a long URL in a paragraph of text. I have also seen this happening vertically.
You need to find the container element to your oversize html and set the overflow to hidden in the css. It may be that this is the body tag or the html tag in the html or you need to wrap your content.
ie;
<div style="overflow:hidden">
Overflowing text
</div>
Failing that you may have to play with the following tag
Or set the container div to width:100% & height:100%;

Making fading effect with NSView

I have a subview which I want to show up to make a dissolve fade effect. For that purpose, I'm trying to:
Show it
Fade it
For that purpose, I set a "showFade" method on my superview (Fader is the subview defined previously. someImage is already defined).
- (void)showFade {
[Fader setHidden:NO];
[Fader setImage: someImage];
[[Fader animator] setHidden:YES];
}
The problem is: it works for the first time, and the subview fades, but it never shows up again. What am I doing wrong?
EDIT: As requested, here is a more complete sample of the code
- (void)awakeFromNib {
Fader = [[NSImageView alloc] initWithFrame: [self bounds]];
Images[0] = #"Tower";
Images[1] = #"Desert";
Images[2] = #"Fall";
image = 0;
[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:#selector(showFader:) userInfo:nil repeats:YES];
[self addSubview: Fader];
[self nextImage];
[super awakeFromNib];
}
- (void)showFader: (NSTimer*)timer {
[Fader setImage: [self image]];
[Fader setHidden:NO];
[[Fader animator] setHidden:YES];
[self nextImage];
}
- (void)nextImage {
if (image == 2) {
image = 0;
}
else {
image++;
}
[self setImage: [NSImage imageNamed: Images[image]]];
}
So, basically, I have a repeating timer making the parent NSImageView to loop between an array of images and making the "Fader" to show up with the previous image and fade out. The problem is that it only shows up once.
I had the same problem a long time ago (yet I never solved it), but I think this could have something to do with the fact that you ask your view to be showed and dissolved in the same event loop.
I guess you should let the event loop make an iteration (so that your view is actually displayed) before asking to dissolve the view.
I guess you could do it by setting a (short) timer that would fire a couple of millisecs after the view is displayed (by setHidden:NO).
I also guess there are easier ways to solve the problem (and I would be glad to hear them).
What is more, you could also try animator setAlphaValue:0.0 to have a smooth transition.
I am not definitely not sure I have a correct answer, that's only a suggestion...
hidden is a BOOL property, you can't animate BOOLs cause there are only two discrete values: NO and YES. Use the opacity instead, you can animate opacity from 1.0 (fully visible) to 0.0 (invisible) to fade out. And hidden has to be set to NO for this to work, otherwise it will be hidden all the time.
This method works perfectly for me to make an image start out blurry and smoothly come into focus.
let parentView = UIView()
let imageView = UIImageView(image: UIImage(named: "ImageToFocus"))
parentView.addSubview(imageView)
let blurEffect = UIBlurEffect(style: .Light)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = imageView.frame
blurView.alpha = 1.0
self.blurTimer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: Selector("animateBlur:"), userInfo: blurView, repeats: true)
func animateBlur(timer:NSTimer)
{
let blurView = timer.userInfo as UIVisualEffectView
if blurView.alpha > 0.0 {
blurView.alpha -= 0.05
} else {
blurTimer = nil
}
}

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)
}
}