I am relatively new to programming on the iPad and I was trying to put together a simple program. Basically, it's a children's book and I need the functionality of a comic book style (or photo) viewer, where people swipe to change "pages" (or images).
Each image is 1024x768. Currently, they are stored as JPGs because of the very large file sizes PNGs seem to produce. For this story, there are 28 pages.
I took a look at the PageControl example, implementing a UIScrollView. On initialization, I create a big enough scrollview area. Then as the user scrolls, I load in the previous and next images. Again, just like the example only without implementing the page control at the bottom.
The problem I am running into is a very slight pause in the animation when I am flipping. Once the images are loaded or cached, this doesn't happen. Now, I know the photo application doesn't do this and I'm not sure what is causing it.
Here is my code for the scrollViewDidScroll method. I keep up with the page number and it will only call the loadPageIntoScrollView when a page has changed - I was thinking that the insane number of calls it was making was causing the slight pause in animation, but it turned out not to be the case.
- (void) scrollViewDidScroll: (UIScrollView *) sender
{
CGFloat pageWidth = scrollView.frame.size.width;
int localPage = floor( (scrollView.contentOffset.x - pageWidth / 2 ) / pageWidth ) + 1;
if( localPage != currentPage )
{
currentPage = localPage;
[self loadPageIntoScrollView:localPage - 1];
[self loadPageIntoScrollView:localPage];
[self loadPageIntoScrollView:localPage + 1];
}
} // scrollViewDidScroll
And here is my loadPageIntoScrollView method. I'm only creating a UIImageView and loading an image into that - I don't see how that could be much "leaner". But somehow it's causing the pause. Again, it's not a HUGE pause, just one of those things you notice and is enough to make the scrolling look like it has a very. very slight hiccup.
Thank you in advance for any help you could provide.
- (void) loadPageIntoScrollView: (int)page
{
if( page < 0 || page >= kNumberOfPages )
return;
UIImageView *controller = [pages objectAtIndex:page];
NSLog( #"checking pages" );
if( (NSNull *)controller == [NSNull null] )
{
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
NSString *pageName = [[NSString alloc] initWithFormat:#"%d.jpg", page];
controller = [[UIImageView alloc]initWithImage:[UIImage imageNamed:pageName]];
[controller setUserInteractionEnabled:YES];
[controller addGestureRecognizer:singleTap];
[pages replaceObjectAtIndex:page withObject:controller];
[controller release];
} // if controller == null
// add the page to the scrollview
if( controller.superview == nil )
{
NSLog(#"superview was nil, adding page %d", page );
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
controller.frame = frame;
[scrollView addSubview:controller];
} // if
} // loadPageIntoScrollView
Since you say after an image is loaded in it no longer lags, I'd suspect that it is disk access that is causing your lag, but you should run your app through instruments to try to rule out cpu-spikes as well as evaluate file system usage. You may try to pre-load images to the left and right of whatever image you are on so that the user doesn't perceive as much lag.
First off, you should be able to use PNG's just fine. I have build several apps that do exactly what you are doing here, you can fit 3 1024 x 768 PNGs in memory without running out (but you can't do much more). You should also use PNG's as they are the preferred format for iOS as they are optimized when the app is bundled together during build.
The slight lag is caused by loading the image, in this line:
controller = [[UIImageView alloc]initWithImage:[UIImage imageNamed:pageName]];
What I usually do is load the images in a separate thread, using something like this:
[self performSelectorInBackground:#selector(loadPageIntoScrollView:) withObject:[NSNumber numberWithInt:localPage]];
Note that you need to put your localPage integer into a NSNumber object to pass it along, so don't forget to change your loadPageIntoScrollView: method:
- (void) loadPageIntoScrollView: (NSNumber *)pageNumber
{
int page = [pageNumber intValue];
....
Related
I made a spritekit game with 10 different scenes with a single base scene which directs to each of the scenes. when I move to and from every scene, memory keeps going up, assuming due to texture caching, as explained here before.
The problem is that memory keeps going up to 300 MB and in weak devices it crashes after 3-4 scenes. I have tried to "free" memory using a cleanup function:
- (void)willMoveFromView:(SKView *)view {
[self.children enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
SKNode* child = obj;
[child removeAllActions];
}];
[self removeAllChildren];
}
However, this doesn't help. Any ideas how to solve this?
This is my main (and only) ViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the view.
SKView * skView = (SKView *)self.view;
// skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
// Create and configure the scene.
MainMenu *scene = [MainMenu sceneWithSize: self.view.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
and my switch to scene code:
- (void)switchToRoom
{
SKTransition *transition = [SKTransition fadeWithColor:[UIColor whiteColor] duration:0.5];
SKScene * scene = [[RoomScene alloc] initWithSize:self.size];
[self.view presentScene:scene transition:transition];
}
UPDATE:
After 3 days of struggling I found out that my bad practice was using a "curtain" styled animation to navigate between scenes. The problem with this method is that a "curtain" is at the size of the screen, and an Atlas of curtains with ~30 textures is HUGE, and probably what caused the memory to run so high, I am not sure why it caused a leak alike behavior, but when reducing the number of frames inside the atlas to around 10, I no longer see memory goes that high, and it looks like that there is no leak.
Another bad practice for this situation was to use "preloadTextures:textures" method, which appears to consume a lot of memory for this type of animation. When I removed it, performance became even better.
SK is suppose to take care of all that by itself baring you have a retain loop. However I too have run into this issue. My solution ended up being to manually set everything to nil:
-(void)willMoveFromView:(SKView *)view {
// for all arrays
[self.myArray removeAllObjects]; // and so on...
// for all objects
self.worldNode = nil;
self.player = nil; // and so on...
}
As you already stated, there will be some increase due to caching but you should level at some point. If this does not work, try running Instruments to see if you can spot a leak.
Pretty new to cocoa development and really stuck with probably a fundamental problem.
So in short, my app UI looks like a simple window with a nsslider at the bottom. What I need is to generate N images and place them, onto N nsviews in my app window.
What it does so far:
I'm clicking on the slider (holding it) and dragging it. While I'm dragging it nothing happens to my views (pictures are not generated). When I release the slider the pictures got generated and my view get filled with them.
What I want:
- I need the views to be filled with pictures as I'm moving the slider.
I figured out the little check box on the NSSlider properties, which is continuous, and I'm using it, but my image generator still doesn't do anything until I release the slider.
Here is my code:
// slider move action
- (IBAction)sliderMove:(id)sender
{
[self generateProcess:[_slider floatValue];
}
// generation process
- (void) generateProcess:(Float64) startPoint
{
// create an array of times for frames to display
NSMutableArray *stops = [[NSMutableArray alloc] init];
for (int j = 0; j < _numOfFramesToDisplay; j++)
{
CMTime time = CMTimeMakeWithSeconds(startPoint, 60000);
[stops addObject:[NSValue valueWithCMTime:time]];
_currentPosition = initialTime; // set the current position to the last frame displayed
startPoint+=0.04; // the step between frames is 0.04sec
}
__block CMTime lastTime = CMTimeMake(-1, 1);
__block int count = 0;
[_imageGenerator generateCGImagesAsynchronouslyForTimes:stops
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
if (CMTimeCompare(actualTime, lastTime) != 0)
{
NSLog(#"new frame found");
lastTime = actualTime;
}
else
{
NSLog(#"skipping");
return;
}
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 500, 100, 100);
NSImageView *iView = [[NSImageView alloc] initWithFrame:rect];
[iView setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[iView setImage:myImage];
[self.windowForSheet.contentView addSubview: iView];
[_viewsToRemove addObject:iView];
}
if (result == AVAssetImageGeneratorFailed)
{
NSLog(#"Failed with error: %#", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled)
{
NSLog(#"Canceled");
}
count++;
}];
}
}
If you have any thoughts or ideas, please share with me, I will really appreciate it!
Thank you
In order to make your NSSlider continuous, open your window controller's XIB file in Interface Builder and click on the NSSlider. Then, open the Utilities area
select the Attributes Inspector
and check the "Continuous" checkbox
under the Control header. Once you've done this, your IBAction sliderMove: will be called as the slider is moved rather than once the mouse is released.
Note: Alternatively, with an
NSSlider *slider = //...
one can simply call
[slider setContinuous:YES];
for clarification I'll just add 2 overlaid Screenshots, one in Interface Builder, the other on the device.
The lower UISegmentedControl is fresh out of the library with no properties edited, still it looks different on the Device (in this case a non-Retina iPad, though the problem is the same for Retina-iPhone) (Sorry for the quick and dirty photoshopping)
Any ideas?
EDIT: I obviously tried the "alignment" under "Control" in the Utilities-Tab in Interface Builder. Unfortunately none of the settings changed anything for the titles in the UISegment. I don't think they should as they are not changing titles in Interface Builder either.
EDIT2: Programmatically setting:
eyeSeg.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
doesn't make a difference either.
Found the Problem "UISegmentedControlStyleBezeled is deprecated. Please use a different style."
See also what-should-i-use-instead-of-the-deprecated-uisegmentedcontrolstylebezeled-in-io
Hmm...have you checked the alignment? Maybe that's the case.
You can recursively search the subviews of the UISegmentedControl view for each of the UILabels in the segmented control and then change the properties of each UILabel including the textAlignment property as I've shown in a sample of my code. Credit to Primc's post in response to Change font size of UISegmented Control for suggesting this general approach to customizing the UILabels of a UISegmentedControl. I had been using this code with the UISegmentedControlStyleBezeled style by the way even after it was deprecated although I have recently switched to UISegmentedControlStyleBar with an adjusted frame height.
- (void)viewDidLoad {
[super viewDidLoad];
// Adjust the segment widths to fit the text. (Will need to calculate widths if localized text is ever used.)
[aspirationControl setWidth:66 forSegmentAtIndex:0]; // Navel Lint Collector
[aspirationControl setWidth:48 forSegmentAtIndex:1]; // Deep Thinker
[aspirationControl setWidth:49 forSegmentAtIndex:2]; // Mental Wizard
[aspirationControl setWidth:64 forSegmentAtIndex:3]; // Brilliant Professor
[aspirationControl setWidth:58 forSegmentAtIndex:4]; // Nobel Laureate
// Reduce the font size of the segmented aspiration control
[self adjustSegmentText:aspirationControl];
}
- (void)adjustSegmentText:(UIView*)view {
// A recursively called method for finding the subviews containing the segment text and adjusting frame size, text justification, word wrap and font size
NSArray *views = [view subviews];
int numSubviews = views.count;
for (int i=0; i<numSubviews; i++) {
UIView *thisView = [views objectAtIndex:i];
// Typecast thisView to see if it is a UILabel from one of the segment controls
UILabel *tmpLabel = (UILabel *) thisView;
if ([tmpLabel respondsToSelector:#selector(text)]) {
// Enlarge frame. Segments are set wider and narrower to accomodate the text.
CGRect segmentFrame = [tmpLabel frame];
// The following origin values were necessary to avoid text movement upon making an initial selection but became unnecessary after switching to a bar style segmented control
// segmentFrame.origin.x = 1;
// segmentFrame.origin.y = -1;
segmentFrame.size.height = 40;
// Frame widths are set equal to 2 points less than segment widths set in viewDidLoad
if ([[tmpLabel text] isEqualToString:#"Navel Lint Collector"]) {
segmentFrame.size.width = 64;
}
else if([[tmpLabel text] isEqualToString:#"Deep Thinker"]) {
segmentFrame.size.width = 46;
}
else if([[tmpLabel text] isEqualToString:#"Mental Wizard"]) {
segmentFrame.size.width = 47;
}
else if([[tmpLabel text] isEqualToString:#"Brilliant Professor"]) {
segmentFrame.size.width = 62;
}
else {
// #"Nobel Laureate"
segmentFrame.size.width = 56;
}
[tmpLabel setFrame:segmentFrame];
[tmpLabel setNumberOfLines:0]; // Change from the default of 1 line to 0 meaning use as many lines as needed
[tmpLabel setTextAlignment:UITextAlignmentCenter];
[tmpLabel setFont:[UIFont boldSystemFontOfSize:12]];
[tmpLabel setLineBreakMode:UILineBreakModeWordWrap];
}
if (thisView.subviews.count) {
[self adjustSegmentText:thisView];
}
}
}
The segmented control label text has an ugly appearance in IB but comes out perfectly centered and wrapped across 2 lines on the device and in the simulator using the above code.
I have the following code which draws a bunch of uiimage tiles on screen each time the screen is touched (its a character exploring a tile dungeon). But the movement is very jumpy if I click quickly. Why is it acting sluggishly.
Thanks.
- (void) updateView {
// lets remove all subviews and start fresh
for (UIView *subview in self.view.subviews)
[subview removeFromSuperview];
[self drawVisibleDungeon];
[self displayMonsters];
[self drawPlayer];
[self.view setNeedsDisplay];
[self.view addSubview:messageDisplay];
[self.view addSubview:miniMap];
[miniMap setNeedsDisplay];
}
- (void) processTouchAtX: (int)x AndY: (int)y
{
int tempx = 0;
int tempy = 0;
if (x > 4)
tempx++;
if (x < 4)
tempx--;
if (y > 6)
tempy++;
if (y < 6)
tempy--;
[[[self _accessHook] dungeonReference]processOneTurnWithX:tempx AndY:tempy];
[self updateView];
}
- (void) displayMonsters
{
UIImage *aImg;
int x, y;
NSString *aString;
int monCount = [[[_accessHook dungeonReference]aDungeonLevel]monsterCount];
NSMutableArray *monArray = [[[_accessHook dungeonReference]aDungeonLevel]mArray];
int player_x_loc = [[[self _accessHook] dungeonReference] thePlayer].x_location;
int player_y_loc = [[[self _accessHook] dungeonReference] thePlayer].y_location;
for (int i=0; i<monCount; i++)
{
x = [[monArray objectAtIndex: i] x_loc];
y = [[monArray objectAtIndex: i] y_loc];
if ([self LOSFrom:CGPointMake(x, y)])
if ((x >= player_x_loc-4) && (x < (player_x_loc+5)) && (y >= player_y_loc-6) && (y < (player_y_loc+6)))
{
UIImageView *imgView=[[UIImageView alloc]init];
aString = [[monArray objectAtIndex:i]monsterPicture];
aImg = [UIImage imageNamed:aString];
imgView.frame=CGRectMake((x - (player_x_loc-4))*32, (y - (player_y_loc-6))*32, 32, 32);
imgView.image = aImg;
[self.view addSubview:imgView];
[self.view bringSubviewToFront:imgView];
[imgView release];
}
}
}
Your idea of using cocos2d is fine, and it's a very good framework and probably well suited to other problems you may face. But... why don't we also discuss why you're having so much trouble.
You're using a separate UIImageView for each tile. UIImage is fine, and pretty efficient for most purposes. But a full view for every tile is pretty expensive. Worse, every time you call updateView, you take everything and throw it away. That's very, very expensive.
Just as step one, because it's easy for you to fix, set up all the image views at the beginning and store them in an array. Then in displayMonsters, just change their locations and images rather than deleting them and creating new ones. That by itself will dramatically improve your performance I suspect.
If you still need more performance, you can switch to using CALayer rather than UIImageView. You can very easily create a CALayer and set its contents to aImg.CGImage. Again, you'd add a bunch of CALayer objects with [view.layer addSublayer:layer], just like adding views. Layers are just much cheaper than views because they don't handle things like touch events. They just draw.
But if you anticipate needing fancier 2D graphics, there's nothing wrong with Cocos2D.
It's slow because using UIImage for a bunch of map tiles has a lot of overhead. It would be best to draw everything using OpenGL and vertex buffers.
To get a sense of what I'm doing without posting pages of code... I have an NSOperation that I'm using to process files as they are added to a folder. In that NSOperation I'm using the NSNotificationCenter to send notifications to an NSView whenever a new job is started. The idea is, that I want to add a new subview to give me some information about the job that just started. The problem is I can't seem to get new subviews to draw. Here is what I have right now.
- (void)drawRect:(NSRect)dirtyRect
{
NSLog(#"Draw Count %i", [jobViewArray count]);
int i = 0;
while (i < [jobViewArray count]) {
[self addSubview:[jobViewArray objectAtIndex:i]];
}
}
and then further down:
-(void) newJobNotification: (NSNotification *) notification
{
if (!jobViewArray)
jobViewArray = [[NSMutableArray alloc] init];
++jobCount;
NSRect rect;
rect.size.width = 832;
rect.size.height = 120;
NSPoint point = { 0, ((jobCount * 120) - 120) };
rect.origin = point;
ProgressView *newJob = [[ProgressView alloc] initWithFrame:rect];
[jobViewArray addObject:newJob];
NSLog(#"Notice Count %i", [jobViewArray count]);
}
}
When I use my app to add a job, the notification is properly received by my NSView, the subview is properly added to the jobViewArray, but then when drawRect: gets called again my jobViewArray is empty. It's the first time I've tried to do something like this so I'm probably doing something completely wrong here... I guess that goes with out saying since it doesn't work huh?
You shouldn't be adding the subview to the view in drawRect:. When you receive the notification you should add the subviews there because the second time the notification comes around, you're going to add 2 subviews, then the next time 3 subviews and so one.
If you add the subview in the notification then you'll not need to mess around with the array.