views:

1016

answers:

4

I'm using a UITextView to roughly replicate the SMS text box above the keyboard. I'm using a UITextView instead of a field so that it can expand with multiple lines.

The problem is that, in my UITextView, the correction suggestions pop up below the text, causing them to be partially obscured by the keyboard.

In the SMS app, the suggestions pop up above the text. The placement does not appear to be a property of UITextView, or UITextInputTraits.

Any idea how to replicate this behavior? Thanks!

A: 

If the bottom of your UITextView clears the keyboard, you should be able to just resize your UITextView to be tall enough to see the corrections. The corrections themselves don't display outside of the UITextView's frame.

If you want to mimic what you are getting in the SMS app (corrections above), you'll probably have to roll your own.

Matt Long
A: 

Make sure your view controller delegate is listening to the notification when the keyboard pops up so that you resize your UITextView so that the keyboard doesn't obscure the UITextView. Then your correction won't be obscured by the keyboard. See:

http://www.iphonedevsdk.com/forum/iphone-sdk-development/12641-uitextview-scroll-while-editing.html

Here is a copy of the code from that page in case the original link is broken:

// the amount of vertical shift upwards keep the Notes text view visible as the keyboard appears
#define kOFFSET_FOR_KEYBOARD                    140.0

// the duration of the animation for the view shift
#define kVerticalOffsetAnimationDuration        0.50

- (IBAction)textFieldDoneEditing:(id)sender
{
    [sender resignFirstResponder];
}

- (IBAction)backgroundClick:(id)sender
{
    [latitudeField resignFirstResponder];
    [longitudeField resignFirstResponder];
    [notesField resignFirstResponder];

    if (viewShifted)
    {
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:kVerticalOffsetAnimationDuration];

        CGRect rect = self.view.frame;
        rect.origin.y += kOFFSET_FOR_KEYBOARD;
        rect.size.height -= kOFFSET_FOR_KEYBOARD;
        self.view.frame = rect;

        [UIView commitAnimations];

        viewShifted = FALSE;
    }       
}

- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
    if (!viewShifted) {     // don't shift if it's already shifted

        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:kVerticalOffsetAnimationDuration];

        CGRect rect = self.view.frame;      
        rect.origin.y -= kOFFSET_FOR_KEYBOARD;
        rect.size.height += kOFFSET_FOR_KEYBOARD;
        self.view.frame = rect;

        [UIView commitAnimations];

        viewShifted = TRUE;
    }
    return YES;
}
lucius
+3  A: 

The problem is that the Keyboard is implemented as a separate UIWindow, rather than as a view within the main UIWindow, so layout with it is tricky. Here are some pointers in the right direction:

  • Hunt through the application's -windows property to find the private UITextEffectsWindow window and figure out its frame. This is the keyboard
  • Hunt through the TextView's subviews to find the private UIAutocorrectInlinePrompt view. This is the autocorrect bubble.
  • Move that subview into a separate wrapper view (added to the TextView) and then move that wrapper view so it's above the above-mentioned keyboard window.

You'll notice two mentions of "private" above. That carries all the relevant caveats. I have no idea why Apple has allowed the problem to persist when even their apps have had to work around it.

Rob Napier
I wonder... is traversing subviews without explicitly referring to private classes considered a violation? I mean, I assume it extends UIView right? What if I just happen to search through the tree of UIViews and change the location of one of them? Doesn't seem "private" to me, but perhaps Apple disagrees...
DougW
Transversing subviews is completely based on public APIs. But no matter what you're going to wind up relying on undocumented behavior. The main danger here is that an update could break you, so in production code I'd want to fail gracefully. I would not consider either of these private subclasses to be strongly stable, since they're both a bit funky, and funky things ate the most likely to be redesigned by Apple. Believe it or not, Apple doesn't make things private just to be mean :) Very often, they are the things Apple plans to refactor. Take it for what it's worth.
Rob Napier
@Rob Napier: By chance do you have any sample code? I'm curious if you used method swizzling or what you might have done to actually implement this...
marcc
I unfortunately can't post this code, but we did not need method swizzling. You just need to subclass UITextView and overload -layoutSubviews, which will be called when the autocorrect bubble is added.
Rob Napier
+1  A: 

By doing the search for the UIAutocorrectInlinePrompt in an overridden or swizzled layoutSubViews it is possible to alter the layout of the correction so that it appears above. You can do this without calling any private APIs by looking for the subs views of particular classes positioned in a way you'd expect them. This example works out which view is which, checks to see that the correction is not already above the text and moves the correction above, and draws it on the window so that it is not bounded by the UITextView itself. Obviously if apple change the underlying implementation then this will fail to move correction. Add this to your overriden or swizzled layoutSubViews implementation.

- (void) moveSpellingCorrection {
 for (UIView *view in self.subviews)
 {
  if ([[[view class] description] isEqualToString:@"UIAutocorrectInlinePrompt"])
  {
   UIView *correctionShadowView = nil; // [view correctionShadowView];

   for (UIView *subview in view.subviews)
   {
    if ([[[subview class] description] isEqualToString:@"UIAutocorrectShadowView"])
    {
     correctionShadowView = subview;
     break;
    }
   }

   if (correctionShadowView)
   {
    UIView *typedTextView = nil; //[view typedTextView];
    UIView *correctionView = nil; //[view correctionView];

    for (UIView *subview in view.subviews)
    {
     if ([[[subview class] description] isEqualToString:@"UIAutocorrectTextView"])
     {
      if (CGRectContainsRect(correctionShadowView.frame,subview.frame))
      {
       correctionView = subview;
      }
      else
      { 
       typedTextView = subview;
      }
     }

    }
    if (correctionView && typedTextView)
    {

     CGRect textRect = [typedTextView frame];
     CGRect correctionRect = [correctionView frame];
     if (textRect.origin.y < correctionRect.origin.y)
     {
      CGAffineTransform moveUp = CGAffineTransformMakeTranslation(0,-50.0);
      [correctionView setTransform: moveUp];
      [correctionShadowView setTransform: moveUp];

      CGRect windowPos = [self convertRect: view.frame toView: nil ];
      [view removeFromSuperview];
      [self.window addSubview: view];
      view.frame = windowPos;
     }

    }
   }

  }

 }
}
Johnno
Thanks for the code. It's definitely interesting, but I agree with some of the other comments that it's too unpredictable for general use.
DougW