In my app there are few forms. Sometimes keyboard hides fields so user can't see what he it typing. For that case I found way to move view or scrollview up so textfields stays above keyboard.
Problem is that on iPhone 5 I need to move view up for last 3 textfields but for iPhone 6 - only for the last textfield.
Of corse I can define all cases of fields and device screen height values.
But I want to find more elegant solution to detect is texfield is under the keyboard on current device and is it necessary to move view?
Use TPKeyboardAvoidingScrollView. Its easy to use
drop the TPKeyboardAvoidingScrollView.m and TPKeyboardAvoidingScrollView.h source files into your project, pop a UIScrollView into your view controller's xib, set the scroll view's class to TPKeyboardAvoidingScrollView, and put all your controls within that scroll view. You can also create it programmatically, without using a xib - just use the TPKeyboardAvoidingScrollView as your top-level view.
There is a great help guide by Apple here
You need to listen to keyboard notifications like
// Call this method somewhere in your view controller setup code.
- (void)registerForKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
}
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.height;
if (!CGRectContainsPoint(aRect, activeField.frame.origin) ) {
[self.scrollView scrollRectToVisible:activeField.frame animated:YES];
}
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
}
You can detect which UITextField is the "Active" one by using its delegate method - (void)textFieldDidBeginEditing:(UITextField *)textField.
Use textField.frame to calculate the offset you need to set for scrollView.contentOffset.
In the - textFieldDidBeginEditing: method you can reset the contentOffset = CGPointZero
Related
I have an iOS app. It works great.
Except when the user has a hotspot on or is in a call but the call app is minimised
The extended height of the status bar pushes my ui down, making part of it disappear,
at the bottom.
I want this extended bar to overlay the top of the screen and not push the ui downwards.
How do I achieve that ?
The Simplest Solution is to make sure that your view's springs-and-struts or Autolayout properties allow for compression or expansion of the view , If you have some complex UI then you can implement UIApplicationWillChangeStatusBarFrameNotification observer.
You can handle the UIApplicationWillChangeStatusBarFrameNotification and UIApplicationDidChangeStatusBarOrientationNotification notifications which will tell you the new size of the status bar.
If you are intent on using a transform on your view to handle resizing, you can implement -viewWillLayoutSubviews in your view controllers (probably in a common base class) to set a transform on the root view of the view controller.
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(statusFrameChanged:)
name:UIApplicationWillChangeStatusBarFrameNotification
object:nil];
}
-(void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationWillChangeStatusBarFrameNotification
object:nil];
}
- (void)statusFrameChanged:(NSNotification*)note
{
CGRect statusBarFrame = [note.userInfo[UIApplicationStatusBarFrameUserInfoKey] CGRectValue];
CGFloat statusHeight = statusBarFrame.size.height;
UIScreen *screen = [UIScreen mainScreen];
CGRect viewRect = screen.bounds;
viewRect.size.height -= statusHeight;
viewRect.origin.y = statusHeight;
self.view.frame = viewRect;
[self.view setNeedsLayout];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
CGRect baseFrame = self.view.frame;
// 548.0 is the full height of the view. Update as necessary.
CGFloat scale = self.view.frame.size.height / 548.0;
[self.view setTransform:CGAffineTransformMakeScale(1.0, scale)];
self.view.frame = baseFrame;
}
I used use "Vertical space - Bottom layout Guide - Button". This way, a button I have on the bottom of the screen stays in the same place when there is an in call bar and if a different screen size is used (3.5inch or 4icnh).
I have some custom views with embedded table views in them. So I had to program the whole scrolling up etc when the keyboard appears myself. I've used the apple docs for that and came up with this:
- (void)viewDidLoad
{
[super viewDidLoad];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)keyboardDidShow:(NSNotification *)notification
{
// keyboard frame is in window coordinates
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
// convert own frame to window coordinates, frame is in superview's coordinates
CGRect ownFrame = [self.tableView.window convertRect:self.tableView.frame fromView:self.tableView.superview];
// calculate the area of own frame that is covered by keyboard
CGRect coveredFrame = CGRectIntersection(ownFrame, keyboardFrame);
// now this might be rotated, so convert it back
coveredFrame = [self.tableView.window convertRect:coveredFrame toView:self.tableView.superview];
// set inset to make up for covered array at bottom
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, coveredFrame.size.height, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;
}
- (void)keyboardWillHide:(NSNotification *)notification
{
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;
}
But now the not so nice thing about this (and another code I've tried, same results) is: When scrolling around the table and switching back to non-edit mode, some cells which where hidden before at the bottom of the table are immediately in the non-edit mode and not transitioning back from the indented state. Very rarely this results even in an error, that one cell remains in edit mode even though all cells have been switched back. This error disappears if I scroll the cell away and back and then it's in the correct format.
Any idea why? I've definitely narrowed it down to this code. When I comment everything out it works as it should (well of course without the scrolling when the keyboard appears :-)
[self.scrollView scrollRectToVisible:rect animated:YES];
Does anyone have a clue of why this works perfectly fine on iOS6.1 and on iOS7.0.4 always scrolls to the UITextField that has become firstResponder no matter what kind of rect I send as an argument?
CGRect rect = CGRectMake(0, self.scrollView.frame.size.height - 1, 320, 1);
[self.scrollView scrollRectToVisible:rect animated:YES];
This code will scroll the UIScrollView to its bottom when the keyboard is showed due to a UITextField inside the UIScrollView has become first responder on iOS6.1 but on iOS7.0.4 it is scrolled so that the UITextFiled is visible instead.
As I figure this, the UIScrollView in the iOS7 SDK no matter what, autoscrolls to whatever has become the first responder inside of it when scrollRectToVisible:animated: is called.
I suspect that most of you developers are using scrollRectToVisible:Animated: in conjunction with system keyboard notifications as explained in the Apple Docs here. For me the sample code provided by Apple didn't work (well, only half of it did).
Putting the method call inside a dispatch block fixed the problem for me:
dispatch_async(dispatch_get_main_queue(), ^{
[self.scrollView scrollRectToVisible:rect animated:YES];
});
I don't fully understand why this works and I'm not sure if this is 100% safe but on the other hand it feels a lot safer than just delaying the call by 0.1 seconds as suggested in another answer by Rikkles.
I'm not an expert on threading issues (yet) but it seems like whatever hidden system method is overriding the scrolling behavior is already on the main queue when the UIKeyboardDidShowNotification is sent. So if we put our method call on the main queue as well it will be executed afterwards and therefor yield the desired effect. (But that's only a guess.)
On iOS 8 (and possibly 7), the OS autoscrolls to the UITextField at the tail end of the runloop operation, just before it goes back to listening to user input.
I haven't found any way to get in after the OS autoscroll and before the user input. Neither UIKeyboardWillShowNotification nor UIKeyboardDidShowNotification are hooks that will work.
However, what will always work is the good old trick of performing a selector after delay. Simply put the scrolling code in its own method, and call that method like this:
- (void)keyboardDidShow:(NSNotification*)aNotification {
// ... all code to choose the view you want ...
[self performSelector:#selector(moveToView:) withObject:visibleView afterDelay:0.1];
}
- (void)moveToView:(UIView *)aView {
[self.scView scrollRectToVisible:aView.frame animated:YES];
}
And that will run after the OS autoscrolls, and you're golden.
I met this problem before. Not an easy one, but boring for sure.
It was because I set contentSize to 0 (because you don't want it to scroll). And you should set at least 1.
[scrollView setContentSize: CGSizeMake(1, self.view.frame.size.height)];
I hope it's the solution ;)
I found a solution to this problem, but it is not a pretty one. In order to scroll the scrollview to the desired location, u must register for both the keyboardWillShow and keyboardDidShow notifications. Then code to set the scrollview's insets is placed in the keyboardWillShowNotification's observer's selector and the code to scroll the scrollview to the desired location is placed in the keyboardDidShowNotification's observer's selector. Here is what I have:
Inside viewDidLoad:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
Notification Methods:
- (void) keyboardWillShow: (NSNotification*) aNotification;
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
float kbHeight = kbSize.height < kbSize.width ? kbSize.height : kbSize.width;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbHeight, 0.0);
_scrollView.contentInset = contentInsets;
_scrollView.scrollIndicatorInsets = contentInsets;
}
-(void)keyboardDidShow:(NSNotification*)notification
{
CGRect aRect = CGRectMake(0, 0, _scrollView.frame.size.width, _scrollView.frame.size.height - _scrollView.frame.origin.y - self.scrollView.contentInset.bottom);
CGRect scrollFrame = CGRectMake(self.loginView.frame.origin.x + self.loginButton.frame.origin.x, self.loginView.frame.origin.y + self.loginButton.frame.origin.y, self.loginButton.frame.size.width, self.loginButton.frame.size.height);
if (!CGRectContainsRect(aRect, scrollFrame)) {
[_scrollView scrollRectToVisible:scrollFrame animated:YES];
}
}
I was following the Apple docs but with no success. Then I tried calling setContentOffset(_:animated:) on my scrollView, instead of scrollRectToVisible(_:animated:), and that made it work.
The code below scrolls to myView if it is hidden under keyboard, supposing you call keyboardWillShow function when you receive a UIResponder.keyboardWillShowNotification.
Swift 5
#objc private func keyboardWillShow(_ notification: Notification) {
if let keyboardHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
let visibleViewFrame = myView.frame
var scrollViewFrame = scrollView.frame
scrollViewFrame.size.height -= keyboardHeight
if !scrollViewFrame.contains(visibleViewFrame) {
scrollView.setContentOffset(CGPoint(x: 0, y: visibleViewFrame.origin.y), animated: true)
}
}
}
I've just found an annoying bug with the new keyboardDismissMode property of the scroll view. When using this with a text view with the value UIScrollViewKeyboardDismissModeInteractive and the keyboard is dismissed the scroll view seems to jump up to the top before it continues to decelerate.
I've filed a bug report with Apple but need a workaround. I've tried the DAKeyboardControl without the new iOS7 support which behind the scenes is using the keyboardDismissMode and it still does it which to me indicates this is a much deeper problem.
Any suggestions?
for this issue better you code with scrollviewDelegete and simply mention when you want dismiss keyboard through ResignFirstResponder
Does seem to be a bug or just a non-ideal default state. But based on the code in the test project something like the below may work after some finer tuning.
There are two problems with the sample code, one is that you aren't doing anything about the size of the text when the keyboard does appear, so you can't use or see the text under the keyboard. There are other solutions but a quick and dirty solution is to change the frame size (in a submission app I would also grab the animation info and animate the view frame change to match the keyboard animation which is beyond the scope of this question). You do that in 'willShow' or the like, and bring it back in 'didHide' or the like.
Then, the content offset is fudged when its hidden and there does appear to be some strange states while you are dragging it offscreen before and around your callbacks for hiding and scroll view changes. I just save the state and "fix" it once the keyboard goes away and I've updated the text view.
I created a few properties and an outlet in the storyboard to fudge with the text view.
- (void) viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
}
- (void) keyboardWillShow:(NSNotification *)notification
{
NSDictionary * info = [notification userInfo];
CGSize size = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
CGRect rect = self.textView.frame;
rect.size.height -= size.height;
self.textView.frame = rect;
}
- (void)keyboardDidHide:(NSNotification *)notification
{
NSLog(#"====== keyboardDidHide =======");
NSDictionary * info = [notification userInfo];
CGSize size = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
CGRect rect = self.textView.frame;
rect.size.height += size.height;
self.textView.frame = rect;
self.hidingKeyboard = YES;
}
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"%f", scrollView.contentOffset.y);
if(self.hidingKeyboard == YES)
{
scrollView.contentOffset = self.lastOffset;
self.hidingKeyboard = NO;
NSLog(#"====== reset =======");
}
else
self.lastOffset = scrollView.contentOffset;
}
I have a view-based application, and in one of the subviews there is a UIScrollView. I have written handlers to adjust the size of the scroll view when the keyboard appears and disappears. I would like the keyboard to be dismissed when the user leaves the view, so I call [currentField resignFirstResponder] in viewWillDisappear. This dismisses the keyboard, but does not call the handler to resize the scroll view (when I call the same code in other places, it does). Any suggestions?
EDIT: These are the handlers that I use:
-(void) keyboardWasShown:(NSNotification*) notification
{
if(keyboardShown)
return;
NSDictionary* info=[notification userInfo];
NSValue* value=[info objectForKey:UIKeyboardFrameEndUserInfoKey];
CGSize keyboardSize=[value CGRectValue].size;
CGRect viewFrame=[scrollView frame];
viewFrame.size.height-=keyboardSize.height;
scrollView.frame=viewFrame;
keyboardShown=YES;
}
-(void) keyboardWasHidden:(NSNotification*) notification
{
NSDictionary* info=[notification userInfo];
NSValue* value=[info objectForKey:UIKeyboardFrameEndUserInfoKey];
CGSize keyboardSize=[value CGRectValue].size;
CGRect viewFrame=[scrollView frame];
viewFrame.size.height+=keyboardSize.height;
scrollView.frame=viewFrame;
keyboardShown=NO;
}
When I call [currentField resignFirstResponder] anywhere else, it calls the handler without problems.
So you were being removed as observer before UIKeyboardDidHideNotification was posted, glad I could help. But observing the UIKeyboardWillHideNotification and UIKeyboardWillShowNotification is probably enough for your reaction to the keyboard. The keyboard notifications have a user info key UIKeyboardAnimationDurationUserInfoKey which you can use to animate your frame adjustments with the keyboard animations. This avoids the 'clunk' feeling your views will have if you don't animate them to new positions. Here is a quick example of what you can do:
-(void)keyboardWillNotificationTarget:(NSNotification *)note{
// Find current keyboard origin Y
NSValue *keyboardCurrentFrameValue = [note.userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey];
CGFloat currentOriginY = keyboardCurrentFrameValue.CGRectValue.origin.y;
// Find keyboard Y that will be
NSValue *keyboardNewFrameValue = [note.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey];
CGFloat newOriginY = keyboardNewFrameValue.CGRectValue.origin.y;
// Calculate new frame for scrollView
CGFloat heightChangeForScrollView = newOriginY - currentOriginY;
CGRect svFrame = scrollView.frame;
svFrame.size.height += heightChangeForScrollView;
// Find duration of animation
NSNumber *animationDurationNumber = [note.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
CGFloat animationDuration = animationDurationNumber.floatValue;
// Animate scrollView with keyboard
[UIView animateWithDuration:animationDuration animations:^{
scrollView.frame = svFrame;
}];
}
Now you simply add this method as the target for both notifications:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillNotificationTarget:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillNotificationTarget:) name:UIKeyboardWillHideNotification object:nil];