UITextView's NSTextContainer wrong position on text view height NSLayoutConstraint change - ios

I have subclassed UITextView to make it return an intrinsic content size like this:
- (void) layoutSubviews
{
[super layoutSubviews];
if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) {
[self invalidateIntrinsicContentSize];
}
}
- (CGSize)intrinsicContentSize
{
/*
Intrinsic content size of a textview is UIViewNoIntrinsicMetric
We have to build what we want here: contentSize + textContainerInset should do the trick
*/
CGSize intrinsicContentSize = self.contentSize;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0f) {
intrinsicContentSize.width += (self.textContainerInset.left + self.textContainerInset.right ) / 2.0f;
intrinsicContentSize.height += (self.textContainerInset.top + self.textContainerInset.bottom) / 2.0f;
}
return intrinsicContentSize;
}
I have added an observer to the UITextViewTextDidChangeNotification and when the text view content change I update its height to make it growth with the text height:
- (void)textViewTextDidChangeNotification:(NSNotification *)notification
{
UITextView *textView = (UITextView *)notification.object;
[self.view layoutIfNeeded];
void (^animationBlock)() = ^
{
self.messageInputViewHeightConstraint.constant = MAX(0, self.messageInputView.intrinsicContentSize.height);
[self.view layoutIfNeeded];
};
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:animationBlock
completion:nil];
}
But when their is enough lines to fill the text view height, half of the time when a new line is added the NSTextContainer of the UITextView is not well placed like you can see in this picture
(The NSTextContainer is outlined in red and UITextView is outlined in blue)
The other half of the time when a new line is added the NSTextContainer is replaced correctly.
I did not found how to solve this weird behavior.
I hope one of you will have an answer to fix it.
Thank you

Related

Resizing a UIView hides subviews

I have a view controller with a view that doesn't fill the screen and has transparency around the edges. In said view I have a tableview, some buttons and a text field.
I have all of the controls attached to the bottom of the view with constraints and the tableview attached to the top.
Everything resizes fine when the device is put in landscape/portrait mode, but when I manually resize it when the keyboard appears, the bottom of the view just moves up and covers the lowest elements.
Resizing/animation code:
- (void)keyboardDidShow:(NSNotification *)notification {
//Stuff to get keyboard frames
void (^animations)() = ^() {
CGRect mainViewFrame = self.mainView.frame;
CGRect tableViewFrame = self.tableView.frame;
tableViewFrame.size.height = tableViewFrame.size.height - 50;
mainViewFrame.size.height = mainViewFrame.size.height - 50;
CGFloat paddingOffset = self.view.frame.size.height - self.mainView.frame.origin.y - self.mainView.frame.size.height;
mainViewFrame.origin.y = (mainViewFrame.origin.y - keyboardEndFrame.size.height + paddingOffset - 8);
self.mainView.frame = mainViewFrame;
[self.tableView setNeedsLayout];
[self.mainView setNeedsLayout];
};
[UIView animateWithDuration:animationDuration
delay:0.0
options:(animationCurve << 16)
animations:animations
completion:^(BOOL finished){
if (finished) {
NSLog(#"Finished animating");
self.acceptBottomConstraint.constant = 5;
self.cancelBottomConstraint.constant = 5;
}
}];
}
This is my UI in the Storyboard:
This is what it looks like after resizing:

Change height of inputAccessoryView issue

When I change the height of inputAccessoryView in iOS 8, the inputAccessoryView not go to the right origin, but covers the keyboard.
Here are some code snippets:
in table view controller
- (UIView *)inputAccessoryView {
if (!_commentInputView) {
_commentInputView = [[CommentInputView alloc] initWithFrame:CGRectMake(0, 0, [self width], 41)];
[_commentInputView setPlaceholder:NSLocalizedString(#"Comment", nil) andButtonTitle:NSLocalizedString(#"Send", nil)];
[_commentInputView setBackgroundColor:[UIColor whiteColor]];
_commentInputView.hidden = YES;
_commentInputView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
}
return _commentInputView;
}
in CommentInputView
#when the textview change height
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
if (height > _textView_height) {
[self setHeight:(CGRectGetHeight(self.frame) + height - _textView_height)];
[self reloadInputViews];
}
}
in UIView Category from ios-helpers
- (void)setHeight: (CGFloat)heigth {
CGRect frame = self.frame;
frame.size.height = heigth;
self.frame = frame;
}
Finally, i found the answer. In ios8, apple add a NSContentSizeLayoutConstraints to inputAccessoryView and set a constant with 44. You can't remove this constaint, because ios8 use it to calculate the height of inputAccessoryView. So, the only solution is to change value of this constant.
Example
in ViewDidAppear
- (void)viewDidAppear:(BOOL)animated {
if ([self.inputAccessoryView constraints].count > 0) {
NSLayoutConstraint *constraint = [[self.inputAccessoryView constraints] objectAtIndex:0];
constraint.constant = CommentInputViewBeginHeight;
}
}
change inputAccessoryView height when the textview height changed
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
NSLayoutConstraint *constraint = [[self constraints] objectAtIndex:0];
float new_height = height + _textView_vertical_gap*2;
[UIView animateWithDuration:0.2 animations:^{
constraint.constant = new_height;
} completion:^(BOOL finished) {
[self setHeight:new_height];
[self reloadInputViews];
}];
}
That is.
One way you can update the constraint mentioned in Yijun's answer when changing the height of the inputAccessoryView is by overwriting setFrame: on your inputAccessoryView. This doesn't rely on the height constraint being the first in the array.
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
for (NSLayoutConstraint *constraint in self.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = frame.size.height;
break;
}
}
}
The first answer didn't totally solve my problem but gave me a huge hint.
Apple did add a private constraint to the accessory view, but you cannot find it in the constraint list of the accessory view. You have to search for it from its superview. It killed my a few hours.
After reading the answer above, which is a great find, I was concerned that relying on the constraint you need to change being [0] or firstObject is an implementation detail that's likely to change under us in the future.
After doing a bit of debugging, I found that the Apple-added constraints on the accessory input view seem to have a priority of 76. This is a crazy low value and not one of the listed enums in the documentation for priority.
Given this low priority value it seems like a cleaner solution to simply conditionally add/remove another constraint with a high priority level, say UILayoutPriorityDefaultHigh when you want to resize the view?
For Xcode 11.2 and swift 5 this function will update inputAccessoryView constraints even in animation block
func updateInputContainerConstraints() {
if let accessoryView = inputAccessoryView,
let constraint = accessoryView.superview?.constraints.first(where: { $0.identifier == "accessoryHeight" }) {
constraint.isActive = false
accessoryView.layoutIfNeeded()
constraint.constant = accessoryView.bounds.height
constraint.isActive = true
accessoryView.superview?.addConstraint(constraint)
accessoryView.superview?.superview?.layoutIfNeeded()
}
}
Try this:
_vwForSendChat is the input accessory view
_txtViewChatMessage is the textview inside input accessory view
-(void)textViewDidChange:(UITextView *)textView {
CGFloat fixedWidth = textView.frame.size.width;
CGSize newSize = [textView sizeThatFits:CGSizeMake(fixedWidth, MAXFLOAT)];
CGRect newFrame = textView.frame;
newFrame.size = CGSizeMake(fmaxf(newSize.width, fixedWidth), newSize.height);
if (newFrame.size.height < 40) {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, 40);
} else {
if (newFrame.size.height > 200) {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, 200);
} else {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, newFrame.size.height);
}
}
[self.txtViewChatMessage reloadInputViews];
}

Change NSLayoutConstraint constant not working in layoutSubviews

I am trying to change a UIButton's width when the view animates to landscape mode. But the method is called because I set a break point there, but the button's width doesn't change. I add a IBOutlet constraint to button's width named: globalButtonWidthConstraint.
My current code :
- (void)layoutSubviews {
[super layoutSubviews];
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
CGFloat screenH = screenSize.height;
CGFloat screenW = screenSize.width;
BOOL isLandscape = !(self.frame.size.width == (screenW*(screenW<screenH))+(screenH*(screenW>screenH)));
if (isLandscape) {
self.globalButtonWidthConstraint.constant = 100;
[self layoutIfNeeded];
} else {
self.globalButtonWidthConstraint.constant = 47;
[self layoutIfNeeded];
}
}
Try updating constraint constant in "viewDidLayoutSubviews".
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// Update constraint constant
[self.view layoutSubviews];
}
It worked for me.
EDIT: Make sure that there is no other constraints that conflicting this width constraint.

Resize UItextview according to it's content in iOS7

I'm trying to resize a text view according to content & also it's sibling and parent container.
Below code is working fine in iOS 6
if (/* less than ios 7 */) {
CGRect frame = _textView.frame;
CGSize conSize = _textView.contentSize;
CGFloat difference = conSize.height - frame.size.height;
frame.size.height += difference;
_textView.frame = frame;
UIScrollView *parentView = (UIScrollView *)_textView.superview;
// adjust views residing below this text view.
// sibling view
UIView *belowView = // access it somehow
CGRect frame1 = belowView.frame;
frame1.origin.y += difference;
belowView.frame = frame1;
// adjust parent scroll view, increase height.
CGSize frame3 = parentView.contentSize;
frame3.height += difference;
parentView.contentSize = frame3;
} else {
// tried
[_textView sizeToFit];
[_textView layoutIfNeeded];
[parentView sizeToFit];
[parentView layoutIfNeeded];
}
Tried to follow iOS 7 solution from:
How do I size a UITextView to its content on iOS 7?
but not working.
Any pointers?
Working code solution from #NSBouzouki
if (/* ios 7 */) {
[_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
[_textView layoutIfNeeded];
}
CGRect frame = _textView.frame;
CGSize conSize = _textView.contentSize;
CGFloat difference = conSize.height - frame.size.height;
frame.size.height += difference;
_textView.frame = frame;
UIScrollView *parentView = (UIScrollView *)_textView.superview;
// adjust views residing below this text view.
// sibling view
UIView *belowView = // access it somehow
CGRect frame1 = belowView.frame;
frame1.origin.y += difference;
belowView.frame = frame1;
// adjust parent scroll view, increase height.
CGSize frame3 = parentView.contentSize;
frame3.height += difference;
parentView.contentSize = frame3;
It seems UITextView's contentSize property is not correctly set in iOS 7 till viewDidAppear:. This is probably because NSLayoutManager lays out the text lazily and the entire text must be laid out for contentSize to be correct. The ensureLayoutForTextContainer: method forces layout of the provided text container after which usedRectForTextContainer: can be used for getting the bounds. In order to get total width and height correctly, textContainerInset property must be taken into account. The following method worked for me.
- (CGRect)contentSizeRectForTextView:(UITextView *)textView
{
[textView.layoutManager ensureLayoutForTextContainer:textView.textContainer];
CGRect textBounds = [textView.layoutManager usedRectForTextContainer:textView.textContainer];
CGFloat width = (CGFloat)ceil(textBounds.size.width + textView.textContainerInset.left + textView.textContainerInset.right);
CGFloat height = (CGFloat)ceil(textBounds.size.height + textView.textContainerInset.top + textView.textContainerInset.bottom);
return CGRectMake(0, 0, width, height);
}
Additionally, it seems UITextView's setContentSize: method is called from layoutSubviews. So, calling layoutIfNeeded on a textView (which itself calls layoutSubviews) after calling ensureLayoutForTextContainer: on its layoutManager, should make the textView's contentSize correct.
[someTextView.layoutManager ensureLayoutForTextContainer:someTextView.textContainer];
[someTextView layoutIfNeeded];
// someTextView.contentSize should now have correct value
GrowingTextViewHandler is an NSObject subclass which resizes text view as user types text.
Here is how you can use it.
#interface ViewController ()<UITextViewDelegate>
#property (weak, nonatomic) IBOutlet UITextView *textView;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *heightConstraint;
#property (strong, nonatomic) GrowingTextViewHandler *handler;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.handler = [[GrowingTextViewHandler alloc]initWithTextView:self.textView withHeightConstraint:self.heightConstraint];
[self.handler updateMinimumNumberOfLines:3 andMaximumNumberOfLine:8];
}
- (void)textViewDidChange:(UITextView *)textView {
[self.handler resizeTextViewWithAnimation:YES];
}
#end

Cursor outside UITextView

I've searched high and low and can't seem to find an answer. The closest thing I've seen is here:
UITextView cursor below frame when changing frame
Sorry, no screenshots as I have (nearly) no reputation, but it looks similar to the linked SO post.
When I run the app on iOS6, things work perfectly (content scrolls with the cursor to keep it on screen), but on iOS7, the cursor goes one line beyond the end of the UITextView. I tried adding UIEdgeInsets to move the content, but when the user is actively entering text, it just keeps adding a new line until the cursor is below the end of the text view.
My layout consists of a Label (headerText) with a UITextView (textView) below it. This view is shown from a tab bar. There is a keyboard input accessory that is added, but it's height is calculated into the keyboard height automatically before the function is called.
Here is the function I use to resize my views, called from keyboard show/hide delegate, rotate, initial layout, etc:
-(void)resizeViewsWithKbdHeight:(float)kbHeight
{
//set the header label frame
//set constraints
//total view width - 30 (15 each for left and right padding)
CGFloat labelWidth = ([[[self navigationController] view] bounds].size.width - 30);
CGSize constraintSize = {labelWidth, CGFLOAT_MAX};
//calculate needed height for header label
CGSize textSize = [[self headerText] sizeWithFont:[UIFont systemFontOfSize:17.0f]
constrainedToSize:constraintSize
lineBreakMode:NSLineBreakByWordWrapping];
//build and set frame for header label:
//make it the same as the old
CGRect headerTempSize = [[self headerLabel] frame];
//except for the height
headerTempSize.size.height = textSize.height;
//and set it
[[self headerLabel] setFrame:headerTempSize];
//correct the placement of the UITextView, so it's under the header label
//build a new frame based on current textview frame
CGRect newFrame = [[self textView] frame];
//get the y position of the uitextview, the +8 is the padding between header and uitextview
CGFloat vertPadding = [[self headerLabel] frame].origin.y + [[self headerLabel] frame].size.height + 8;
//bump it down vertically
newFrame.origin.y = vertPadding;
//bump things down by the amount of the navigation bar and status bar
float offscreenBump = [[[self navigationController] navigationBar] frame].origin.y + [[[self navigationController] navigationBar] frame].size.height;
//if we aren't showing the keyboard, add the height of the tab bar
if(kbHeight == 0) {
offscreenBump += [[[self tabBarController] tabBar] frame].size.height;
}
//calculate the new height of the textview, the +9 is for padding below the text view
CGFloat newHeight = [[[self navigationController] view] bounds].size.height - ([[self textView] frame].origin.y + 9 + kbHeight + offscreenBump);
//resize the height as calculated
newFrame.size.height = newHeight;
//set textview frame to this new frame
[[self textView] setFrame:newFrame];
}
I'm trying to support for iOS5, so no AutoLayout.
It's possible I'm being incredibly naive about how I'm doing things.
Thanks in advance!
This appears to be a bug in iOS 7. The only way I've found to correct this issue is to add a delegate for the UITextView and implement textViewDidChangeSelection, resetting the view to show the selection like this:
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
- (void) textViewDidChangeSelection: (UITextView *) tView {
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")) {
[tView scrollRangeToVisible:[tView selectedRange]];
}
}
I found a hackier yet more effective way to deal with the problem:
- (void)textViewDidChangeSelection:(UITextView *)textView
{
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
if ([textView.text characterAtIndex:textView.text.length-1] != ' ') {
textView.text = [textView.text stringByAppendingString:#" "];
}
NSRange range0 = textView.selectedRange;
NSRange range = range0;
if (range0.location == textView.text.length) {
range = NSMakeRange(range0.location - 1, range0.length);
} else if (range0.length > 0 &&
range0.location + range0.length == textView.text.length) {
range = NSMakeRange(range0.location, range0.length - 1);
}
if (!NSEqualRanges(range, range0)) {
textView.selectedRange = range;
}
}
}
Basically, I make sure that there's always a trailing space in the text field. Then, if the user tries to change the selection such that the space is revealed, change the selection. By always keeping a space ahead of the cursor, the field scrolls itself as it's supposed to.
Finally, if you need to, remove the trailing space when copying the text into your model (not shown.)

Resources