0

Here is what I'm trying to do: I want to have an app that displays a full-screen image and have that image animate by sliding a UISlider. A changing slider value will sequence through images of an array creating an animation. This animation is a single character on a turn-table. The character looks as though he is rotating around the turn table as the slider changes value.

This project is a portfolio piece for a 3D artist. The artist gave me a sequence of 180 images of the character rendered at the full screen retina resolution. He also gave be an additional 180 images rendered at the non-retina full screen resolution. The idea is that when somebody is viewing his character from any angle on a retina iPad, they can toggle using a UISwitch between retina and non-retina display. The code posted below works fine on the simulator.

However when running this code on an iPad 4, it works for a bit, then I get a memory warning. Then it crashes. I'm assuming having that many images of that size being displayed as fast as somebody wants to move a slider is too much for the iPad 4 to handle.

I'm curious what limitations I should take into account when working with images like this. Is 180 images way too many? What is a reasonable amount? Is there a more efficient way of producing my desired result? Instead of resorting the guess-and-check method of how many images would not cause it to crash, I figured somebody might have some useful insight on my issue.

@implementation RBViewController

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

_imageDisplay.image = [UIImage imageNamed:@"HyperReal_0.png"];

_arrayRetina = [[NSMutableArray alloc] initWithCapacity:180];
_arrayNormal = [[NSMutableArray alloc] initWithCapacity:180];

for(int i = 0; i < 179; i++)
{
    NSString *myImageString = [[NSString alloc] initWithFormat:@"HyperReal_%i.png", i];
    UIImage *myImageGraphic = [UIImage imageNamed:myImageString];
    [_arrayRetina addObject:myImageGraphic];
}

for(int i = 0; i < 179; i++)
{
 NSString *myImageString = [[NSString alloc] initWithFormat:@"lameHyperReal_%i.png", i];
    UIImage *myImageGraphic = [UIImage imageNamed:myImageString];
    [_arrayNormal addObject:myImageGraphic];
}
}

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

- (IBAction)switchButton:(id)sender {

if (_switchOnForRetina.isOn == YES)
{
    _imageDisplay.image = [_arrayRetina objectAtIndex:_sliderBar.value];
}
else
    _imageDisplay.image = [_arrayNormal objectAtIndex:_sliderBar.value];

}

- (IBAction)changeSlider:(id)sender {

if (_switchOnForRetina.isOn == YES)
{

_imageDisplay.image = [_arrayRetina objectAtIndex:_sliderBar.value];
}
else

_imageDisplay.image = [_arrayNormal objectAtIndex:_sliderBar.value];
}



@end
BJ Homer
  • 48,806
  • 11
  • 116
  • 129
iOSAppGuy
  • 633
  • 7
  • 23
  • 2
    Can you explain why the need to toggle between retina and non-retina versions of the same images? Retina devices should load the retina version and non-retina should load the non-retina version (the SDK does this automatically when you ask for [UIImage imageNamed:myImageString]), assuming you have used the standard naming convention of "SomeImage.png" and "SomeImage@2x.png" to distinguish between non-retina and retina and both images are available in the app bundle. – mccrager Mar 12 '13 at 17:27
  • I realize that normally when an app is loaded it has either retina graphics loaded or non-retina graphics loaded. In this case, my client is showing his portfolio to his client. He can say to his client "this is what your character will look like on a retina display... and this is what he will look like on a non-retina display." He can rotate his character around at any angle and toggle between the two displays without having to have the portfolio loaded on two different iPads. – iOSAppGuy Mar 12 '13 at 18:04
  • Well, having an image scaled from a lower resolution up to the retina display will not show a customer what it will look like on a non-retina display. It will look worse than if you actually had a non-retina display, and it takes more resources to scale it up. – lnafziger Mar 12 '13 at 18:16
  • The images are not being scaled up. He renders his images out to any resolutions he wants. He does one batch at retina resolution. He does another batch at non-retina resolution. He could produce the same result if he took each image of the retina resolution and scaled them down to non-retina resolution. I'm assuming that an image at non-retina resolution when displayed on a retina device will look the same as a non-retina ipad displaying that image. It seems the retina ipad would display every pixel with 4 pixels and the non-retina ipad would display every pixel with 1 pixel. – iOSAppGuy Mar 12 '13 at 18:27
  • 1
    @lnafziger is correct, you will be scaling the image up. You are going to be fitting an image that is half the resolution in the same space as you would the retina graphic (unless you are actually resizing the UIImageView to be half size as well when non-retina is selected in your toggle). A non-retina graphic on a retina display will not look the same as a non-retina graphic on a non-retina display assuming the same view size on both devices. – mccrager Mar 12 '13 at 21:18
  • If the image frame is my ipad screen (768 x 1024) and my image is twice that size (1536 x 2048), that image is fitting it's pixels into a frame 1/2 it's size thus showing every retina pixel. Then when I toggle to non-retina my image frame is still (768 x 1024). The frame hasn't changed but this image is now 768 x 1024 pixels. So this image frame isn't changing, the size of the image is changing. Since I'm changing the size of the image, I'm not needed to change the size of the frame. – iOSAppGuy Mar 13 '13 at 00:39
  • You can repeat yourself as often as you like, but it doesn't change the fact that when you interpolate the image to display a low resolution image on a high resolution display that it will not look the same... – lnafziger Mar 13 '13 at 03:13
  • Put it on an actual iPad 1/2 and iPad 3/4 screen and you will see. Don't forget to accept an answer here too! :) – mccrager Mar 13 '13 at 14:11
  • @mccrager That is a great point. I will put an iPad 4 next to an iPad 2. I will put an image on each that (according to me) will look the same. That way I will have actual evidence instead of my sounds-about-right theory. I will also try out your continuous slider solution and see if my memory warning goes away. So far I've only tried shaase's answer and it doesn't crash anymore but I still get memory warnings. – iOSAppGuy Mar 13 '13 at 18:15

3 Answers3

1

Instead of loading all 180 images at once into an array, couldn't you instantiate the current image to be displayed during the slider's continuous UIContolEventValueChanged event?

Create the slider

UISlider *slider = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 155, 20)];
slider.continuous = YES;
slider.value = 0.0f;
slider.minimumValue = 0.0f;
slider.minimumValue = 179.0f;
[slider addTarget:self action:@selector(handleContinuousSlider:) forControlEvents:UIControlEventValueChanged];

Handle it's continuous change event

- (void)handleContinuousSlider:(UISlider *)slider {
    //create a UIImage with file name that makes the current integer value of the slider
    NSString *myImageString = [[NSString alloc] initWithFormat:@"HyperReal_%i.png", slider.value];
    UIImage *myImageGraphic = [UIImage imageNamed:myImageString];

   //I assume this is a UIImageView
   [_imageDisplay setImage:myImageGraphic];

   [myImageGraphic release];
}
mccrager
  • 1,038
  • 6
  • 13
1

I'm not quite sure why you need to programmatically switch between Retina and non-Retina, as @2x handles that really well, but I'll assume there is a usage case I'm not familiar with and will leave it at that.

Regarding the memory usage, we're working on an app with similar functionality, and our solution was to populate an array with strings that refer to the image names (or file paths) of all the individual frames. In your case, you would store the reference myImageString rather than myImageGraphic.

Then when you drag your slider, you'd use something like this:

- (IBAction)changeSlider:(id)sender
{
    if (_switchOnForRetina.isOn == YES)
        _imageDisplay.image = [UIImage imageNamed:[_arrayRetina objectAtIndex:_sliderBar.value]];
    else
        _imageDisplay.image = [UIImage imageNamed:[_arrayNormal objectAtIndex:_sliderBar.value]];
}

One additional point, we use imageWithContentsOfFile due to lower memory usage as per this post, though imageNamed's caching may help in this case.

EDIT:

As mccrager pointed out, the above method only changes on touch up. Here's an example I'm using, though I'm using UIGestureRecognizer on a UIView to send a percent to the method rather than relying on a slider.

- (void)dragPosition:(float)myPercent
{
    NSInteger index = (self.images.count - 1) * myPercent;
    NSString *imageName = [self.images objectAtIndex:index];
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
    self.image = [UIImage imageWithContentsOfFile:imagePath];
}

Hope this helps!

Community
  • 1
  • 1
shrug
  • 191
  • 1
  • 8
  • I like this! I'll try this and let you know if it solves my issue. FYI: I realize that normally when an app is loaded it has either retina graphics loaded or non-retina graphics loaded. In this case, my client is showing his portfolio to his client. He can say to his client "this is what your character will look like on a retina display... and this is what he will look like on a non-retina display." He can rotate his character around at any angle and toggle between the two displays without having to have the portfolio loaded on two different iPads. – iOSAppGuy Mar 12 '13 at 18:10
  • I was thinking that you could load thumbnail versions of the images, and after the slider has stopped changing values for a certain period of time (maybe a half second) you go ahead and load in the full res version. That way they still get a "preview" image, which may be enough to move on without loading it. – lnafziger Mar 12 '13 at 18:19
  • These solutions will only change the UIImageView when the user lifts their finger off the slider on a location/value. The example I posted above is continuous, meaning that the message to update the UIImageView will be continuously sent for every new value encountered while the user drags the slider, not only when they let go of the slider. Thus making the animation effect. – mccrager Mar 12 '13 at 18:22
  • Yes, mccrager is correct. For ours, we're not using a slider per se but are using a `UIGestureRecognizer` on a view, and then passing a percentage to a method – shrug Mar 12 '13 at 19:24
  • 1
    mccgrager - The UIImageView does change continuously with the code I posted in the original question. Sliding the slider produces an animated sequence of images. When the finger is released from the slider, the animation simply stops. Once the slider is touched again the animation will happen again upon movement of the slider. – iOSAppGuy Mar 12 '13 at 21:22
  • @RyanBittorf, yes of course your solution does because you are loading all of the images at once (but obviously that isn't a good solution though since you are crashing). What I am saying is that out of all of the answers posted, which load one image at a time depending on slider position, mine was continuous and not just updating on touch end. – mccrager Mar 13 '13 at 14:14
  • I tried this solution and it keeps my app from crashing! ...the bad news is I still get occasional memory warnings. – iOSAppGuy Mar 13 '13 at 18:32
  • @RyanBittorf, have you had a chance to compare `imageWithContentsOfFile` vs `imageNamed`? Perhaps `imageNamed`'s caching is pushing up the memory. – shrug Mar 14 '13 at 14:38
1

Prebuffering memory cost is roughtly:

((2048*1536*4 + 1024*768*4)*180)/1024/1024/1024 = 2.63671875 giga bytes

Even if there some underground optimisation going on, your iPad dont have enough memory to safely run that program.

As a work around your crashes, dont preload all images and instead just load required one

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [ self changeSlider: nil ];
}


- (IBAction)changeSlider:(id)sender {

    if (_switchOnForRetina.isOn == YES)
    {
        _imageDisplay.image =   nil;
        NSString*   pResourcePath   =   [ [ NSBundle mainBundle ] pathForResource: [ NSString stringWithFormat: @"HyperReal_%i", ( int )_sliderBar.value ] ofType: @"png" ];
        _imageDisplay.image =   [ UIImage imageWithContentsOfFile: pResourcePath  ];
    }
    else
    {
        _imageDisplay.image =   nil;
        NSString*   pResourcePath   =   [ [ NSBundle mainBundle ] pathForResource: [ NSString stringWithFormat: @"lameHyperReal_%i", ( int )_sliderBar.value ] ofType: @"png" ];
        _imageDisplay.image =   [ UIImage imageWithContentsOfFile: pResourcePath ];
    }
}

A better solution here would be to create 2 h264 movies from your images sequence

one in forward animation order, second in backward animation order

then play right movie depending on slider animation

Of course that's a lot more work.

Cheers

hsarret
  • 446
  • 1
  • 3
  • 10
  • This approach of using imageWithContentsOfFile is a lot better that just creating tons of UIImage objects. But, it does not address CPU usage of loading and playing the animation smoothly, see http://stackoverflow.com/questions/19820701/how-to-animate-big-images-in-ios/26905046#26905046 – MoDJ Nov 13 '14 at 09:17