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!
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.
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;
}
}
}
}
}
}
Actually doing
textview.scrollEnabled = NO;
will set the bubble on top of the text... the caveat is that you lose scrolling, in my case it wasn't a problem due to havinng a textfield only for input purposes with character limit
Actually, the keyboard simply uses the result of -[UITextInput textInputView] to determine where to put the correction view (and to ask if your view supports correction). So all you need to do is this:
- (UIView *)textInputView {
for (UIWindow *window in [UIApplication sharedApplication].windows) {
if ([window isKindOfClass:NSClassFromString(#"UITextEffectsWindow")] &&
window != self.window) {
return window;
}
}
// Fallback just in case the UITextEffectsWindow has not yet been created.
return self;
}
Note that you'll likely also need to update -[UITextInput firstRectForRange:] to use the coordinate system of the window / device, so you can do this:
- (CGRect)firstRectForRange:(CoreTextTokenTextRange *)range {
CGRect firstRect = [self firstRectForRangeInternal:range];
return [self convertRect:firstRect toView:[self textInputView]];
}
(In the above context, self is a class that implements UITextInput).
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.
Putting the below method, adjustAutocorrectPromptView in layoutSubviews worked for me in portrait and landscape. I have a category that provides the bottom and top methods on view but you get the idea.
NSArray * subviewsWithDescription(UIView *view, NSString *description)
{
return [view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"class.description == '%#'", description]]];
}
- (void) adjustAutocorrectPromptView;
{
UIView *autocorrectPromptView = [subviewsWithDescription(self, #"UIAutocorrectInlinePrompt") lastObject];
if (! autocorrectPromptView)
{
return;
}
UIView *correctionShadowView = [subviewsWithDescription(autocorrectPromptView, #"UIAutocorrectShadowView") lastObject];
if (! correctionShadowView)
{
return;
}
UIView *typedTextView = nil; //[view typedTextView];
UIView *correctionView = nil; //[view correctionView];
for (UIView *subview in subviewsWithDescription(autocorrectPromptView, #"UIAutocorrectTextView"))
{
if (CGRectContainsRect(correctionShadowView.frame,subview.frame))
{
correctionView = subview;
}
else
{
typedTextView = subview;
}
}
if (correctionView && typedTextView)
{
if (typedTextView.top < correctionView.top)
{
correctionView.bottom = typedTextView.top;
correctionShadowView.center = correctionView.center;
}
}
}
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;
}
Related
I'm trying to create a custom keyboard for a UITextField, the background of this inputView should be transparent, I have set the background color in the view's xib file to "clear color". It is working great on iOS 6 and earlier.. but on iOS 7 it not working
Any idea how can I make it work? I want it to be fully transparent
This will set the backdrops opacity to zero when displaying your custom keyboard and reset it back to 1 when the normal keyboard is shown.
+ (void)updateKeyboardBackground {
UIView *peripheralHostView = [[[[[UIApplication sharedApplication] windows] lastObject] subviews] lastObject];
UIView *backdropView;
CustomKeyboard *customKeyboard;
if ([peripheralHostView isKindOfClass:NSClassFromString(#"UIPeripheralHostView")]) {
for (UIView *view in [peripheralHostView subviews]) {
if ([view isKindOfClass:[CustomKeyboard class]]) {
customKeyboard = (CustomKeyboard *)view;
} else if ([view isKindOfClass:NSClassFromString(#"UIKBInputBackdropView")]) {
backdropView = view;
}
}
}
if (customKeyboard && backdropView) {
[[backdropView layer] setOpacity:0];
} else if (backdropView) {
[[backdropView layer] setOpacity:1];
}
}
+ (void)keyboardWillShow {
[self performSelector:#selector(updateKeyboardBackground) withObject:nil afterDelay:0];
}
+ (void)load {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(keyboardWillShow) name:UIKeyboardWillShowNotification object:nil];
}
I am chasing the same issue, as I have a numeric keypad which fills only the left half of the screen in landscape mode (and is basically unusable on iOS7 where the blur effect covers the entire width of the screen). I haven't quite figured out how to get what I want (blurred background only behind my actual inputView), but I have figured out how to disable the blur entirely:
Define a custom subclass of UIView and specify that in your xib file
In this class override willMoveToSuperview: as follows
- (void)willMoveToSuperview:(UIView *)newSuperview
{
if (UIDevice.currentDevice.systemVersion.floatValue >= 7 &&
newSuperview != nil)
{
CALayer *layer = newSuperview.layer;
NSArray *subls = layer.sublayers;
CALayer *blurLayer = [subls objectAtIndex:0];
[blurLayer setOpacity:0];
}
}
This appears to impact the background of every custom inputView I have (but not the system keyboard) so you might need to save/restore whatever the normal opacity value is when your inputView gets removed from the superview if you don't want that.
iOS 7 is doing some things under the hood that are not documented. However, you can examine the view hierarchy and adjust the relevant views by overriding -willMoveToSuperview in your custom input view. For instance, this code will make the backdrop transparent:
- (void)willMoveToSuperview:(UIView *)newSuperview {
NSLog(#"will move to superview of class: %# with sibling views: %#", [newSuperview class], newSuperview.subviews);
if ([newSuperview isKindOfClass:NSClassFromString(#"UIPeripheralHostView")]) {
UIView* aSiblingView;
for (aSiblingView in newSuperview.subviews) {
if ([aSiblingView isKindOfClass:NSClassFromString(#"UIKBInputBackdropView")]) {
aSiblingView.alpha = 0.0;
}
}
}
}
I want to create UIScrollView with scrolling buttons.So when user press left arrow button, scroll must scroll properly.
The issue is: when I click button 3 times quickly scroll can't scroll properly (because of many calls of scrollRectToVisible). May be I can stop current animation before next animation?
P.S. If I set [self scrollScrollViewToIndex:index animated:NO] everything works properly, but I need animation
Here is my code:
- (void)scrollScrollViewToIndex:(int)index animated:(BOOL)animated
{
NSLog(#"scrolled to index: %d", index);
CGFloat offsetX = CGRectGetWidth(_scrollMain.frame) * index;
CGRect scrollRect = CGRectMake(offsetX, 0, CGRectGetWidth(_scrollMain.frame), CGRectGetHeight(_scrollMain.frame));
[_scrollMain scrollRectToVisible:scrollRect animated:animated];
// [self.scrollMain setContentOffset:CGPointMake(offsetX, 0) animated:animated];
}
- (IBAction)leftArrowPressed:(id)sender
{
int indexOfVoucher = [_arrayVouchers indexOfObject:_voucher];
indexOfVoucher--;
self.voucher = [_arrayVouchers objectAtIndex:indexOfVoucher];
[self updateViewWithVoucherWithScrolling:YES];
}
- (IBAction)rightArrowPressed:(id)sender
{
int indexOfVoucher = [_arrayVouchers indexOfObject:_voucher];
indexOfVoucher++;
self.voucher = [_arrayVouchers objectAtIndex:indexOfVoucher];
[self updateViewWithVoucherWithScrolling:YES];
}
- (void)updateViewWithVoucherWithScrolling:(BOOL)withScrolling
{
int indexOfVoucher = [_arrayVouchers indexOfObject:_voucher];
_leftArrowButton.hidden = _rightArrowButton.hidden = NO;
if (indexOfVoucher == 0)
{
_leftArrowButton.hidden = YES;
}
else if (indexOfVoucher == [_arrayVouchers count] - 1)
{
self.rightArrowButton.hidden = YES;
}
if (withScrolling)
{
[self scrollScrollViewToIndex:indexOfVoucher animated:YES];
}
}
update:
working code according to Mar0ux's advice
- (void)scrollScrollViewToIndex:(int)index animated:(BOOL)animated
{
NSLog(#"scrolled to index: %d", index);
CGFloat offsetX = CGRectGetWidth(_scrollMain.frame) * index;
if (animated)
{
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState //Multiple options
animations:^ {
// [self.scrollMain setContentOffset:CGPointMake(offsetX, 0) animated:NO];
CGRect scrollRect = CGRectMake(offsetX, 0, CGRectGetWidth(_scrollMain.frame), CGRectGetHeight(_scrollMain.frame));
[_scrollMain scrollRectToVisible:scrollRect animated:NO];
}
completion:^ (BOOL finished) {
}];
}
else
{
CGRect scrollRect = CGRectMake(offsetX, 0, CGRectGetWidth(_scrollMain.frame), CGRectGetHeight(_scrollMain.frame));
[_scrollMain scrollRectToVisible:scrollRect animated:NO];
}
}
You can always animate the contentOffset property yourself and use UIViewAnimationOptionBeginFromCurrentState option. As soon as the second animation begins, the first will end, and by using current state option, the second animation will start from where the first left off.
A few suggestions:
1) do you really want the user hammering on the button while its scrolling? If so then I suggest that your UI design may need redesign.
2) when you perturb the UI in an action method, its best to post other UI actions by dispatching a block with the code to the main queue - the button hiliting will look better.
3) in your specific case, you could in the action method disable the button, then re-enable it when the scrolling has stopped.
in my table I use cells with UITextField as subview. I also can edit it but only for the upper cells in the table I also see, what I edit, because the keyboard hides the lower cells.
Now what must I do, to scroll the cell I whant to edit move to the top?
I tried
selectRowAtIndexPath:indexPathOfCurrrentCell animated:NO scrollPostion:UITableViewSchrollPositionTop
and
scrollToRowAtIndexPath:idexPathOfCurrentCell atScrollPosition:UITableViewSchrollPositionTop animated:NO
but none of it works. Must I use an other command or add something additional? What must I change?
Thanks
If you can't manually scroll the tableView to somewhere where the cell is visible; the code won't either.
The solution is to set the frame of the tableView to have a height that respects the keyboard's height of 170 points.
You could try something like this:
tableView.frame = CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y, tableView.frame.size.width, tableView.frame.size.height-170);
Do this in your method which gets called when the textField becomes the first responder.
Take a look at this post. I think you'll find what you need.
Get UITableView to scroll to the selected UITextField and Avoid Being Hidden by Keyboard
Use Delegate Method of UITextField
Even I encountered this scrolling problem and i use the below logic to get rid of this.
This Works fine
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
if (textField==textFieldOfYourInterest )
{
CGRect frame = self.view.frame;
if ([textField isFirstResponder] && self.view.frame.origin.y >= 0)
{
frame.origin.y -= 70;
}
else if (![textField isFirstResponder] && self.view.frame.origin.y < 0)
{
frame.origin.y += 70;
}
[self.view setFrame:frame];
}
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
CGRect frame = self.view.frame;
if (![topTextFieldName isFirstResponder] && self.view.frame.origin.y < 0)
{
frame.origin.y += 70;
}
[self.view setFrame:frame];
NSLog(#"text field should return");
[textField resignFirstResponder];
return YES;
}
If any problem revert to me.
Thanks
Nagaraja Sathe
A UIScrollView contains several UIView objects; how can I tell if a point on the screen not generated by touches is within a specific subview of the scrollview? so far atempts to determine if the point is in the subview always return the first subview in the subviews array of the parent scrollview, ie the coordinates are relative to the scrollview, not the window.
Here's what I tried (edited)
-(UIView *)viewVisibleInScrollView
{
CGPoint point = CGPointMake(512, 384);
for (UIView *myView in theScrollView.subviews)
{
if(CGRectContainsPoint([myView frame], point))
{
NSLog(#"In View");
return myView;
}
}
return nil;
}
It looks like you're point is relative to the window, and you want it relative to the current view. convertPoint:fromView: should help with this.
There are probably errors here, but it should look something like this:
-(UIView *)viewVisibleInScrollView
{
CGPoint point = CGPointMake(512, 384);
CGPoint relativePoint = [theScrollView convertPoint:point fromView:nil]; // Using nil converts from the window coordinates.
for (UIView *myView in theScrollView.subviews)
{
if(CGRectContainsPoint([myView frame], relativePoint))
{
NSLog(#"In View");
return myView;
}
}
return nil;
}
Here's what I do:
#implementation UIScrollView (FOO)
- (id)foo_subviewAtPoint:(CGPoint)point
{
point.x += self.contentOffset.x;
for (UIView *subview in self.subviews) {
if (CGRectContainsPoint(subview.frame, point)) {
return subview;
}
}
return nil;
}
#end
And here's how I use it:
CGPoint center = [scrollView convertPoint:scrollView.center fromView:scrollView.superview];
UIView *view = [scrollView foo_subviewAtPoint:center];
Some things to note:
The point passed to foo_subviewAtPoint: is expressed in the coordinate system of the scroll view. This is why in the code above I have to convert center to that coordinate system from that of its superview.
I'm using iOS 6.1+ with layout constraints. I've never tested this code with anything else, so YMMV.
Is there a way to control the position of the auto correct view that pops up while typing in a UITextField?
By default it appears to always appear below the text field. However in Apple's apps like SMS it sometimes appears above the text field.
For text fields aligned right above the keyboard the auto correct is blocked by the keyboard and not usable.
The position of the auto correction prompt is determined by the firstRect... method of the UITextInput protocol. Unfortunately UITextField uses a private class as delegate for this protocol so you cannot subclass and override it.
You COULD however implement your own UITextField replacement by drawing the contents yourself (like with CoreText), implement your own selection and loupe handling and then you would be able to affect the position of the auto correction prompt. Though it's designed to always be below the edited text, so you would have to essentially lie on the firstRect ... method.
Long story short: it's too much hassle.
One answer it worked for me is to use setInputAccessoryView method of the textview/textview.
I have a toolbar which contains the textView.
If I set the toolbar as the inputaccessoryview of the textfield, and set to NO clipsToBound property of the toolbar, I don't know exactly why, but the balloon appears above the keyboard
Here is a solution using private APIs as there are no ways to do this using public APIs.
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.
Using swizzled layoutSubViews,
- (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;
}
}
}
}
}
}
For more details check.
In my case, the AutoCorrect view's position is shifted because of the font's leading (line gap).
So, I tried to move it up the leading px by using firstRect function as the code below.
class CustomTextView: UITextView {
override var font: UIFont? {
didSet {
if let font = font {
leadingFont = font.leading
} else {
leadingFont = 0
}
}
}
var leadingFont: CGFloat = 0
override func firstRect(for range: UITextRange) -> CGRect {
var newRect = super.firstRect(for: range)
newRect.origin = CGPoint(x: newRect.origin.x, y: newRect.origin.y - leadingFont)
print("newRect: \(newRect)")
return newRect
}
}
Although it's UITextView but you can do the same thing with UITextField