75

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?

starball
  • 20,030
  • 7
  • 43
  • 238
Sergey Grischyov
  • 11,995
  • 20
  • 81
  • 120

19 Answers19

64

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};
}
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
CSolanaM
  • 3,138
  • 1
  • 20
  • 21
  • 3
    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) – audub Mar 10 '13 at 13:17
  • 3
    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: http://stackoverflow.com/a/7467414/81008 – Skyler Jul 14 '13 at 22:31
  • 9
    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: http://stackoverflow.com/a/6051404/357641 – Greg Aug 28 '13 at 17:48
  • I Prefer using `NSStringFromSelector(@selector(contentSize))` instead of just `@"contentSize"`. And don't forget to put the `removeObserver:forKeyPath:` to a try-catch statement! – Laszlo Jun 17 '15 at 08:52
  • with autolayout you have to use let size = tv.sizeThatFits(CGSizeMake(CGRectGetWidth(tv.bounds), CGFloat(MAXFLOAT))) instead of [tv contentSize] – Ievgen Aug 14 '15 at 19:40
  • 9
    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. – s.ka Oct 25 '15 at 21:58
  • 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]; } – DJtiwari May 24 '16 at 10:58
  • It is so frustrating that this cannot be achieved directly in the Storyboard – MathMax Aug 04 '20 at 16:41
57

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)
        }
    }
}
Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • 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. – Yuchen Feb 07 '16 at 04:26
  • 4
    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. – Gobe Feb 16 '16 at 20:30
  • One should take into account `layoutMargins` and write `var topCorrection = (bounds.size.height - contentSize.height * zoomScale + layoutMargins.top + layoutMargins.bottom) / 2.0` – malex Dec 01 '20 at 05:22
27

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)
}
Beninho85
  • 3,273
  • 27
  • 22
  • 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? – lifewithelliott Mar 15 '17 at 02:55
  • 1
    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 – Beninho85 Mar 15 '17 at 14:39
  • How to even use this, while working? Sorry, if I'm asking a dumb question. New to swift. – Shyam Aug 14 '17 at 11:09
  • @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) – Beninho85 Aug 15 '17 at 19:40
  • @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? – Shyam Aug 16 '17 at 04:23
  • 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. – Beninho85 Aug 16 '17 at 15:05
  • This is the only solution works even in Xcode 10 Swift 4+ – Takasur Jun 06 '19 at 15:39
20

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.

s.ka
  • 498
  • 5
  • 9
17

It is the simple task using NSLayoutManager to get real text size of the NSTextContainer

class VerticallyCenteredTextView: UITextView {

    override func layoutSubviews() {
        super.layoutSubviews()
    
        let rect = layoutManager.usedRect(for: textContainer)
        let topInset = (bounds.size.height - rect.height) / 2.0
        textContainerInset.top = max(0, topInset)
    }
}

Don't use contentSize and contentInset in your final calculations.

malex
  • 9,874
  • 3
  • 56
  • 77
  • This works great. Make sure you set correct class on the xib identity inspector. https://d.pr/i/hLQe9C – Nafiz Aug 19 '21 at 05:22
13

Here's a UITextView extension that centers content vertically:

extension UITextView {

    func centerVertically() {
        let fittingSize = CGSize(width: bounds.width, height: CGFloat.max)
        let size = sizeThatFits(fittingSize)
        let topOffset = (bounds.size.height - size.height * zoomScale) / 2
        let positiveTopOffset = max(0, topOffset)
        contentOffset.y = -positiveTopOffset
    }

}
Rudolf Adamkovič
  • 31,030
  • 13
  • 103
  • 118
11

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

Antzi
  • 12,831
  • 7
  • 48
  • 74
KOTIOS
  • 11,177
  • 3
  • 39
  • 66
  • 1
    This works well if you disable scrolling on the UITextView – Ryan Romanchuk Aug 14 '18 at 07:18
  • 2
    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. – rgkobashi Dec 26 '18 at 06:56
8

I just created a custom vertically centered text view in Swift 3:

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)
        }
    }
}

Ref: https://geek-is-stupid.github.io/2017-05-15-how-to-center-text-vertically-in-a-uitextview/

Tai Le
  • 8,530
  • 5
  • 41
  • 34
  • 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 – Radu Jun 15 '18 at 06:01
  • This is a really elegant solution. It worked perfectly for a UIViewRepresentable in SwiftUI that responds to a MagnificationGesture changing font size dynamically. Bravo! – codewithfeeling Oct 08 '19 at 10:20
7
func alignTextVerticalInTextView(textView :UITextView) {

    let size = textView.sizeThatFits(CGSizeMake(CGRectGetWidth(textView.bounds), CGFloat(MAXFLOAT)))

    var topoffset = (textView.bounds.size.height - size.height * textView.zoomScale) / 2.0
    topoffset = topoffset < 0.0 ? 0.0 : topoffset

    textView.contentOffset = CGPointMake(0, -topoffset)
}
Ievgen
  • 1,596
  • 16
  • 13
6

I have a textview that I'm using with autolayout and with setting the lineFragmentPadding and textContainerInset to zero. None of the solutions above worked in my situation. However, this works for me. Tested with iOS 9

@interface VerticallyCenteredTextView : UITextView
@end

@implementation VerticallyCenteredTextView

-(void)layoutSubviews{
    [self recenter];
}

-(void)recenter{
    // using self.contentSize doesn't work correctly, have to calculate content size
    CGSize contentSize = [self sizeThatFits:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX)];
    CGFloat topCorrection = (self.bounds.size.height - contentSize.height * self.zoomScale) / 2.0;
    self.contentOffset = CGPointMake(0, -topCorrection);
}

@end
user1687195
  • 9,058
  • 2
  • 15
  • 15
  • Thanks, that is what was the need for me. – iAleksandr Oct 09 '18 at 21:51
  • @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}; } ` – iWill Aug 02 '19 at 06:52
5

I have also this problem and I solved it with a UITableViewCell with UITextView. I created method in a custom UITableViewCell subclass, property statusTextView:

- (void)centerTextInTextView
{
    CGFloat topCorrect = ([self.statusTextView bounds].size.height - [self.statusTextView contentSize].height * [self.statusTextView zoomScale])/2.0;
    topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );
    self.statusTextView.contentOffset = (CGPoint){ .x = 0, .y = -topCorrect };

And call this method in methods:

- (void)textViewDidBeginEditing:(UITextView *)textView
- (void)textViewDidEndEditing:(UITextView *)textView
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

This solution worked for me with no issues, you can try it.

ThomasCle
  • 6,792
  • 7
  • 41
  • 81
Miro Durkovic
  • 79
  • 1
  • 5
5

Swift 3:

override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        textField.frame = self.view.bounds

        var topCorrect : CGFloat = (self.view.frame.height / 2) - (textField.contentSize.height / 2)
        topCorrect = topCorrect < 0.0 ? 0.0 : topCorrect
        textField.contentInset = UIEdgeInsetsMake(topCorrect,0,0,0)

    }
Arnaldo
  • 673
  • 6
  • 22
2

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};
}
2

Auto-layout solution:

  1. Create a UIView that acts as a container for the UITextView.
  2. Add the following constraints:
    • TextView: Align leading space to: Container
    • TextView: Align trailing space to: Container
    • TextView: Align center Y to: Container
    • TextView: Equal Height to: Container, Relation: ≤
Etan
  • 17,014
  • 17
  • 89
  • 148
  • 1
    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? – Balazs Nemeth Mar 14 '15 at 18:13
  • 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. – Etan Mar 14 '15 at 19:10
  • This solution also requires auto-resizeable text view - http://stackoverflow.com/questions/16868117/uitextview-that-expands-to-text-using-auto-layout – Dmitry Nov 24 '16 at 16:33
  • This solution is way better! – Bright May 25 '17 at 06:35
1

You can try below code, no observer mandatorily required. observer throws error sometimes when view deallocates. You can keep this code in viewDidLoad, viewWillAppear or in viewDidAppear anywhere.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_async(dispatch_get_main_queue(), ^(void) {
        UITextView *tv = txtviewDesc;
        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};
    });
});
Kiran Jasvanee
  • 6,362
  • 1
  • 36
  • 52
0

I did it like this: first of all, I embedded the UITextView in an UIView (this should work for mac OS too). Then I pinned all four sides of the external UIView to the sides of its container, giving it a shape and size similar or equal to that of the UITextView. Thus I had a proper container for the UITextView. Then I pinned the left and right borders of the UITextView to the sides of the UIView, and gave the UITextView a height. Finally, I centered the UITextView vertically in the UIView. Bingo :) now the UITextView is vertically centered in the UIView, hence text inside the UITextView is vertically centered too.

user2626974
  • 51
  • 1
  • 3
-1

UITextView+VerticalAlignment.h

//  UITextView+VerticalAlignment.h
//  (c) The Internet 2015
#import <UIKit/UIKit.h>

@interface UITextView (VerticalAlignment)

- (void)alignToVerticalCenter;
- (void)disableAlignment;

@end

UITextView+VerticalAlignment.m

#import "UITextView+VerticalAlignment.h"

@implementation UITextView (VerticalAlignment)

- (void)alignToVerticalCenter {
    [self addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:NULL];
}

- (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};
}

- (void)disableAlignment {
    [self removeObserver:self forKeyPath:@"contentSize"];
}
@end
Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
-1

I fixed this problem by creating extension to center height vertically.

SWIFT 5:

extension UITextView {
    func centerContentVertically() {
        let fitSize = CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
        let size = sizeThatFits(fitSize)
        let heightOffset = (bounds.size.height - size.height * zoomScale) / 2
        let positiveTopOffset = max(0, heightOffset)
        contentOffset.y = -positiveTopOffset
    }
}
user1039695
  • 981
  • 11
  • 20
-4

Solution for iOS10 in RubyMotion:

class VerticallyCenteredTextView < UITextView

    def init
        super
    end

    def layoutSubviews
        self.recenter
    end

    def recenter
        contentSize = self.sizeThatFits(CGSizeMake(self.bounds.size.width, Float::MAX))
        topCorrection = (self.bounds.size.height - contentSize.height * self.zoomScale) / 2.0;
        topCorrection = 0 if topCorrection < 0
        self.contentInset = UIEdgeInsetsMake(topCorrection,  0, 0, 0)
    end

end
David Hersey
  • 1,049
  • 11
  • 13