Center text vertically in a UITextView
Solution 1
First add an observer for the contentSize
key value of the UITextView
when the view is loaded:
- (void) viewDidLoad {
[textField addObserver:self forKeyPath:@"contentSize" options:(NSKeyValueObservingOptionNew) context:NULL];
[super viewDidLoad];
}
Then add this method to adjust the contentOffset
every time the contentSize
value changes:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
UITextView *tv = object;
CGFloat topCorrect = ([tv bounds].size.height - [tv contentSize].height * [tv zoomScale])/2.0;
topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );
tv.contentOffset = (CGPoint){.x = 0, .y = -topCorrect};
}
Solution 2
Because UIKit is not KVO compliant, I decided to implement this as a subclass of UITextView
which updates whenever the contentSize
changes.
It's a slightly modified version of Carlos's answer which sets the contentInset
instead of the contentOffset
. In addition to being compatible with iOS 9, it also seems to be less buggy on iOS 8.4.
class VerticallyCenteredTextView: UITextView {
override var contentSize: CGSize {
didSet {
var topCorrection = (bounds.size.height - contentSize.height * zoomScale) / 2.0
topCorrection = max(0, topCorrection)
contentInset = UIEdgeInsets(top: topCorrection, left: 0, bottom: 0, right: 0)
}
}
}
Solution 3
If you don't want to use KVO you can also manually adjust offset with exporting this code to a function like this :
-(void)adjustContentSize:(UITextView*)tv{
CGFloat deadSpace = ([tv bounds].size.height - [tv contentSize].height);
CGFloat inset = MAX(0, deadSpace/2.0);
tv.contentInset = UIEdgeInsetsMake(inset, tv.contentInset.left, inset, tv.contentInset.right);
}
and calling it in
-(void)textViewDidChange:(UITextView *)textView{
[self adjustContentSize:textView];
}
and every time you edit the text in the code. Don't forget to set the controller as the delegate
Swift 3 version:
func adjustContentSize(tv: UITextView){
let deadSpace = tv.bounds.size.height - tv.contentSize.height
let inset = max(0, deadSpace/2.0)
tv.contentInset = UIEdgeInsetsMake(inset, tv.contentInset.left, inset, tv.contentInset.right)
}
func textViewDidChange(_ textView: UITextView) {
self.adjustContentSize(tv: textView)
}
Solution 4
For iOS 9.0.2. we'll need to set the contentInset instead. If we KVO the contentOffset, iOS 9.0.2 sets it to 0 at the last moment, overriding the changes to contentOffset.
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
UITextView *tv = object;
CGFloat topCorrect = ([tv bounds].size.height - [tv contentSize].height * [tv zoomScale])/2.0;
topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );
[tv setContentInset:UIEdgeInsetsMake(topCorrect,0,0,0)];
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:NO];
[questionTextView addObserver:self forKeyPath:@"contentSize" options:(NSKeyValueObservingOptionNew) context:NULL];
}
I used 0,0, and 0 for the left,bottom and right edge insets respectively. Make sure to calculate those as well for your use case.
Solution 5
You can set it up directly with only constraints:
There are 3 constraints i added to align text vertically and horizontally in constraints as below :
- Make height 0 and add constraints greater than
- Add vertically align to parent constraints
- Add horizontally align to parent constraints
Sergey Grischyov
St. Petersburg State University graduate. Cocoa Controls contributor.
Updated on July 05, 2022Comments
-
Sergey Grischyov almost 2 years
I want to center the text vertically inside a big
UITextView
that fills the whole screen - so that when there's little of text, say a couple of words, it is centered by height. It's not a question about centering the text (a property that can be found in IB) but about putting the text vertically right in the middle ofUITextView
if the text is short, so there are no blank areas in theUITextView
. Can this be done? Thanks in advance! -
audub about 11 yearsThis works great for me until the UITextView is filled with text and starts to scroll. Then the view bounces for each new character. I put your code in an if to check if content is higher than view: if (([myView bounds].size.height - [myView contentSize].height) > 0)
-
Skyler almost 11 yearsThis solution worked great for me; however, while testing my app I noticed that I'd get a crash if I called this code 3-4 times. Looks like you also want to remove the observer. I added this and it fixed the crash: - (void)viewWillDisappear:(BOOL)animated { [textField removeObserver:self forKeyPath:@"contentSize"]; } I found this solution thanks to this post: stackoverflow.com/a/7467414/81008
-
Greg over 10 yearsUITextView (and other UIKit classes) are not guaranteed to be KVO compliant. Relying on this could cause your code to break in future iOS versions. See this post by a member of the UIKit development team: stackoverflow.com/a/6051404/357641
-
Balazs Nemeth about 9 yearsUnfortunately it does not work for me. Even these four constraints are not enough. Can I somehow set the textField to 'stick around' the text content?
-
Etan about 9 yearsWhat happens if you put the container on the same level as the text view in the hierarchy? - So, instead of it acting as an actual container, it's just a placeholder located at the same location.
-
Laszlo almost 9 yearsI Prefer using
NSStringFromSelector(@selector(contentSize))
instead of just@"contentSize"
. And don't forget to put theremoveObserver:forKeyPath:
to a try-catch statement! -
Kiran Jasvanee almost 9 yearsI tried to remove the observer too in viewWillDisappear & viewDidDisappear, then also it's crashing sometimes.
-
Kiran Jasvanee almost 9 yearsRather than setting observer, this process is simple and flexible to keep anywhere.
-
sbarow almost 9 yearsYou can remove the observer in the
dealloc
method,-(void)dealloc {}
-
Kiran Jasvanee almost 9 years
dealloc
is not allowed anymore in ARC projects. :) -
sbarow almost 9 yearsThe dealloc method is allowed in ARC projects, just don't call
[super dealloc];
-
Ievgen over 8 yearswith autolayout you have to use let size = tv.sizeThatFits(CGSizeMake(CGRectGetWidth(tv.bounds), CGFloat(MAXFLOAT))) instead of [tv contentSize]
-
s.ka over 8 yearsThis appears to be broken in iOS 9.0.2. However, a workaround exists. Instead of setting the contentOffset, we can set the contentInset instead (Just change the last line of code in the observeValueForKeyPath method to): ` [tv setContentInset:UIEdgeInsetsMake(topCorrect,0,0,0)];` Note: If we KVO the contentOffset, iOS 9 sets it to 0 at the last moment, overriding any changes to contentOffset.
-
Yuchen over 8 yearsInstead of updating the
contentInset
, I try resizing the textView by changing the constraints whenever the abovecontentSize didSet
gets called and that also works. -
Gobe about 8 yearsWorking fine for iOS 9.2. Just wanted to point out that scrolling MUST be enabled for this to work. If you want to ensure that programmatically (probably safer than expecting someone to set it correctly in the storyboard), just call
scrollEnabled = true
before calculating thetopCorrection
and, in case you still want to disable users to scroll, you will also needuserInteractionEnabled = false
. Also, a minor for copy-pasters: Xcode will require aninit
method in your subclassed UITextView. -
nnarayann about 8 yearsThanks!, it works but had to use
setTextContainerInset
instead ofsetContentInset
-
DJtiwari almost 8 yearsSame code is not working in UITableViewCell - (void)awakeFromNib { [self.textViewLarge addObserver:self forKeyPath:@"contentSize" options:(NSKeyValueObservingOptionNew) context:NULL]; [self.textView1 addObserver:self forKeyPath:@"contentSize" options:(NSKeyValueObservingOptionNew) context:NULL]; [self.textView2 addObserver:self forKeyPath:@"contentSize" options:(NSKeyValueObservingOptionNew) context:NULL]; [super awakeFromNib]; }
-
Pârvănescu Stefan over 7 yearsThis solution also requires auto-resizeable text view - stackoverflow.com/questions/16868117/…
-
lifewithelliott about 7 yearsthis works as shown, but I can not initialize the text view upon cell appearing. Only works if I pass the delegates textView argument into the adjustContentSize(_:), not if I pass in the IBOutlet variable name. Any idea why this may be the case?
-
Beninho85 about 7 yearsIt's because things with x and y position like size, insets, are valid once the view has redrawn the view with autolayout constraint, so you can call adjustContentSize after the viewDidLayoutSubviews function in your controller and it should work
-
Bright almost 7 yearsThis solution is way better!
-
Shyam over 6 yearsHow to even use this, while working? Sorry, if I'm asking a dumb question. New to swift.
-
Beninho85 over 6 years@Shyam TextviewDidChange is a delegate of UITextfield. If you don't know what is it, I suggest you to take a look at the delegate pattern because it's used in many scenarios in iOS. Basically, you have to set the delegate as your current view controller ( yourtextfield.delegate = self) and implement required functions (textViewDidChange in this case)
-
Shyam over 6 years@Beninho85, thanks for your time. Yeah, got a hold of delegates. Well, my question should have been more detailed. I use a placeholder in a
UITextView
. And this adheres to auto layout. So, I would like to centre the placeholder vertically, in all views. But, if text is entered by the user, the placeholder is replaced, and the alignment should revert back to use the full area available. The vertical alignment is only for the placeholder. Does it make sense? -
Beninho85 over 6 yearsYes, it's because my solution uses the Content Offset property of the textfield to adjust the current text to the middle of the UITextview each time the user types something. If you want to center the placeholder only, just call adjustContentSize function once after setting your placeholder text. I'm not sure about the behaviour after that, but normally you don't need the delegate because it's used only for adjust text when typed. If it doesn't work, just try to reset the offset at 0 or something like that in the textviewdidchange property.
-
Jake T. about 6 yearsDoes this need to be called after the view has been laid out, I imagine?
-
schinj about 6 yearsThe max should be calculated between 0 and topOffset because 1 might cause the clipping of text from bottom in case the contentSize is same as the textView size.
-
Radu almost 6 yearsDidn't work....this callback gets called 5-6 times per one screen appearance, and the calculations are not identical each time....it's not good at all please don't use
-
Ryan Romanchuk over 5 yearsThis works well if you disable scrolling on the UITextView
-
iAleksandr over 5 yearsThanks, that is what was the need for me.
-
rgkobashi over 5 yearsThis worked for me, however adding these constraints xcode will throw an error that there is some ambiguity. To silence it I added top and bottom constraints equal to 0 with 250 as priority.
-
Takasur almost 5 yearsThis is the only solution works even in Xcode 10 Swift 4+
-
iWill almost 5 years@user3777977 Just copy your answer here. Add to Carlos answer, just in case you have text in tv bigger then tv size you don't need to recenter text, so change this code:
tv.contentOffset = (CGPoint){.x = 0, .y = -topCorrect};
to this:if ([tv contentSize].height < [tv bounds].size.height) { tv.contentOffset = (CGPoint){.x = 0, .y = -topCorrect}; }
-
Duck over 4 yearsBrilliant answer. Thanks.
-
codewithfeeling over 4 yearsThis is a really elegant solution. It worked perfectly for a UIViewRepresentable in SwiftUI that responds to a MagnificationGesture changing font size dynamically. Bravo!
-
anon_nerd over 4 yearscuts off text on iOS 10 if there are line breaks
-
MathMax almost 4 yearsIt is so frustrating that this cannot be achieved directly in the Storyboard
-
malex over 3 yearsOne should take into account
layoutMargins
and writevar topCorrection = (bounds.size.height - contentSize.height * zoomScale + layoutMargins.top + layoutMargins.bottom) / 2.0
-
Ibrahim Nafiz over 2 yearsThis works great. Make sure you set correct class on the xib identity inspector. d.pr/i/hLQe9C