Center text vertically in a UITextView

70,813

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 :

enter image description here

  1. Make height 0 and add constraints greater than
  2. Add vertically align to parent constraints
  3. Add horizontally align to parent constraints

enter image description here

Share:
70,813
Sergey Grischyov
Author by

Sergey Grischyov

St. Petersburg State University graduate. Cocoa Controls contributor.

Updated on July 05, 2022

Comments

  • Sergey Grischyov
    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 of UITextView if the text is short, so there are no blank areas in the UITextView. Can this be done? Thanks in advance!

  • audub
    audub about 11 years
    This 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
    Skyler almost 11 years
    This 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
    Greg over 10 years
    UITextView (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
    Balazs Nemeth about 9 years
    Unfortunately 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
    Etan about 9 years
    What 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
    Laszlo almost 9 years
    I Prefer using NSStringFromSelector(@selector(contentSize)) instead of just @"contentSize". And don't forget to put the removeObserver:forKeyPath: to a try-catch statement!
  • Kiran Jasvanee
    Kiran Jasvanee almost 9 years
    I tried to remove the observer too in viewWillDisappear & viewDidDisappear, then also it's crashing sometimes.
  • Kiran Jasvanee
    Kiran Jasvanee almost 9 years
    Rather than setting observer, this process is simple and flexible to keep anywhere.
  • sbarow
    sbarow almost 9 years
    You can remove the observer in the dealloc method, -(void)dealloc {}
  • Kiran Jasvanee
    Kiran Jasvanee almost 9 years
    dealloc is not allowed anymore in ARC projects. :)
  • sbarow
    sbarow almost 9 years
    The dealloc method is allowed in ARC projects, just don't call [super dealloc];
  • Ievgen
    Ievgen over 8 years
    with autolayout you have to use let size = tv.sizeThatFits(CGSizeMake(CGRectGetWidth(tv.bounds), CGFloat(MAXFLOAT))) instead of [tv contentSize]
  • s.ka
    s.ka over 8 years
    This 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
    Yuchen over 8 years
    Instead of updating the contentInset, I try resizing the textView by changing the constraints whenever the above contentSize didSet gets called and that also works.
  • Gobe
    Gobe about 8 years
    Working 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 the topCorrection and, in case you still want to disable users to scroll, you will also need userInteractionEnabled = false. Also, a minor for copy-pasters: Xcode will require an init method in your subclassed UITextView.
  • nnarayann
    nnarayann about 8 years
    Thanks!, it works but had to use setTextContainerInset instead of setContentInset
  • DJtiwari
    DJtiwari almost 8 years
    Same 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
    Pârvănescu Stefan over 7 years
    This solution also requires auto-resizeable text view - stackoverflow.com/questions/16868117/…
  • lifewithelliott
    lifewithelliott about 7 years
    this 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
    Beninho85 about 7 years
    It'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
    Bright almost 7 years
    This solution is way better!
  • Shyam
    Shyam over 6 years
    How to even use this, while working? Sorry, if I'm asking a dumb question. New to swift.
  • Beninho85
    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
    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
    Beninho85 over 6 years
    Yes, 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.
    Jake T. about 6 years
    Does this need to be called after the view has been laid out, I imagine?
  • schinj
    schinj about 6 years
    The 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
    Radu almost 6 years
    Didn'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
    Ryan Romanchuk over 5 years
    This works well if you disable scrolling on the UITextView
  • iAleksandr
    iAleksandr over 5 years
    Thanks, that is what was the need for me.
  • rgkobashi
    rgkobashi over 5 years
    This 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
    Takasur almost 5 years
    This is the only solution works even in Xcode 10 Swift 4+
  • iWill
    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
    Duck over 4 years
    Brilliant answer. Thanks.
  • codewithfeeling
    codewithfeeling over 4 years
    This 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
    anon_nerd over 4 years
    cuts off text on iOS 10 if there are line breaks
  • MathMax
    MathMax almost 4 years
    It is so frustrating that this cannot be achieved directly in the Storyboard
  • malex
    malex over 3 years
    One should take into account layoutMargins and write var topCorrection = (bounds.size.height - contentSize.height * zoomScale + layoutMargins.top + layoutMargins.bottom) / 2.0
  • Ibrahim Nafiz
    Ibrahim Nafiz over 2 years
    This works great. Make sure you set correct class on the xib identity inspector. d.pr/i/hLQe9C