UITableView scroll smooth with certain speed? - iphone

I'm building a custom slot machine with a column that exists of a uitableview.
When the user pulls a lever the tableview should scroll to a certain position with an index.
I used the method:
- scrollToRowAtIndexPath:atScrollPosition:animated:
But this method will make the table scroll with a constant duration. So you will not really recognize a long or short spin.
I'm looking for a way to:
A) Slow down the scroll animation. Or,
B) Change the duration for the scroll animation to a self defined value.
The normal scroll animation (with the finger) does show this effect.
Maybe it is a stupid idea, but is it an idea to invoke a touchesBegan and touchesDidEnd method on my tableView?
Thanks already

May need to look in that direction?
[UIView animateWithDuration: 1.0
animations: ^{
[tableViewExercises scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:previousSelectedExerciseCell inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}completion: ^(BOOL finished){
}
];
Work only with animated:NO.

Because a UITableView inherits from UIScrollView you might also use setContentOffset:animated:
This way you can make your tableview "scroll" a certain amount of pixels of your choosing to any side you like.
This can be done the same with the scrollToRowAtIndexPath:atScrollPosition:animated:
I made a prototype just to show you how it works.
Because this is done with timers and stuff you can set how long the autoScroll will last and how fast (and how far if you're using the contentoffset) the animation will go.
This is the .h file:
#import <UIKit/UIKit.h>
#interface AutomaticTableViewScrollViewController : UIViewController <UITableViewDelegate,UITableViewDataSource>
{
UITableView *slotMachine;
NSMutableArray *listOfItems;
NSTimer *tableTimer;
}
#property (nonatomic,retain) UITableView *slotmachine;
#property (nonatomic,retain) NSMutableArray *listOfItems;
#property (nonatomic,retain) NSTimer *tableTimer;
-(void)automaticScroll;
-(void)stopscroll;
#end
This is the .m file:
#import "AutomaticTableViewScrollViewController.h"
#implementation AutomaticTableViewScrollViewController
#synthesize slotmachine;
#synthesize listOfItems;
#synthesize tableTimer;
-(void)loadView
{
[super loadView];
slotmachine = [[UITableView alloc] initWithFrame:self.view.frame style:UITableViewStylePlain];
slotmachine.delegate = self;
slotmachine.dataSource = self;
[self.view addSubview:slotmachine];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Set up the cell...
if (indexPath.row % 2 == 0)
{
cell.textLabel.text = #"blalala";
}
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 99999;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//you might want to do this action in ur buttonTargetMethod
//start timers
tableTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 //this value arranges the speed of the autoScroll
target:self
selector:#selector(automaticScroll)
userInfo:nil
repeats:YES];
[NSTimer scheduledTimerWithTimeInterval:5 //this arranges the duration of the scroll
target:self
selector:#selector(stopscroll)
userInfo:nil
repeats:NO];
}
-(void)automaticScroll
{
[slotmachine setContentOffset:CGPointMake(slotmachine.contentOffset.x, slotmachine.contentOffset.y + 50) animated:YES]; //the 50 stands for the amount of movement every tick the timer will make
}
-(void)stopscroll
{
//stop tabletimer again
[tableTimer invalidate];
}
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return YES;
}
#end
If you have any question feel free to leave a comment and I will elaborate.

If you can require iOS 5, you could use the UIScrollViewDelegate method scrollViewWillEndDragging:withVelocity:targetContentOffset:.
This allows you to see, how fast the user was moving the finger, where the deceleration animation would end with the default speed, and it allows you to override the animation speed, so that it ends at a different point.

the common way to make apps-slot-machines is with UIPickerView maybe you should check this..

How about setup a timer and then call the scroll when timer fires:
start_index = 0;
dest_index = 20;
timer = [NSTimer scheduledTimerWithTimeInterval:(0.1) target:self selector:#selector(rolling) userInfo:nil repeats:YES];
- (void)rolling {
start_index++;
if (start_index < dest_index) {
NSIndexPath *Index = [NSIndexPath indexPathForRow:start_index inSection:0];
[self.tableView scrollToRowAtIndexPath:Index atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
} else {
[timer invalidate];
}
}

I've been using the Sparrow framework's tweening features to do similar animations.
The link above has an example of how to set it up. You can animate any numeric property of any Objective C object, and you can use transitions like "easy in", "ease out", "ease in elastic", etc., or just good old linear animations.
The property contentSize is a CGPoint though, so you would need actually animate a different property on one of your classes and then implement an actual method for the property setter function so that it updates the contentOffset.
- (void) setTableScrollPosition:(CGFloat)position
{
CGPoint newOffset = CGPointMake(0.0, position);
scrollView.contentOffset = newOffset;
}

I searched a lot for this answer but ultimately had to come up with one of my own.
You can call the method scrollAutomatically with the starting row number like:
[self scrollAutomatically:0];
So this is what the function looked like. I was dealing with a table which always had 3000 rows and I intended to scroll to the bottom.
- (void) scrollAutomatically:(int) i
{
__block int j = i;
[UIView animateWithDuration: 0//Change this to something more for slower scrolls
animations: ^{
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:j inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:NO];
}
completion: ^(BOOL finished){
j = j + 10;//Changing this number affects speed.
if(j<=2999)//here you could provide the index of destination row
[self scrollAutomatically:j];
else
{
//I had some code here that popped up a UIAlertView.
}
}];
}
Now coming to the speed of the scroll.
FOR REALLY FAST SCROLLS:
If I set value by which I increment the row index (j) on each call of the function to 10,
i.e. if I write j = j+10; then my 3000 rows took about 9 seconds to scroll. (3000 * mean FPS that I could muster).
If I set it to j = j+20; then the 3000 rows took about 4.5 seconds. So you get the idea.
To make it scroll slower reduce the increment value.
FOR SLOW, READABLE SCROLLS:
[UIView animateWithDuration: 1.5//this will directly set your speed. n rows scrolled every 1.5 seconds.
NOTE: If you change frames of CALayers or Views (eg. a customView you may have added to the contentView of your tableViewCell), then those animations will start to bother you here. For a large animation duration, they will be very visible and you may see strange cell beahviour.
In that case wherever you change your frames etc. look at something like:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[myView.myCALayer.frame = (CGRect){ { 10, 10 }, { 100, 100 } ];
[CATransaction commit];
Find the above solution here.
You may also have to set the actions dictionary for the layer to return nulls for all actions.
NSMutableDictionary *newActions = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSNull null], #"sublayers", nil];
superlayer.actions = newActions;
This seems to be too late but for people facing the same problem I hope this answer will be helpful. Also please feel free to guide me in case I have made some obvious (or not so much) blunder.
Edit: Oops, I see the exact thing above my answer :(
Anyways, this is a bit more detailed and I am only a beginner :)

You can't (to my knowledge, I have been looking everywhere for a way to do this) make a speed that isn't constant for
- scrollToRowAtIndexPath:atScrollPosition:animated:
So I would suggest... that if you really need it, make your own out of some animation or something, or do something alot easier that would save you some time, use UIPickerView

Related

IOS Scrollview Marquee style

I have a set of 4 images that i want to show in a UIScrollView. This scroll view contains ad banners. So they have to keep scrolling vertically down automatically similar to marquee effect - user does not need to touch the screen to scroll.This process should keep repeating.
If the user touches the screen, the scrolling should stop & when user re-touches the screen the scrolling should restart from where it had left.
There is as simpler way, I guess. UIImageView can help you. Follow these Steps :
1) Add 4 UIImageViews Vertically Down.
2) Create 4 Different Arrays for these ImageViews.
NSArray *frames1 = [NSArray arrayWithObjects:[UIImage imageWithName:#"a.png"],[UIImage imageWithName:#"b.png"],[UIImage imageWithName:#"c.png"],[UIImage imageWithName:#"d.png"],nil];
NSArray *frames2 = [NSArray arrayWithObjects:[UIImage imageWithName:#"b.png"],[UIImage imageWithName:#"c.png"],[UIImage imageWithName:#"d.png"],[UIImage imageWithName:#"a.png"],nil];
NSArray *frames3 = [NSArray arrayWithObjects:[UIImage imageWithName:#"c.png"],[UIImage imageWithName:#"d.png"],[UIImage imageWithName:#"a.png"],[UIImage imageWithName:#"b.png"],nil];
NSArray *frames4 = [NSArray arrayWithObjects:[UIImage imageWithName:#"d.png"],[UIImage imageWithName:#"a.png"],[UIImage imageWithName:#"b.png"],[UIImage imageWithName:#"c.png"],nil];
3) Provide these Arrays to all these different ImageViews.
yourImageView1.animationImages = frames1;
yourImageView2.animationImages = frames2;
yourImageView3.animationImages = frames3;
yourImageView4.animationImages = frames4;
4) You can add some Effects also...
// all frames will execute in 1.75 seconds
yourImageView.animationDuration = 1.75;
// repeat the annimation forever
yourImageView.animationRepeatCount = 0;
5) Simply startAnimating the ImageViews.
[yourImageView1 startAnimating];
[yourImageView2 startAnimating];
[yourImageView3 startAnimating];
[yourImageView4 startAnimating];
6) You can always use -touchesEnded method to start and stop Animation.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if ([[touches anyObject] view] == yourImageView1) {
//Here you can write the code to stop and start Animation of UIImageView
}
}
I think this will give you the effect what you want.
GoodLuck !!!
This is how I have done it finally :-) .
Call the below method from viewDidLoad
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(scrollTheScrollView) userInfo:nil repeats:YES];
Now implement the method
-(void)scrollTheScrollView{
static int i=0;
i++;
if(self.scrollView.contentOffset.y == 700.0)//This 700.0 will be different for you
i=0;
NSLog(#"Content offset in scrolling method is %#",self.scrollView);
[self.scrollView setContentOffset:CGPointMake(0, i*5)];}
I made an API for this, if you're interested.
https://github.com/dokun1/DOMarqueeLabel

UIAlertView Rendering Error

I have been working on an app for a couple of months now, but have finally run into an issue that I can't solve myself, and can't find anything on the internet to help.
I am using several normal UIAlertViews, in my app. Some have 2 buttons, some have 3 buttons, and a couple have 2 buttons and a text field. However all have the same issue. When you call [someAlertView show]; the alert view appears as normal, but then suddenly its graphics context seems to get corrupted as you can see from the screenshot.
This happens on both iPhone and iPad simulators (both 5.0 and 5.1), and happens on an iPad and iPhone4S device as well.
The image showing through is whatever happens to be behind the alertView.
The Alert still works, I can click the buttons, type in the text field, then when it dismisses the delegate methods are called correctly and everything goes back to normal. When the alertView appears again, the same thing happens.
The view behind the alert is a custom UIScrollView subclass with a content size of approximately 4000 pixels by 1000 with a UIImage as the background. The png file is mostly transparent, so is only about 80kB in memory size, and the phone is having no issues rendering it - the scroll view is still fully responsive and not slow.
It also has a CADisplayLink timer attached to it as part of the subclass. I have tried disabling this just before the alertView is shown, but it makes no difference so I am doubtful that is the issue.
This app is a partial rewrite of one I made for a university project, and that one could display UIAlertViews over the top of a scrollView of the same size and subclass without issue. The difference between this app and that one is that in my old app, I had subclassed UIAlertView to add extra things such as a pickerView, however I decided that I didn't like the way it looked so moved everything out of the alert and am just sticking with a standard UIAlertView.
This is how the alertView in the screenshot is called:
- (IBAction)loadSimulation:(id)sender {
importAlert = [[UIAlertView alloc] initWithTitle:#"Load Simulation" message:#"Enter Filename:" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Load", nil];
[importAlert setAlertViewStyle:UIAlertViewStylePlainTextInput];
[importAlert showPausingSimulation:self.simulationView]; //Calling [importAlert show]; makes no difference.
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
[self hideOrganiser]; //Not an issue as the problem occurs on iPad as well.
}
}
With this being the categorised AlertView to add the ability to stop the scrollViews CADisplay link.
#interface UIAlertView(pauseDisplayLink)
- (void)showPausingSimulation:(UILogicSimulatorView*)simulationView;
#end
#implementation UIAlertView(pauseDisplayLink)
- (void)showPausingSimulation:(UILogicSimulatorView *)simulationView {
[simulationView stopRunning];
[simulationView removeDisplayLink]; //displayLink needs to be removed from the run loop, otherwise it will keep going in the background and get corrupted.
[self show];
}
I get no memory warnings when this happens, so I am doubtful it is due to lack of resources.
Has anyone come across an issue like this before? If you need further information I can try to provide it, but I am limited in what code I can post. Any help would be appreciated, I've been trying to solve this for two weeks and can't figure it out.
Edit:
It appears that it is not the AlertView at all (or rather it is not just the alertView), as the problem goes away when I remove the scroll view behind it, so there must be some issue between the two. This is the code for my UIScrollView subclass:
.h file:
#import
#import
#class ECSimulatorController;
#interface UILogicSimulatorView : UIScrollView {
CADisplayLink *displayLink;
NSInteger _updateRate;
ECSimulatorController* _hostName;
}
#property (nonatomic) NSInteger updateRate;
#property (nonatomic, strong) ECSimulatorController* hostName;
- (void) removeDisplayLink;
- (void) reAddDisplayLink;
- (void) displayUpdated:(CADisplayLink*)timer;
- (void) startRunning;
- (void) stopRunning;
- (void) refreshRate:(NSInteger)rate;
- (void) setHost:(id)host;
- (void)setMinimumNumberOfTouches:(NSInteger)touches;
- (void)setMaximumNumberOfTouches:(NSInteger)touches;
#end
.m file:
#import "UILogicSimulatorView.h"
#import "ECSimulatorController.h"
#import <QuartzCore/QuartzCore.h>
#implementation UILogicSimulatorView
#synthesize updateRate = _updateRate;
#synthesize hostName = _hostName;
- (void)reAddDisplayLink {
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; //allows the display link to be re-added to the run loop after having been removed.
}
- (void)removeDisplayLink {
[displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; //allows the display link to be removed from the Run loop without deleting it. Removing it is essential to prevent corruption between the games and the simulator as both use CADisplay link, and only one can be in the run loop at a given moment.
}
- (void)startRunning {
[self refreshRate:self.updateRate];
[displayLink setPaused:NO];
}
- (void)refreshRate:(NSInteger)rate {
if (rate > 59) {
rate = 59; //prevent the rate from being set too an undefined value.
}
NSInteger frameInterval = 60 - rate; //rate is the number of frames to skip. There are 60FPS, so this converts to frame interval.
[displayLink setFrameInterval:frameInterval];
}
- (void)stopRunning {
[displayLink setPaused:YES];
}
- (void)displayUpdated:(CADisplayLink*)timer {
//call the function that the snakeController host needs to update
[self.hostName updateStates];
}
- (void)setHost:(ECSimulatorController*)host;
{
self.hostName = host; //Host allows the CADisplay link to call a selector in the object which created this one.
}
- (id)initWithFrame:(CGRect)frame
{
//Locates the UIScrollView's gesture recogniser
if(self = [super initWithFrame:frame])
{
[self setMinimumNumberOfTouches:2];
displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayUpdated:)]; //CADisplayLink will update the logic gate states.
self.updateRate = 1;
[displayLink setPaused:YES];
}
return self;
}
- (void)setMinimumNumberOfTouches:(NSInteger)touches{
for (UIGestureRecognizer *gestureRecognizer in [self gestureRecognizers])
{
if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
//Changes the minimum number of touches to 'touches'. This allows the UIPanGestureRecogniser in the object which created this one to work with one finger.
[(UIPanGestureRecognizer*)gestureRecognizer setMinimumNumberOfTouches:touches];
}
}
}
- (void)setMaximumNumberOfTouches:(NSInteger)touches{
for (UIGestureRecognizer *gestureRecognizer in [self gestureRecognizers])
{
if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
//Changes the maximum number of touches to 'touches'. This allows the UIPanGestureRecogniser in the object which created this one to work with one finger.
[(UIPanGestureRecognizer*)gestureRecognizer setMaximumNumberOfTouches:touches];
}
}
}
#end
Well, I have managed to come up a solution to this. Really it is probably just masking the issue rather than finding the route cause, but at this point I will take it.
First some code:
#interface UIView (ViewCapture)
- (UIImage*)captureView;
- (UIImage*)captureViewInRect:(CGRect)rect;
#end
#implementation UIView (ViewCapture)
- (UIImage*)captureView {
return [self captureViewInRect:self.frame];
}
- (UIImage*)captureViewInRect:(CGRect)rect
{
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.layer renderInContext:context];
UIImage *screenShot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return screenShot;
}
#end
- (void)showPausingSimulation:(UILogicSimulatorView *)simulationView {
[simulationView stopRunning];
UIView* superView = simulationView.superview;
CGPoint oldOffset = simulationView.contentOffset;
for (UIView* subview in simulationView.subviews) {
//offset subviews so they appear when content offset is (0,0)
CGRect frame = subview.frame;
frame.origin.x -= oldOffset.x;
frame.origin.y -= oldOffset.y;
subview.frame = frame;
}
simulationView.contentOffset = CGPointZero; //set the offset to (0,0)
UIImage* image = [simulationView captureView]; //Capture the frame of the scrollview
simulationView.contentOffset = oldOffset; //restore the old offset
for (UIView* subview in simulationView.subviews) {
//Restore the original positions of the subviews
CGRect frame = subview.frame;
frame.origin.x += oldOffset.x;
frame.origin.y += oldOffset.y;
subview.frame = frame;
}
[simulationView setHidden:YES];
UIImageView* imageView = [[UIImageView alloc] initWithFrame:simulationView.frame];
[imageView setImage:image];
[imageView setTag:999];
[superView addSubview:imageView];
[imageView setHidden:NO];
superView = nil;
imageView = nil;
image = nil;
[self show];
}
- (void)dismissUnpausingSimulation:(UILogicSimulatorView *)simulationView {
UIView* superView = simulationView.superview;
UIImageView* imageView = (UIImageView*)[superView viewWithTag:999];
[imageView removeFromSuperview];
imageView = nil;
superView = nil;
[simulationView setHidden:NO];
[simulationView startRunning];
}
Then modifying the dismiss delegate method in my class to have this line:
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
[alertView dismissUnpausingSimulation:self.simulationView];
...
When the alert view is called, but before it is shown, I need to hide the simulator to prevent it corrupting the alert. However just hiding it is ugly as then all is visible behind is a empty view.
To fix this, I first make a UIImage from the simulator views graphics context. I then create a UIImageView with the same frame as the simulator and set the UIImage as its image.
I then hide the simulator view (curing the alert issue), and add my new UIImageView to the simulators superview. I also set the tag of the image view so I can find it later.
When the alert dismisses, the image view is then recovered based on its tag, and removed from its superview. The simulator is then unhidden.
The result is that the rendering issue is gone.
I know its too late for an answer to this question. Lately I had experianced this very same issue.
My Case:
Added couple of custom UIViews with background images and some controlls to the scroll view with shadow effect. I had also set the shadowOffset.
The Solution:
After some step by step analysis, I found out that setting the setShadowOpacity caused The rendering problem for me. When i commented that line of code, it cured the UIAlertView back to normal appearance.
More:
To make sure, I created a new project mimicing the original ui with shadowOpacity. But it didnt caused the rendering problem as i expected. So I am not sure about the root cause. For me it was setShadowOpacity.

how to display UIActivityIndicatorView BEFORE rotation begins

I'd like to display an activity indicator BEFORE the work undertaken by willAnimateRotationToInterfaceOrientation:duration: begins. Most of the time in my app, this work is quickly completed and there would be no need for an activity indicator, but occasionally (first rotation, i.e. before I have cached data, when working with a large file) there can be a noticeable delay. Rather than re-architect my app to cope with this uncommon case, I'd rather just show the UIActivityIndicatorView while the app generates a cache and updates the display.
The problem is (or seems to be) that the display is not updated between the willRotateToInterfaceOrientation:duration and the willAnimateRotationToInterfaceOrientation:duration: method. So asking iOS to show UIActivityIndicator view in willRotate method doesn't actually affect the display until after the willAnimateRotation method.
The following code illustrates the issue. When run, the activity indicator appears only very briefly and AFTER the simulateHardWorkNeededToGetDisplayInShapeBeforeRotation method has completed.
Am I missing something obvious? And if not, any smart ideas as to how I could work around this issue?
Update: While suggestions about farming the heavy lifting off to another thread etc. are generally helpful, in my particular case I kind of do want to block the main thread to do my lifting. In the app, I have a tableView all of whose heights need to be recalculated. When - which is not a very common use case or I wouldn't even be considering this approach - there are very many rows, all the new heights are calculated (and then cached) during a [tableView reloadData]. If I farm the lifting off and let the rotate proceed, then after the rotate and before the lifting, my tableView hasn't been re-loaded. In the portrait to landscape case, for example, it doesn't occupy the full width. Of course, there are other workarounds, e.g. building a tableView with just a few rows prior to the rotate and then reloading the real one over that etc.
Example code to illustrate the issue:
#implementation ActivityIndicatorViewController
#synthesize activityIndicatorView = _pgActivityIndicatorView;
#synthesize label = _pgLabel;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
[self hideActivityIndicatorView];
}
- (void) simulateHardWorkNeededToGetDisplayInShapeBeforeRotation;
{
NSLog(#"Starting simulated work");
NSDate* date = [NSDate date];
while (fabs([date timeIntervalSinceNow]) < 2.0)
{
//
}
NSLog(#"Finished simulated work");
}
- (void) showActivityIndicatorView;
{
NSLog(#"showActivity");
if (![self activityIndicatorView])
{
UIActivityIndicatorView* activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self setActivityIndicatorView:activityIndicatorView];
[[self activityIndicatorView] setCenter:[[self view] center]];
[[self activityIndicatorView] startAnimating];
[[self view] addSubview: [self activityIndicatorView]];
}
// in shipping code, an animation with delay would be used to ensure no indicator would show in the good cases
[[self activityIndicatorView] setHidden:NO];
}
- (void) hideActivityIndicatorView;
{
NSLog(#"hideActivity");
[[self activityIndicatorView] setHidden:YES];
}
- (void) dealloc;
{
[_pgActivityIndicatorView release];
[super dealloc];
}
- (void) viewDidLoad;
{
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(50.0, 50.0, 0.0, 0.0)];
[label setText:#"Activity Indicator and Rotate"];
[label setTextAlignment: UITextAlignmentCenter];
[label sizeToFit];
[[self view] addSubview:label];
[self setLabel:label];
[label release];
}
#end
The app doesn't update the screen to show the UIActivityIndicatorView until the main run loop regains control. When a rotation event happens, the willRotate... and willAnimateRotation... methods are called in one pass through the main run loop. So you block on the hard work method before displaying the activity indicator.
To make this work, you need to push the hard work over to another thread. I would put the call to the hard work method in the willRotate... method. That method would call back to this view controller when the work is completed so the view can be updated. I would put show the activity indicator in the willAnimateRotation... method. I wouldn't bother with a didRotateFrom... method. I recommend reading the Threaded Programming Guide.
Edit in response to a comment: You can effectively block user interaction by having the willAnimateRotation... method put a non functioning interface on screen such as a view displaying a dark overlay over and the UIActivityIndicatorView. Then when the heavy lifting is done, this overlay is removed, and the interface becomes active again. Then the drawing code will have the opportunity to properly add and animate the activity indicator.
More digging (first in Matt Neuberg's Programming iPhone 4) and then this helpful question on forcing Core Animation to run its thread from stackoverflow and I have a solution that seems to be working well. Both Neuberg and Apple issue strong caution about this approach because of the potential for unwelcome side effects. In testing so far, it seems to be OK for my particular case.
Changing the code above as follows implements the change. The key addition is [CATransaction flush], forcing the UIActivityIndicatorView to start displaying even though the run loop won't be ended until after the willAnimateRotationToInterfaceOrientation:duration method completes.
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
[CATransaction flush]; // this starts the animation right away, w/o waiting for end of the run loop
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
[self hideActivityIndicatorView];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
}
Try performing you work on a second thread after showing the activity view.
[self showActivityIndicatorView];
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.01];
Either execute the heavy lifting in a background thread and post the results in the foreground thread to update the UI (UIKit is only thread safe since iOS 4.0):
[self performSelectorInBackground:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil]
Or you can schedule the heavy lifting method to be executed after the rotation took place:
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.4]
But these are only hacks and the real solution is to have proper background processing if your UI needs heavy processing to get updated, may it be in portrait or landscape. NSOperation and NSOperationQueue is a good place to start.

using completion with animateWithDuration causes exc_bad_access

I am trying to animate 2 UIButtons in a UITableViewCell called addToPlaylist and removeFromPlayList (they animate off to the right after being swiped on) and am using a block as follows
[UIView animateWithDuration:0.25 animations:^{
self.addToPlaylist.center = CGPointMake(contentsSize.width + (buttonSize.width / 2), (buttonSize.height / 2));
self.removeFromPlaylist.center = CGPointMake(contentsSize.width + (buttonSize.width / 2), (buttonSize.height / 2));
myImage.alpha = 1.0;
}
completion:^ (BOOL finished)
{
if (finished) {
// Revert image view to original.
NSLog(#"Is completed");
self.addToPlaylist.hidden = YES;
self.removeFromPlaylist.hidden = YES;
self.hasSwipeOpen = NO;
}
}];
on completion I want to hide the buttons to attempt to lessen redraw on scroll etc.
This code sits within '-(void) swipeOff' which is called in the UITableViewControllers method scrollViewWillBeginDragging like so:
- (void)scrollViewWillBeginDragging:(UIScrollView *) scrollView
{
for (MediaCellView* cell in [self.tableView visibleCells]) {
if (cell.hasSwipeOpen) {
[cell swipeOff];
}
}
}
The problem is the completion code, if I remove it or set it to nil all is good, if I include it I get an EXC_BAD_ACCESS. even if I include it with any or all of the lines within the if(finished) commented out
Am I using this in the wrong way, any help much appreciated.
Thanks
I had the same problem with animations. I've solved it by removing -weak_library /usr/lib/libSystem.B.dylib from Other Linker flags.
Also, according to this answer, if you need this flag, you may replace it with -weak-lSystem.
Check if you are not calling a UIView (collectionView, Mapview, etc) from inside the UIView block, meaning, it would be a call outside the main thread. If you are, try this:
DispatchQueue.main.async {
self.mapBoxView.setZoomLevel(self.FLYOVERZOOMLEVEL, animated: true
)}

pragmatically scroll UIScrollView upon shake gesture

HI,
I have a view that has three UIScrollViews on the screen. I want to randomly scroll the UIScrollViews to different positions whenever a user shakes the iPhone, but I am unable to get it done.
I have implemented the following in my ViewController class to detect and handle the shake gesture, the 3 UIScrollViews also belong to the same class. The shake gesture is detected, but the UIScrollViews do not change. What am I doing Wrong??
i have tried both motionBegan and motionEnded.
-(BOOL)canBecomeFirstResponder {
return YES;
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
- (void)viewWillDisappear:(BOOL)animated {
[self resignFirstResponder];
[super viewWillDisappear:animated];
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake)
{
int randomTag = arc4random() % [dirContents count];
CGRect nextImageView = [[scrollView1 viewWithTag:2] frame];
[scrollView1 scrollRectToVisible:nextImageView animated:YES];
randomTag = arc4random() % [dirContents count];
nextImageView = [[scrollView2 viewWithTag:4] frame];
[scrollView2 scrollRectToVisible:nextImageView animated:YES];
randomTag = arc4random() % [dirContents count];
nextImageView = [[scrollView3 viewWithTag:4] frame];
[scrollView3 scrollRectToVisible:nextImageView animated:YES];
NSLog(#"Shake Detected End");
}
}
Thank You
Have you tried using SetContentOffset instead of scrollRectToVisible yet?
if the images in your tableView are of equal height the offset per "Element" is always the same.
[scrollView3 setContentOffset:yourRandomOffsetInPixels animated:YES];
maybe this works.
also consider, that The Problem might be that your shake-detection Method runs on a separate Thread this would mean that you have to call your motionEnded Method on the mainthread like so:
[self performSelectorOnMainThread:#selector(motionEnded) withObject:nil waitUntilDone:NO];
Did you check your nextImageView variable to see if it was correct ?
Further more if you are trying will have the motion of a slot machine, I would recommend you to use UITableView instead of doing it by yourself with scrollView
Just one quick question. In your example code, you generate a random tag:
randomTag = arc4random() % [dirContents count];
but then you use a specific tag value (4 in this case)? I assume it still doesn't work when you use the randomTag value? and that you were just doing some testing?
nextImageView = [[scrollView2 viewWithTag:4] frame];