I am having some issues with some animation stuff in an iPad application. I have four UIButtons that are on the screen when the app launches.
I basically want the app to load and the four buttons to animate into view one at a time from the top of the screen into place.
I have the following which kind of animates it but I am struggling with it.
-(void)viewWillAppear:(BOOL)animated
{
CGPoint newLeftCenter = CGPointMake( 15.0f + myCocoButton.frame.size.width / 2.0f, myCocoButton.center.y);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:4.0f];
myCocoButton.center = newLeftCenter;
[UIView commitAnimations];
}
The current code that I have does animate the button but not in the way I want. I can't get my head around how to actually place it exactly where I want it.
In your storyboard, lay out your buttons in their final positions.
In viewWillAppear:, save the location of each button and move the button off-screen:
#implementation MyViewController {
CGPoint _button0TrueCenter;
CGPoint _button1TrueCenter;
CGPoint _button2TrueCenter;
CGPoint _button3TrueCenter;
}
static void moveButtonAndSaveCenter(UIButton *button, CGPoint offscreenCenter, CGPoint *trueCenter) {
*trueCenter = button.center;
button.center = offscreenCenter;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (animated) {
moveButtonAndSaveCenter(self.button0, CGPointMake(-100, 100), &_button0TrueCenter);
moveButtonAndSaveCenter(self.button1, CGPointMake(420, 100), &_button1TrueCenter);
moveButtonAndSaveCenter(self.button2, CGPointMake(-100, 200), &_button2TrueCenter);
moveButtonAndSaveCenter(self.button3, CGPointMake(420, 200), &_button3TrueCenter);
}
}
Then in viewDidAppear:, animate them back to their original locations:
static void animateButton(UIButton *button, CGPoint newCenter, NSTimeInterval delay) {
[UIView animateWithDuration:.25 delay:delay options:0 animations:^{
button.center = newCenter;
} completion:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (animated) {
animateButton(self.button0, _button0TrueCenter, 0);
animateButton(self.button1, _button1TrueCenter, 0.2);
animateButton(self.button2, _button2TrueCenter, 0.4);
animateButton(self.button3, _button3TrueCenter, 0.6);
}
}
Assuming you have your buttons in an array, you could do something like this (your items should be in the correct end positions in the nib)
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGAffineTransform transform = CGAffineTransformMakeTranslation(0.f, -200.f);
[self.buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger idx, BOOL *stop) {
button.transform = transform; // This translates the view's off the top of the screen
[UIView animateWithDuration:0.25f
delay:0.25f * idx
options:UIViewAnimationCurveEaseOut // No point doing easeIn as the objects are offscreen anyway
animations:^{
button.transform = CGAffineTransformIdentity;
} completion:nil];
}];
}
This will animate every time. You may need to add an ivar to stop this happening and just set it to YES after this has run once. The problem I found when I was just messing around is that when it was calling viewDidAppear: animated was NO on first load, so I couldn't use that BOOL as a check
I've written a blog post about this subject: http://www.roostersoftstudios.com/2011/07/01/iphone-dev-performing-sequential-uiview-animations/
and I have a sample project there.
You can just chain the animations where when one finishes it calls the function of the next animation. Can you be more specific about the exact issues you are having?
Related
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.
So, right now I have a rocket which bounces side to side across the bottom of the iphone 5 screen. I want the rocket however to stop moving side to side but to go upwards once the user touches the screen (so touching the screen makes the rocket take off).
First, in viewDidAppear I told the compiler to run the spawnRocket method after 2 seconds.
In the spawnRocket method I established the rockets image and frame, and then I added it to the view and performed a uiview animation sending the rocket to the right side of the screen. In the finished parameter in the uiview animation method I told the compiler to run the moveRocketLeft method.
In the moveRocketLeft method I did an animation which would send the rocket to the left of the screen, and in the finished parameter I ran the moveRocketRight method.
The moveRocketMethod is basically the spawnRocket method except I don't establish the rockets image and frame and add it to the view.
So after all this, I tried to implemented the -touchesBegan:withEvent: method. I tried simply running a uiview animation which changed the rockets y to off the screen upwards, and the x to whatever the x was currently at when the user touched the screen.
However, I realize that the calling the rockets frame does not return the location that the rocket looks like while animating. It really just returns what the rockets frame will be when its done animating. So, to get the frame I want should I call layer? I remember reading somewhere that layer will return the actual location of a uiimageview during an animation.
But, layer only has the property bounds and not frame, so I'm a little confused.
I also tried calling [self.view.layer removeAllAnimations]; and then running a new animation which would shoot the awkward up, but the removeAllAnimations method never worked (and anyway i don't really like the idea of using that because i heard it lags).
So anyone have any idea how i can implement touchesBegan:withEvent: method so that the rocket can take off correctly? (or if you think I have to change my whole program please feel free to help)
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property UIImageView *rocket;
#end
#implementation ViewController
#synthesize rocket=_rocket;
- (void)viewDidLoad
{
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self.view.layer removeAllAnimations];
// [UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^(){self.rocket.frame=CGRectMake(self.rocket.frame.origin.x, -40, 25, 40);} completion:^(BOOL finished){}];
// this didn't work :(
}
-(void)viewDidAppear:(BOOL)animated{
[self performSelector:#selector(spawnRocket) withObject:self afterDelay:2];
}
-(void)spawnRocket{
self.rocket=[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"default.png"]]; //places imageview right off screen
self.rocket.frame=CGRectMake(-25, 420, 25, 40);
[self.view addSubview:self.rocket];
[UIView animateWithDuration:1.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^(){self.rocket.frame=CGRectMake(295, 420, 25, 40);} completion:^(BOOL finished){if (finished)[self moveRocketLeft]; }];
}
-(void) moveRocketLeft{
[UIView animateWithDuration:1.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^(){self.rocket.frame=CGRectMake(0, 420, 25, 40);} completion:^(BOOL finished){if (finished)[self moveRocketRight];}];
}
-(void)moveRocketRight{
[UIView animateWithDuration:1.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^(){self.rocket.frame=CGRectMake(295, 420, 25, 40);} completion:^(BOOL finished){if (finished)[self moveRocketLeft];}];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
This should do the trick
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
CALayer *rocketPresentationLayer = [self.rocket.layer presentationLayer];
self.rocket.layer.position = rocketPresentationLayer.position;
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionBeginFromCurrentState animations:^(){self.rocket.frame=CGRectMake(self.rocket.frame.origin.x, -40, 25, 40);} completion:^(BOOL finished){}];
}
Note 1: I didn't put logic in here to check whether the user tapped the rocket again, when it is taking off vertically. You might want to add a BOOL or something to see if the user successfully hit the rocket or not, and if so, don't perform the vertical animation again.
Note 2: If in future you only want this to occur when the rocket is hit, you can use hitTest checking to call the animation conditionally.
// Get the touch
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:self.view];
// See if we hit the rocket
CALayer *rocketPresentationLayer = [self.rocket.layer presentationLayer];
CALayer* hitTest = [rocketPresentationLayer hitTest:touchPoint];
// If we did, then stop animation and move upwards
if (hitTest) {
....
}
P.S: Also as a slight mention, #property UIImageView *rocket; should be changed to #property (nonatomic) UIImageView *rocket;. This is primarily for performance reasons, as I don't expect your rocket to be accessed by multiple threads (it shouldn't, it's the UI!).
I am using the EGORefreshTableHeaderView [1] to fetch new data from a server into a UITableView.
This works pretty good but in iOS 5.1 the EGORefreshTableHeaderView does not scroll back into the intended height when the user releases the pull down. Normally it should scroll back to an contentInset of 60px. Then the loading view should be visible for the time which the loading process takes and after that scroll back to 0px inset.
The first scroll-back should happen in the egoRefreshScrollViewDidEndDragging:scrollView method.
- (void)egoRefreshScrollViewDidEndDragging:(UIScrollView *)scrollView {
BOOL _loading = NO;
if ([_delegate respondsToSelector:#selector(egoRefreshTableHeaderDataSourceIsLoading:)]) {
_loading = [_delegate egoRefreshTableHeaderDataSourceIsLoading:self];
}
if (scrollView.contentOffset.y <= - 65.0f && !_loading) {
if ([_delegate respondsToSelector:#selector(egoRefreshTableHeaderDidTriggerRefresh:)]) {
[_delegate egoRefreshTableHeaderDidTriggerRefresh:self];
}
[self setState:EGOOPullRefreshLoading];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.2];
scrollView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f, 0.0f);
[UIView commitAnimations];
//I've also tried it with block animations! But doesn't work!
/*[UIView animateWithDuration:0.2 animations:^{
scrollView.contentInset = UIEdgeInsetsMake(60.0f, 0.0f, 0.0f, 0.0f);
}];*/
}
}
The problem is that when a user releases the scroll view on the half of the screen (shown in the screenshot below), the scrollview does not bounce back into the 60px inset where it should reload the data.
My first idea was that it is because of the animations. So I changed it to block animations but nothing changes. I guess the problem is that the animations are not executed on commitAnimations rather at the end of the loading.
Does anyone have a solution for this?
[1]... https://github.com/enormego/EGOTableViewPullRefresh
I would pull up their demo application and follow their delegate methods.
put this inside didEndDragging:
[_delegate egoRefreshScrollViewDidEndDragging:scrollView];
Is there a way to change the speed of the animation when scrolling a UITableView using setContentOffset:animated:? I want to scroll it to the top, but slowly. When I try the following, it causes the bottom few cells to disappear before the animation starts (specifically, the ones that won't be visible when the scroll is done):
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:3.0];
[self.tableView setContentOffset:CGPointMake(0, 0)];
[UIView commitAnimations];
Any other way around this problem? There is a private method _setContentOffsetAnimationDuration that works, but I don't want to be rejected from the app store.
[UIView animateWithDuration:2.0 animations:^{
scrollView.contentOffset = CGPointMake(x, y);
}];
It works.
Setting the content offset directly did not work for me. However, wrapping setContentOffset(offset, animated: false) inside an animation block did the trick.
UIView.animate(withDuration: 0.5, animations: {
self.tableView.setContentOffset(
CGPoint(x: 0, y: yOffset), animated: false)
})
I've taken nacho4d's answer and implemented the code, so I thought it would be helpful for other people coming to this question to see working code:
I added member variables to my class:
CGPoint startOffset;
CGPoint destinationOffset;
NSDate *startTime;
NSTimer *timer;
and properties:
#property (nonatomic, retain) NSDate *startTime;
#property (nonatomic, retain) NSTimer *timer;
and a timer callback:
- (void) animateScroll:(NSTimer *)timerParam
{
const NSTimeInterval duration = 0.2;
NSTimeInterval timeRunning = -[startTime timeIntervalSinceNow];
if (timeRunning >= duration)
{
[self setContentOffset:destinationOffset animated:NO];
[timer invalidate];
timer = nil;
return;
}
CGPoint offset = [self contentOffset];
offset.x = startOffset.x +
(destinationOffset.x - startOffset.x) * timeRunning / duration;
[self setContentOffset:offset animated:NO];
}
then:
- (void) doAnimatedScrollTo:(CGPoint)offset
{
self.startTime = [NSDate date];
startOffset = self.contentOffset;
destinationOffset = offset;
if (!timer)
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:#selector(animateScroll:)
userInfo:nil
repeats:YES];
}
}
you'd also need timer cleanup in the dealloc method. Since the timer will retain a reference to the target (self) and self has a reference to the timer, some cleanup code to cancel/destroy the timer in viewWillDisappear is likely to be a good idea too.
Any comments on the above or suggestions for improvement would be most welcome, but it is working very well with me, and solves other issues I was having with setContentOffset:animated:.
There is no a direct way of doing this, nor doing the way you wrote it. The only way I can accomplish this is by making the movement/animation by my own.
For example move 1px every 1/10 second should simulate a very slow scroll animation. (Since its a linear animation maths are very easy!)
If you want to get more realistic or fancy and simulate easy-in easy-off effect then you need some maths to calculate a bezier path so you can know the exact position at every 1/10 second, for example
At least the first approach shouldn't be that difficult.
Just use or -performSelector:withObject:afterDelay or NSTimerswith
-[UIScrollView setContentOffset:(CGPoint*)];`
Hope it helps
UIView calculates final view and then animates it. That's why cells that invisible on finish of animation invisible on start too. For prevent this needed add layoutIfNeeded in animation block:
[UIView animateWithDuration:2.0 animations:^{
[self.tableView setContentOffset:CGPointMake(0, 0)];
[self.tableView layoutIfNeeded]
}];
Swift version:
UIView.animate(withDuration: 2) {
self.tableView.contentOffset.y = 10
self.tableView.layoutIfNeeded()
}
I'm curious as to whether you found a solution to your problem. My first idea was to use an animateWithDuration:animations: call with a block setting the contentOffset:
[UIView animateWithDuration:2.0 animations:^{
scrollView.contentOffset = CGPointMake(x, y);
}];
Side effects
Although this works for simple examples, it also has very unwanted side effects. Contrary to the setContentOffset:animated: everything you do in delegate methods also gets animated, like the scrollViewDidScroll: delegate method.
I'm scrolling through a tiled scrollview with reusable tiles. This gets checked in the scrollViewDidScroll:. When they do get reused, they get a new position in the scroll view, but that gets animated, so there are tiles animating all the way through the view. Looks cool, yet utterly useless. Another unwanted side effect is that possible hit testing of the tiles and my scroll view's bounds is instantly rendered useless because the contentOffset is already at a new position as soon as the animation block executes. This makes stuff appear and disappear while they're still visible, as to where they used to be toggled just outside of the scroll view's bounds.
With setContentOffset:animated: this is all not the case. Looks like UIScrollView is not using the same technique internally.
Is there anyone with another suggestion for changing the speed/duration of the UIScrollView setContentOffset:animated: execution?
You can set the duration as follows:
scrollView.setValue(5.0, forKeyPath: "contentOffsetAnimationDuration") scrollView.setContentOffset(CGPoint(x: 100, y: 0), animated: true)
This will also allow you to get all of your regular delegate callbacks.
https://github.com/dominikhofmann/PRTween
subclass UITableview
#import "PRTween.h"
#interface JPTableView : UITableView{
PRTweenOperation *activeTweenOperation;
}
- (void) doAnimatedScrollTo:(CGPoint)destinationOffset
{
CGPoint offset = [self contentOffset];
activeTweenOperation = [PRTweenCGPointLerp lerp:self property:#"contentOffset" from:offset to:destinationOffset duration:1.5];
}
IF all your trying to do is scroll your scrollview I think you should use scroll rect to visible. I just tried out this code
[UIView animateWithDuration:.7
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect scrollToFrame = CGRectMake(0, slide.frame.origin.y, slide.frame.size.width, slide.frame.size.height + kPaddingFromTop*2);
CGRect visibleFrame = CGRectMake(0, scrollView.contentOffset.y,
scrollView.frame.size.width, scrollView.frame.size.height);
if(!CGRectContainsRect(visibleFrame, slide.frame))
[self.scrollView scrollRectToVisible:scrollToFrame animated:FALSE];}];
and it scrolls the scrollview to the location i need for whatever duration i am setting it for. The key is setting animate to false. When it was set to true, the animation speed was the default value set by the method
For people who also have issues with disappearing items while scrolling a UITableView or a UICollectionView you can expand the view itself so that we hold more visible items. This solution is not recommended for situations where you need to scroll a great distance or in situations where the user can cancel the animation. In the app I'm currently working on I only needed to let the view scroll a fixed 100px.
NSInteger scrollTo = 100;
CGRect frame = self.collectionView.frame;
frame.size.height += scrollTo;
[self.collectionView setFrame:frame];
[UIView animateWithDuration:0.8 delay:0.0 options:(UIViewAnimationOptionCurveEaseIn) animations:^{
[self.collectionView setContentOffset:CGPointMake(0, scrollTo)];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.8 delay:0.0 options:(UIViewAnimationOptionCurveEaseIn) animations:^{
[self.collectionView setContentOffset:CGPointMake(0, 0)];
} completion:^(BOOL finished) {
CGRect frame = self.collectionView.frame;
frame.size.height -= scrollTo;
[self.collectionView setFrame:frame];
}];
}];
I use transitionWithView:duration:options:animations:completion:
[UIView transitionWithView:scrollView duration:3 options:(UIViewAnimationOptionCurveLinear) animations:^{
transitionWithView:scrollView.contentOffset = CGPointMake(contentOffsetWidth, 0);
} completion:nil];
UIViewAnimationOptionCurveLinear is an option make animation to occur evenly over.
While I found that in an animation duration, the delegate method scrollViewDidScroll did not called until animation finished.
You can simply use block based animation to animate the speed of scrollview.
First calculate the offset point to which you want to scroll and then simply pass that offset value as here.....
[UIView animateWithDuration:1.2
delay:0.02
options:UIViewAnimationCurveLinear
animations:^{
[colorPaletteScrollView setContentOffset: offset ];
}
completion:^(BOOL finished)
{ NSLog(#"animate");
} ];
here colorPaletteScrollView is my custom scrollview and offset is the value passed .
this code works perfectly fine for me.
Is there a reason you're using setContentOffset and not scrollRectToVisible:animated:?
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated
I would recommend doing it like this:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:3.0];
[self.tableView scrollRectToVisible:CGRectMake(0, 0, 320, 0) animated:NO];
[UIView commitAnimations];
Unless that doesnt work. I still think you should try it.
Actually TK189's answer is partially correct.
To achieve a custom duration animated contentOffset change, with proper cell reuse by UITableView and UICollectionView components, you just have to add a layoutIfNeeded call inside the animations block:
[UIView animateWithDuration:2.0 animations:^{
tableView.contentOffset = CGPointMake(x, y);
[tableView layoutIfNeeded];
}];
On Xcode 7.1 - Swift 2.0 :
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
dispatch_async(dispatch_get_main_queue()) {
UIView.animateWithDuration(0, animations: { self.scrollView!.setContentOffset(CGPointZero,animated: true) })
}
return true
}
OR
func textFieldShouldReturn(textField: UITextField) -> Bool {
if(textField.returnKeyType == UIReturnKeyType.Next) {
password.becomeFirstResponder()
}
dispatch_async(dispatch_get_main_queue()) {
UIView.animateWithDuration(0, animations: { self.scrollView!.setContentOffset(CGPointZero,animated: true) })
}
textField.resignFirstResponder()
return true
}
Note: self.scrollView!.setContentOffset(CGPointZero,animated: true) can have different positions depending on the requirement
Example:
let scrollPoint:CGPoint = CGPointMake(0,textField.frame.origin.y/2);
scrollView!.setContentOffset(scrollPoint, animated: true);
I wanted to change the contentOffSet of tableview when textfield begins to edit.
Swift 3.0
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
UIView.animate(withDuration: 0, animations: {
self.sampleTableView.contentOffset = CGPoint(x: 0, y: 0 - (self.sampleTableView.contentInset.top - 200 ))
})
}
}
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;
}