Monday, September 23, 2013

Last Line(s) of a UITextView May Scroll Past the Bounds of the View

I ran across this oddity today that rears its head in UITextViews under iOS 7 (didn't occur under iOS 6). Essentially, the view allows text to be written past the bottom of the UITextView's frame causing text to be hidden by a keyboard and/or keyboard accessory. This problem doesn't manifest if you're typing and the text word wraps; it only appears after tapping Return (whether after text or by itself). The following snippet (which goes in your UITextViewDelegate) did the trick for me:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    [textView scrollRangeToVisible:range];

    if ([text isEqualToString:@"\n"]) {
        [UIView animateWithDuration:0.2 animations:^{
            [textView setContentOffset:CGPointMake(textView.contentOffset.x, textView.contentOffset.y + 20)];
        }];
    }

    return YES;
}

The call to -scrollRangetoVisible mostly does the trick. When you start typing at the bottom of the UITextView, the text will be hidden, but will immediately scroll up to become visible. A touch odd, but bearable. However, if you just tap Return, the text will continue to be non-visible. Thus, we check for a newline, and if so, adjust the contentOffset for the view by an appropriate amount for your text (probably better to calculate the right size if you allow the user to choose a font and/or font size). The animation makes the adjustment smooth so the text view doesn't jerk each time text is changed.

Here's to hoping Apple gets this one fixed in the next point release or two. I'll be filing a Radar.

Update 2013-09-24

After working with the the original version some more, it had a number of shortcomings. This is the version I'm using now, which should be more robust and should properly account for font and font size changes.

- (BOOL)textView:(UITextView *)tView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    CGRect textRect = [tView.layoutManager usedRectForTextContainer:tView.textContainer];
    CGFloat sizeAdjustment = tView.font.lineHeight * [UIScreen mainScreen].scale;
    
    if (textRect.size.height >= tView.frame.size.height - sizeAdjustment) {
        if ([text isEqualToString:@"\n"]) {
            [UIView animateWithDuration:0.2 animations:^{
                [tView setContentOffset:CGPointMake(tView.contentOffset.x, tView.contentOffset.y + sizeAdjustment)];
            }];
        }
    }
    
    return YES;
}

Update 2013-10-03

Kevin Hayes has updated my latest update to account for UITextViews where the view extends underneath the keyboard. He's detailed his update here: Autoscrolling UITextView in iOS7.

Additionally, I've found adding this bit (from the UITextViewDelegate) also helps things work more smoothly, as well:

- (void)textViewDidChangeSelection:(UITextView *)textView {
    [textView scrollRangeToVisible:textView.selectedRange];
}

Friday, September 6, 2013

Alfred Workflow: Rate Track and Play

Inspired by the following couple of tweets, I realized I had a similar problem. I tend to use smart playlists in iTunes to work through music to be rated. Since iTunes 11 was released, changing the rating of a track in a smart playlist where the track no longer belongs on the list causes the audio playback to stop.

Since I'm not a Keyboard Maestro user, I wanted to take advantage of the same workflow but in my favorite tool: Alfred. Wrote up some AppleScript to do the dirty work and embedded it all in an Alfred 2 workflow to trigger them with hotkeys and notify when the rating has been made. Happy listening and rating.

Download the Workflow