I have a HUD panel in an app and I want to be able to take a set of images and show each image on the panel for a few seconds before displaying the next image. I'm very new to Cocoa and am having trouble implementing this so some pointers would be welcomed. Here's what I'm currently trying:
[newShots enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop)
{
//get url
NSURL *imageUrl = [[NSURL alloc] initWithString:[obj objectForKey:#"image_url"]];;
//get image from url
NSImage *image = [[NSImage alloc] initWithContentsOfURL:imageUrl];
//set it to shot panel
[shotPanelImageView setImage:image];
//clean up
[imageUrl release];
[image release];
//set needs refresh
[shotPanelImageView setNeedsDisplay:YES];
//sleep a few before moving to next
[NSThread sleepForTimeInterval:3.0];
}];
As you can see I'm just looping the info for each image, grabbing it via URL, setting it to the view, and then calling thread sleep for a few seconds before moving on. The issue is that the view will not redraw with the new image when it is assigned. I thought that setNeedsDisplay:YES would force a redraw but only the first image in the collection is ever displayed. I've put in NSLog()'s and debugged and I know for sure the enumeration is working correctly as I can see the new image information being set as it should.
Is there something I'm missing or is this a completely wrong way of going about solving this problem?
Thanks,
Craig
You are sleeping the main thread, which I'm pretty sure is not a good idea. I suggest that the Cocoa way to do what you want is to use a timer. In place of your code above:
[NSTimer scheduledTimerWithTimeInterval:3.0
target:self
selector:#selector(showNextShot:)
userInfo:nil
repeats:YES];
(The userInfo parameter allows you to pass along an arbitrary object that you want to use when the timer fires, so you could potentially use this to keep track of the current index as an NSNumber, but it would have to be wrapped in a mutable container object, because you can't set it later.)
Then put the code from your block into the method called by the timer. You'll need to make an instance variable for the current index.
- (void)showNextShot:(NSTimer *)timer {
if( currentShotIdx >= [newShots count] ){
[timer invalidate]; // Stop the timer
return; // Also call another cleanup method if needed
}
NSDictionary * obj = [newShots objectAtIndex:currentShotIdx];
// Your code...
currentShotIdx++;
}
To avoid the initial 3 sec delay caused by using a timer, you can call the same method your timer uses, right before you set it up:
[self showNextShot:nil]
[NSTimer scheduled...
Or could also schedule a non-repeating timer to fire as soon as possible (if you really wanted to use userInfo):
[NSTimer scheduledTimerWithTimeInterval:0.0
...
repeats:NO];
EDIT: I forgot about -initWithFireDate:interval:target:selector:userInfo:repeats:!
NSTimer *tim = [[NSTimer alloc] initWithFireDate:[NSDate date]
interval:3.0
target:self
selector:#selector(showNextShot:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tim
forMode:NSDefaultRunLoopMode];
[tim release];
Related
With the help of a few threads here I created a slideshow that starts as soon as my view loads
However I am trying to make a slight modification where, I want the images to be random Not showing in the order in the array or in any particular order. This is my codde
-(void)viewDidAppear:(BOOL)animated
{
NSArray *myImages=[NSArray arrayWithObjects:
[UIImage imageNamed:#"Pitt Bull"],
[UIImage imageNamed:#"German Sherpard"],
[UIImage imageNamed:#"Pincer"],
nil];
int indexed=arc4random()%[myImages count];
UIImage *image=[myImages objectAtIndex:indexed];
NSArray *imageAr=[NSArray arrayWithObject:image];
[NSTimer scheduledTimerWithTimeInterval:12.0
target:self
selector:#selector(viewDidAppear:)
userInfo:Nil
repeats:YES];
[self.kenView animageWithImages:imageAr
transitionDuration:12
loop:YES
isLandscape:YES];
}
However it only works once. it loads a random picture at the start, but after that it keeps loading the same image. I can see that is because indexed is only assigned an integer when the view "did appear" . I was wondering if there was another way too select a new random number since I am not using a button/action as I saw many of other members doing. OR a better way of accomplishing this.
Thank you
You may want to try this function, + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats from NSTimer.
For example, in ViewDidLoad
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(showImage:) userInfo:nil repeats:YES];
and declare a function
- (void)showImage:(NSTimer*)timer
{
// your code to generate random index and animate image transition
}
if for some reason, you want to stop the timer
[timer invalidate];
Update
To avoid the timer messing up issue, trigger timer inside showImage, in ViewDidLoad, do [self showImage]
- (void)showImage
{
// your code to generate random index and animate image transition
// trigger the timer when the transition is finished
// You can leverage the method animateWithDuration:animations:completion:
// from UIView
[UIView animateWithDuration:12
animations:^(){
// image transition
}
completion:^(BOOL finished){
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(showImage) userInfo:nil repeats:NO];
}]
}
This should provide you the idea.
Consider using this method to randomly shuffle the elements of your array at startup.
I have sub-classed a component (it looks like circle and there is a picture inside it (NSView subclass)).
I want to change the picture every X time (to make it look like an animation).
When in my main view controller I drawing 1 sub-class like this everything works Fine, but when I adding more the picture changes much faster with each.
(I am firing the picture change using an NSTimer)
I am assuming that the issue happens because I have multiple NSTimers on the same queue
However I tried to use
NSTimer *uiTimer = [NSTimer timerWithTimeInterval:(1.0 / 5.0) target:self selector:#selector(changePicture) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:uiTimer forMode:NSRunLoopCommonModes];
It didn't solve the issue (I assume because it is still on the main thread)
So I came up with NSThread Solution
[NSThread detachNewThreadSelector:#selector(updateModel) toTarget:self withObject:nil];
- (void) updateModel
{
[NSTimer scheduledTimerWithTimeInterval:secondsBetweenUpdates
target:self
selector:#selector(changePicture)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
It has a bit different behaviour (but still no luck as more sub-classes I have as faster picture changes).
My last shoot was this solution:
// Update the UI 5 times per second on the main queue
// Keep a strong reference to _timer in ARC
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, (1.0 / 5.0) * NSEC_PER_SEC, 0.25 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
[self changePicture];
});
// Start the timer
dispatch_resume(_timer);
I am not usually giving up , but I am trying to resolve it for 3 days already... And I think I need and advice of how to do this so it will work as intended.
If you are using GCD I would recommend you use dispatch_after, for example:
float delayTime = 0.2f;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(delayTime * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self changePicture];
});
I want to loop through an array of images, subsequently setting them as the new image of a uiimageview, but because it's a for loop it obviously does it so fast you cannot see a difference.
I would like to add a pause of about 0.2s to the loop before it goes on to the next iteration.
Is there a way to do this?
And more importantly is there an easier way to accomplish what I want to do with some kind of inbuilt framework? thanks!
Alternative to what you want but will work better-Use this method of NSTimer with repeats as YES
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
use some method changeImage like
-(void) changeImage:(NSTimer*)theTimer
{
//code to get the image
NSUInteger idx=[yourArrayOfImages indexOfObjectIdenticalTo:yourImageView.image];
if(idx<[yourArrayOfImages count])
yourImageView.image=[yourArrayOfImages objectAtIndex:++id];
[yourImageView setNeedsDisplay];
}
Do not invalidate the timer. You could use any way to index like use an instance variable, pass it as userInfo.
Adding a delay to your for loop won't make a difference, because redrawing won't happen until you return control to the main event loop. Set up a timer to send you a message every so often to advance to the next image you want to show.
More context would be useful, but it sounds like you should look into NSTimer.
Below is the logic is how the Timer to be call a function recursively in a delay, and once the iteration of the array is done , disable the timer
NSArray *arrayOfImages;
timer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(timerFired:) userInfo:nil repeats:NO];
int i = 0
int count = [arrayOfImages count];
- (UIIMage *)timerFired:(NSTimer *)aTimer{
if (i == count)
{
timer == nil;
return nil;
}
i++;
return [arrayOfImages objectAtIndex:i];
}
You'd better use UIImageView property - animationImages.
#property(nonatomic,copy) NSArray *animationImages;
For example you have an UIImageView* animationImageView :
// pass NSArray of UIImage's in the order of desired appearance
animationImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"image1"],
[UIImage imageNamed:#"image2"],
[UIImage imageNamed:#"image3"],
[UIImage imageNamed:#"image4"],
[UIImage imageNamed:#"image5"], nil];
animationImageView.animationDuration = 1.5; // duration (seconds) of ONE animation cycle
animationImageView.animationRepeatCount = 3; // number of repeats (cycles)
animationImageView startAnimating];
In case you want to start some other action when animation finishes, use dispatch_after :
dispatch_time_t animTime = dispatch_time(DISPATCH_TIME_NOW, animationImageView.animationDuration * NSEC_PER_SEC);
dispatch_after (popTime, dispatch_get_main_queue(), ^(void){
// do your stuff
[self animationDidFinish];
});
I'm trying to get multiple Ball objects to form on screen and bounce around. I have it working fine for one, but I am having problems adding more. Ideally, the amount of balls will vary, but for now I am trying to just get two into an array to change the rest of the code.
My problem is that everything compiles and runs fine, but I still only have one ball. This is where I suspect the problem arises.
I also feel like doing this in viewDidLoad is incorrect, but I'm not sure where it actually belongs.
viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
balls = [[NSMutableArray alloc] init];
CGPoint newPos = CGPointMake(5,5);
for(int i = 0; i < 2; i++) {
[balls addObject:[[[Ball alloc] init] autorelease]];
}
[[balls objectAtIndex:1] setPosition:newPos];
[NSTimer scheduledTimerWithTimeInterval:1.0 / 30.0 target:self selector:#selector(onTimer) userInfo:nil repeats:YES];
}
- (void)onTimer
{
for(int i = 0; i < [balls count]; i++) {
Ball *b = [balls objectAtIndex:i];
[b update];
[(BallView *)self.view refresh:b];
}
}
- (void)refresh:(Ball *)aBall {
ball = aBall;
[self setNeedsDisplay];
}
I've added my onTimer, I thought adding the delay in creating the balls would be enough to make this a nonissue. All update does is change the velocity/direction based on the accelerometer/collisions with the edge of the screen.
Indeed both balls should be displayed. Check their coordinates to see if they overlap. On another note, it appears you are leaking the balls array, as well as the ball1 and ball2 objects. You are alloc init'ing them so you have to release them at the end of viewDidLoad, or, if you are using retain properties, just autorelease them when you set them so that they get retained by the properties.
As for where the code should be located, viewDidLoad should be ok, but consider putting the balls array into the init method you call to create your ViewController, since there's probably no way your ViewController could work without that array being set up.
I'm producing a "brick" every second. When you tap it, it goes away. My problem is, when a second, or more, appear on the screen, tapping the previous one eliminates the most recent one, and can't be removed from the screen at all. The code I have is:
- (NSTimer *)getTimer{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector: #selector(produceBricks) userInfo:nil repeats:YES];
return timer;
}
-(IBAction) tapBrick {
//remove last brick
[bricks[count] removeFromSuperview];
//add to score
count++;
NSString *scoreString = [NSString stringWithFormat:#"%d", count];
score.text = scoreString;
}
-(void) produceBricks {
//determine x y coordinates
int xPos, yPos;
xPos = arc4random() % 250;
yPos = arc4random() % 370;
//create brick
bricks[count] = [[UIButton alloc] initWithFrame:CGRectMake(xPos,yPos + 60,70,30)];
[bricks[count] setBackgroundColor:[UIColor blackColor]];
[bricks[count] addTarget:self action:#selector(tapBrick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:bricks[count]];
}
I know it has to do with the bricks[count] removeFromSuperview line being count is always increasing. How would I reference to the brick in the array that's being clicked instead of the current one?
If you add a sender argument, it'll automatically get set to the control which sent the action, so you can do something like the following:
-(IBAction) tapBrick:(id)sender {
[sender removeFromSuperview];
}
Make the method tapBrick:(UIButton *)brick and you'll get a reference to the brick that was tapped as the argument.
Also, I strongly advise against using a C array for this. It's just asking for memory management trouble. Better to use an NSMutableArray and make sure to follow the memory management rules. With the code you've posted, your brick objects will never be released, and instead they'll just keep eating memory until your app exhausts its supply.
Views have a tag property for a reason — So you can identify views of the same type in the same view controller. Consider setting a unique tag for each of these bricks, and then removing the particular one you want to.