UITextView height in iOS 7

I've spent an embarrassing amount of time trying to get text editing in Stampnote working the way it used to under iOS 6. The app has an auto-sizing UITextView embedded in a UITableViewCell. I calculated the heightForRowAtIndexPath: using the text view's contentSize. The UITableViewController handled keeping the cursor caret above the keyboard.

When running on iOS 7, the content size wasn't correct. I found a few forum posts mentioning ways to force the text view to lay itself out so that calling contentSize would work as before (eg, call sizeToFit, layoutIfNeeded, and/or ensureLayoutForTextContainer). After a while I felt like I was typing incantations into viewWillAppear, viewWillLayoutSubviews, and UITextViewDelegate calls, running the app, typing sample text, and repeating. I lost track of what I was doing. One of these may have actually worked, but since I was also looking for the full iOS 6 behavior (autoscrolling to the cursor location) I continued searching.

Displaying

One of the first pieces I got working was the height calculation for the UITextView:


    UITextView *textView = self.entryTextCell.textView;
    CGFloat width = textView.bounds.size.width - 2.0 * textView.textContainer.lineFragmentPadding;
       
    NSDictionary *options = @{ NSFontAttributeName: textView.font };
    CGRect boundingRect = [textView.text boundingRectWithSize:CGSizeMake(width, NSIntegerMax)
                options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
                attributes:options context:nil];

The NSStringDrawingUsesFontLeading isn't necessary; in my testing I got the same values whether it was present or not. I left it in, though. If you search Apple's documentation for "Querying Font Metrics" you'll see the leading as part of the line height.

The above works quite well if you're calculating the size of a UILabel. The text view required more work.

Including the lineFragmentPadding helped greatly; by default text view's have a 5pts padding to the left and right.

This wasn't quite perfect, however, and I'd sometimes end up a couple pixels short. I found a post in the dev forums by Jonathon F. that suggested adding the difference between the font ascent and cap height. That actually seemed to help, typically adding 5px to my bounding rect. I'm using dynamic type, so the amount varies based on the selected system text size.


CGFloat adj = ceilf(textView.font.ascender - textView.font.capHeight);

I also included the content insets, which were about 8pts top and bottom.


CGFloat insets = textView.textContainerInset.top + textView.textContainerInset.bottom;

I also encountered some issues when the last character in the text view was a newline. It seemed like this new line wasn't being factored into the boundingRectWithSize: call. As soon as another character was typed into the text view, the height would become correct. I noticed the difference was simply the line height, exactly.

textView.font.lineHeight

I added this to the calculated height only when the cursor was at the end of the document and a newline was present.

This appears to be working now, for iOS 7.0. As bugs in iOS get fixed it'll probably stop working. As software improves, workarounds become your own bugs. I imagine the best workarounds degrade perfectly.

Editing

This part was frustrating. Tapping in the text view caused the keyboard to come up and, as that was happening, the table would scroll to the end of the document, reverse course, and scroll away. I couldn't see what I was typing. I ended up asking for help, and, a couple days later, finding a decent workaround.