UITextView disappears behind keyboard after typing a while - ios

We're trying to support iOS 9 for a while yet... That's the only place this is happening...
I have a UITextView that occupies the entire view, which occupies the entire screen. I register to receive UIKeyboardWillShowNotification and UIKeyboardWillHideNotification. I get those just fine and adjust the height of my UITextView so that it is above the keyboard:
- (void) keyboardWillShow:(NSNotification *)aNotification
{
CGRect keyboardRect = [[[aNotification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect bounds = self.view.bounds;
bounds.size.height = bounds.size.height - keyboardRect.size.height;
self.textView.frame = bounds;
}
Again, this works great. Now I type a few lines. For a while, the bottom line of text is kept above the keyboard automatically as I add more lines of text. But eventually, usually within just a few lines (maybe 4-5), the next line will go under the keyboard and I can continue typing lines under the keyboard. Eventually the lines I can still see above the keyboard will start scrolling up as I hit the bottom of the screen with the lines I'm typing behind the keyboard.
I am not getting the UIKeyboardWillHideNotification during this process, but it is almost behaving like I am -- that is, the size of my UITextView appears to return to its original size before the keyboard was shown.
All of this works fine in iOS 11. That is, as I type, the text scrolls up so the bottom line of text never goes behind the keyboard. It's just iOS 9. Anybody remember a known issue back in those days, and a work-around?

My best guess is that you're mixing Autolayout w/ Autoresizing and it seems to be picking the intended one in later iOS's but not in iOS 9.
A quick sample using your code + auto layout constraints in the storyboard resulted in what I believe to be the behavior you're seeing in older iOS's (iOS 9.3 Simulator shown below):
If I roll with a pure auto layout approach (i.e. IBOutlet the textView's bottom constraint and adjust the constant for that programmatically in keyboardWillShow) then it results in the correct behavior on iOS 9 (no setFrame call):
#property (strong, nullable) IBOutlet NSLayoutConstraint *textViewBottomConstraint;
...
- (void)keyboardWillShow:(NSNotification *)aNotification {
CGRect keyboardRect = [[[aNotification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
self.textViewBottomConstraint.constant = -keyboardRect.size.height;
}

Related

UIScrollView xcode 8 swift 3

This is my first question and I am brand new to coding.
I'm creating an app and one of the pages has a form for a user to fill out with two labels, two textfields, a switch and a "send" button.
For smaller screen sizes, some elements were disappearing, so I used a stackview, but when the keyboard comes up, it blocks the textfield which will be annoying to the user, I'm sure.
I've seen a few tutorials about adding scrollviews, but setting the height so everything fits nicely for smallest devices leaves an uncomfortably large blank area at the bottom of larger screens.
Is there a way to enable the scroll view only when it is needed? Is this what "dynamic" is for?
If your issue is with UI formatting on different sized screens, auto layout is a built in feature in xcode that addresses this. By adding constraints to your views, autolayout will do its best to resize your UI elements so that what you see in storyboard is what you get across all devices. Using this technique you may be able to avoid a scrollview altogether. Try selecting all your views, then go to Editor -> Resolve Auto Layout Issues -> Reset to Suggested Constraints, and see if that works.
Seems that you have two questions. First one is "For smaller screen sizes, some elements were disappearing". That one is answered by #Dalton Sweeney. Use Auto Layout then Reset to Suggested Constraints
I am going to answer your second question, what you are supposed to do when the keyboard covers some UIElemnts.
You don't need to change view controller, you should dynamically change view's frame when the keyboard hides/appears. I made a demo in GitHub, check the full version if you want:https://github.com/EricZhang90/IOS_Demo/tree/master/KeyboadDemo
The main workflow is following:
1) create two notifications to detect the keyboard is about to hide/appear:
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *ctr = [NSNotificationCenter defaultCenter];
[ctr addObserver:self
selector:#selector(moveKeyboardInResponseToWillShowNotification:)
name:UIKeyboardWillShowNotification
object:nil];
[ctr addObserver:self
selector:#selector(moveKeyboardInResponseToWillHideNotification:)
name:UIKeyboardWillHideNotification
object:nil];
}
2) adjust view's frame
The method takes two parameters: the first is keyboard information sent from notification center. You can get it from the method in first step. The second parameter indicates this adjustment is about to reduce or expand frame. Its value is 0 when the keyboard is about to hide, and is frame of keyboard when the keyboard is about to appear.
-(void)adjustView: (NSDictionary *)info :(CGRect)rect{
// get duration of keyboard appears/hides
CGFloat duration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue];
// get keyboard's curve
UIViewAnimationCurve curve = [[info objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
[self.view layoutSubviews];
// Start to adjust frame:
// Animate:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve:curve];
[UIView setAnimationBeginsFromCurrentState:YES];
// self.buttonLayoutConstraint is the layout constraint between bottom of view and bottom of screen(super view).
// This is the most important part.
self.buttonLayoutConstraint.constant = rect.size.height;
[self.view layoutSubviews];
[UIView commitAnimations];
}
This is what ended up working for me in terms of moving the scrollview up and out of the way when the textView was selected and activated the keyboard.
func textViewDidBeginEditing(_ textView: UITextView) {
scrollView.setContentOffset(CGPoint(x: 0, y: 50), animated: true)
}
**The 50 value was just what worked for my layout...that number may be different for you.
I also put in:
func textViewDidEndEditing(_ textView: UITextView) {
scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
...so that the scrollview returned to normal once the user is done editing.

iOS 8 AutoLayout changing the position of a view

I'm working on an app that I had originally made for iOS 7 which worked perfectly fine, but now after moving to iOS 8, things aren't working properly anymore. Basically, I have a view with a button. When I tap that button, another UIView with a UITextField "slides" up onto the screen from a position below the visible screen area. The problem comes when I tap the UITextField to type into it, where a keyboard comes up, but the view with the UITextField goes back to its initial position off the screen. This was not happening in iOS 7, so I'm assuming this has something to do with the changes to autolayout for iOS 8. Does anyone know what I can possibly do to fix this? I would rather not turn off autolayout if I don't have to, as a good portion of my app depends on it.
Here is some code to show you what I'm referring to. In my example, "locationField" is the button that gets tapped, and "locationView" is the UIView that slides onto the screen:
- (IBAction) locationFieldTapped:(UIButton *)sender {
[self slideViewUp:_locationView];
}
- (void) slideViewUp:(UIView *)slidingView {
if (viewIsSlidUp) {
return;
}
CGRect frame = slidingView.frame;
int availableHeight = self.view.frame.size.height;
// don't move view up if it's already moved up or in the process of moving up
if (frame.origin.y < availableHeight) {
return;
}
viewIsSlidUp = YES;
// move view just off screen for animation start, in case it's not already there
frame.origin.y = availableHeight;
slidingView.frame = frame;
[UIView animateWithDuration:0.5f animations:^{
CGRect newFrame = frame;
newFrame.origin.y = availableHeight - CGRectGetHeight(frame);
slidingView.frame = newFrame;
}];
}
You need to set the constraints of the view you're animating in iOS 8 and the autolayout system then deals with the rest. You can create an IBOutlet to an NSLayoutConstraint and simply change its constant property. Then you call the layoutIfNeeded method on the edited view's super view in the animation block.

How to Use Autolayout to Animate Resizing a UITextView's Bottom Constraint When Content Has Been Scrolled

Background
I'm working on a quick and dirty notes app purely to try to understand autolayout. As such I am looking for an autolayout-specific solution to this problem.
I am quite sure that my terminology and understanding of this subject may be incorrect in places so if I misphrase or omit information through ignorance that would otherwise be helpful I am very happy to update this question with better specifics.
Short Problem Summary
This app is a simple note app. On the detail view of the note, there are two text input views, a UITextField, and a UITextView.
The goal is to use autolayout to animate a change of height to the UITextView when it is being edited (making room for the keyboard), and then animate the UITextView back to it's original size when editing is finished.
The animation code I have in place works, however when the UITextView is scrolled near to the bottom of the text the animation from "editing" size to "non-editing" size displays incorrectly durring the animation. (The final result of the animation, however is correct.)
I'm open to alternate "correct" ways of doing this if there's a common pattern for the solution. I am, however, looking for an autolayout solution which, I believe, means avoiding modifying the view's frame directly. (Could be wrong on that.)
Details and Code
A short video of the problem is available here:
http://pile.cliffpruitt.com/m/constraint_problem.mp4
This is the code performing the animation:
// self.bodyFieldConstraintBottom refers to an outlet referencing the UITextView's bottom constraint
// This animation occurrs when the text view is tapped
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
[self enterEditingMode];
[UIView animateWithDuration:2.35
animations:^{
NSLayoutConstraint *bottom_constraint = self.bodyFieldConstraintBottom;
bottom_constraint.constant = 216;
[self.view layoutIfNeeded];
}];
return YES;
}
// This animation occurrs when editing ends and the text field size is restored
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
[self exitEditingMode];
[UIView animateWithDuration:2.35
animations:^{
NSLayoutConstraint *bottom_constraint = self.bodyFieldConstraintBottom;
bottom_constraint.constant = 20;
[self.view layoutIfNeeded];
}];
return YES;
}
Full project source (in all it's messy glory) can be downloaded here:
http://pile.cliffpruitt.com/dl/LittleNotebooks.zip
Additional Comments
My understanding of cocoa terminology isn't the best so I'm having a hard time making google searches and docs searches effective. My best guess about the problem (based on observing the animation at a slow speed) is that it is related to a scroll offset somehow because unless the text is scrolled past a certain point, the problem does not manifest itself.
I have read quite a few SO question/answers including:
Resizing an UITextView when the keyboard pops up with auto layout
How to resize UITextView on iOS when a keyboard appears?
UIScrollView animation of height and contentOffset "jumps" content from bottom
The problem is that these answers either do not work ([self.bodyField setContentInset:UIEdgeInsetsMake(0, 0, 216, 0)]; seems to have no effect) or appear to rely on setting the frame of the UIText view which I believe is not supposed to be done when using autolayout.
Final Side Note
I've been at this off and on for about 4 days so my understanding and recollection of all I've read and tried is actually a little less clear than when I'd started. I hope I'm explaining this well enough to convey the issue.
EDIT:
I've noticed that this code actually gets somewhat close to the desired result:
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
[self enterEditingMode];
[UIView animateWithDuration:2.35
animations:^{
[self.bodyField setContentInset:UIEdgeInsetsMake(0, 0, 216, 0)];
[self.view layoutIfNeeded];
}];
return YES;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
[self exitEditingMode];
[UIView animateWithDuration:2.35
animations:^{
[self.bodyField setContentInset:UIEdgeInsetsMake(0, 0, 0, 0)];
[self.view layoutIfNeeded];
}];
return YES;
}
The problem with this version is that the scroll indicator scrolls down past the visible area of the text content, meaning it gets "lost" behind the keybaord. Also, it does not help me understand the correct way to animate a UITextView (UIScrollView ?) bottom constraint.
The issue looks weird and I am really not sure whats the main issue but I found out that for the best results you should call [self.view setNeedsUpdateConstraints]; before animating view.
My example code to animate view when keyboard appears:
-(void)keyboardWillShow:(NSNotification *)notification {
CGSize kbSize = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
//BH: iOS7 is messed up
CGFloat keyboardHeight = kbSize.width;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0")) {
keyboardHeight = kbSize.height;
}
self.centerYConstraint.constant = keyboardHeight;
[self.view setNeedsUpdateConstraints];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]];
[UIView setAnimationBeginsFromCurrentState:YES];
[self.view layoutIfNeeded];
[UIView commitAnimations];
}
I am using commit animations to animate view with same animationCurve as iOS is animating keyboard so view is moving 1 to 1 accordingly to keyboard. Also, please notice if statement for iOS8 vs iOS7 where Apple finally fixed window sizing.

Bounds automatically changes on UIScrollView with content insets

I'm using a UIScrollView as my paging scroll view, pagesScrollView. Inside that, I put individual UIScrollViews which are used exclusively for zooming. Inside each of those, I have one view which is the page item which should be zoomable. All of that is inside a UINavigationController with a translucent navbar.
My pagesScrollView has contentInset.top = 64 and bounds.origin.y = -64 (that seems weird to me, but that's what the system is setting automatically for me), and this works just fine. My screen looks great!
However, after I scroll the pagesScrollView even a tiny bit, as soon as scrollViewWillEndDragging is called, the pagesScrollView begins an animated change from bounds.origin.y = -64 to bounds.origin.y = 0 which causes my page items to be obscured by the navbar.
On the left is what it looks like when it loads, on the right is what it looks like after I drag just a few pixels and then let go, it slides up under the navbar (because the bounds.origin.y goes to 0).
The problem is that I don't have any code that is altering the bounds and I don't have any code in the various scroll delegate methods that do anything. I've added a bunch of scroll delegate methods and just added NSLog()s so I can figure out when/where the change is happening, but it's not happening anywhere in my code.
So, I don't know what code I can show you to help you help me.
EDIT: I built a new project from scratch to remove all other variables.. I put a bare UIViewController into a UINavigationController. I put a UIScrollView into my View the entire size of the view. The following code is the entire project.
It turns out the issue (described below) only appears once PAGING IS ENABLED on the UIScrollView! Wtf? :)
Here is a link to download a basic project with only a few lines of code which demonstrates the problem. Just click in the scrollview and you'll see it shift up as the bounds change. http://inadaydevelopment.com/stackoverflow/WeirdScrollViews.zip
How can I have paging enabled on my scrollview without the bounds freaking out during scrolling and shifting everything under the nav bar?
It's possible to set the navbar to opaque and the problem is avoided, but the ideal is to have standard iOS7 behavior so that after the content view is zoomed, THEN the content is allowed to be under the navbar and should show through the translucency normally.
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSArray *colors = #[
[UIColor blueColor],
[UIColor orangeColor],
[UIColor magentaColor],
];
NSArray *zoomerColors = #[
[UIColor greenColor],
[UIColor yellowColor],
[UIColor purpleColor],
];
self.scroller.pagingEnabled = YES;
[self.scroller setContentSize:CGSizeMake(self.scroller.frame.size.width*colors.count, self.scroller.frame.size.height)];
CGRect subviewFrame = CGRectMake(0, 0, 160, 240);
for (int index=0; index < colors.count; index++) {
UIColor *color = [colors objectAtIndex:index];
UIColor *zoomerColor = [zoomerColors objectAtIndex:index];
UIView *subview = [[UIView alloc] initWithFrame:subviewFrame];
subview.backgroundColor = color;
CGRect zoomerFrame = CGRectMake(index*self.scroller.frame.size.width, 0, self.scroller.frame.size.width, self.scroller.frame.size.height);
UIScrollView *zoomer = [[UIScrollView alloc] initWithFrame:zoomerFrame];
[zoomer addSubview:subview];
zoomer.backgroundColor = zoomerColor;
[self.scroller addSubview:zoomer];
}
}
Just switch off Adjust Scroll View Insets
It's an iOS bug. I created the following subclass of UIScrollView to get a log of what happens to y over time and who was pushing it:
#implementation CSScrollView
- (void)setContentOffset:(CGPoint)contentOffset
{
NSLog(#"%0.0f %#", contentOffset.y, [NSThread callStackSymbols]);
NSLog(#"[%#]", self.layer.animationKeys);
[super setContentOffset:contentOffset];
}
#end
(and changed the view class in the storyboard)
When you release your finger, a method called UIScrollView _smoothScrollDisplayLink: starts animating the scroll view to its final position. As per the second log, there's no CAAnimation involved, the scroll view uses its own display link to do its own transition. That custom code appears to make the mistake of animating from y = whatever to y = 0, failing to take the content offset into account.
As a proof-of-concept hack I changed the code to:
#implementation CSScrollView
- (void)setContentOffset:(CGPoint)contentOffset
{
contentOffset.y = -64.0f;
[super setContentOffset:contentOffset];
}
#end
And, unsurprisingly, the problem went away.
You probably don't want to hard code the -64.0f but I'd conclude:
it's an iOS bug;
work around it by rejecting nonsensical values via a subclass of UIScrollView with a suitable custom implementation of - setContentOffset:.
A sensible generic means might be to check the state of self.panGestureRecognizer — that'll allow you to differentiate between scrolls the user is responsible for and other scrolls without relying on any undocumented API or complicated capturing of delegate events. Then if necessary crib the correct contentOffset.y from the current value rather than hardcoding it.
My pagesScrollView has contentInset.top = 64 and bounds.origin.y = -64 (that seems weird to me, but that's what the system is setting automatically for me), and this works just fine. My screen looks great!
It because of iOS 7 sets contentInset.top to 64 on all scrollviews.
Just add this line of code into your view controller and all will work as expected:
-(UIRectEdge)edgesForExtendedLayout {
return UIRectEdgeNone;
}
I checked on your example project.
I have checked you example use below code in viewController.m file
-(void)viewDidLoad
{
if ([[UIDevice currentDevice] systemVersion].floatValue>=7.0) {
self.edgesForExtendedLayout = UIRectEdgeNone;
}
}
It's working fine...
It turns out the issue (described below) only appears once PAGING IS ENABLED on the UIScrollView! Wtf? :)
As you said that, If you enable the scroll paging, the UIScrollView will stop at a paging edge after a dragging or any movement, which is promised by the framework. Bounds.origin.y set by zero means that the first page edge matched the scroll view frame edge, cuz you have 64 contentInsets there. So that's not bug, that is what it is. And since your bar is translucent, remember where is your scroll view's frame edge, it's under the bar. In a word, this is not a bug, I think, but a effect of scroll paging.

How to stick a button that is normally below a UIWebView above the keyboard when it opens?

I would like to add a button and some other views below a UIWebView. When the keyboard opens, these views should smoothly animate up while the keyboard rises. The UIWebView should resize smoothly. I currently have the following:
No keyboard:
Keyboard:
It looks like it's working fine, but in reality, this is what it looks like in the middle of resizing:
The UIWebView's bottom jumps up immediately, the keyboard resizes smoothly, and the bottom views lag a little bit behind.
I have the following code:
- (void)registerForKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleKeyboardWillChangeFrameNotification:)
name:UIKeyboardWillChangeFrameNotification object:nil];
}
- (void)handleKeyboardWillChangeFrameNotification:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
NSTimeInterval animationDuration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
if (!self.is_keyboard_open) {
self.bottom_constraint_constant = self.bottomConstraint.constant;
self.bottomConstraint.constant = kbSize.height + self.bottomConstraint.constant;
} else {
self.bottomConstraint.constant = self.bottom_constraint_constant;
}
[UIView animateWithDuration:animationDuration animations:^{
[self.view layoutIfNeeded];
}];
self.is_keyboard_open = !self.is_keyboard_open;
}
The bottom constraint is the constraint between the text and the bottom of the screen.
Another issue I have is that when the keyboard is toggled more than once, the UIWebView does not resize correctly when it is opened, causing a partial render, as well as a large gray box at the bottom (which I can scroll up and down along with rest of the content, which would fit normally if this gray box was not present), like so:
What can I do? Thank you for your help!
You should animate the constraint change. You have all the information necessary in the notification user info (UIKeyboardAnimationCurveUserInfoKey, UIKeyboardAnimationDurationUserInfoKey).
Your second issue is a problem with UIWebView, I think. I've had this issue too. In iOS7, Apple seems to attempt setting a content inset for the webview scroll view, but sometimes it fails miserably. I ended up doing nasty tricks to prevent the webview from doing those.
Addendum
How to use the animation curve
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
// ... do stuff here
[UIView commitAnimations];
Webview tricks
Our webview use is even more complex because we use contentEditable=YES to edit HTML content. What we did was taking full control of the internal webview subview.
Normally, the view hierarchy of a UIWebView is:
UIWebView
Private UIScrollView subclass
Private view where the actual content is drawn
We ended up taking the private view and putting it inside our own scroll view which we can manage by ourselves. You need to update the content size as the private view grows or shrinks. We used key-value observation on the frame property to listen to changes to the size. To have the correct content inset, we used the keyboard frame to determine how much the bottom content inset is.
I must postface this, and say that ideally, we should not do this. It's error prone. From experience, we know it works from iOS5 to iOS7.1. I cannot promise it will work on iOS8. But since there are bugs which cannot be resolved otherwise, this is the best solution we found.

Resources