How do I properly preload SKScenes? - sprite-kit

I have the exact same problem as described here, however the proposed solution won't work. In short, when transitioning between scenes, the transition in between is skipped entirely because the presentee takes too long to load. Now I'm looking for a way to properly preload all contents of the scene.
What I do is:
alloc/init the scene upon application launch
preload all textures
the scene consists of thereafter
a few seconds later, present the
scene.
The first time a transition is triggered it is always skipped, all following attempts work as expected.
Here is an example of how I alloc/init and preload the scenes (upon launch):
self.menuScene = [[LEMenuScene alloc] initWithSize:self.bounds.size];
[self.menuScene preload];
whereby preload is a category of SKScene:
- (void) preload {
NSMutableArray* allTextures = [[NSMutableArray alloc] init];
[self enumerateChildNodesWithName:#"//SKSpriteNode" usingBlock:^(SKNode *node, BOOL *stop) {
SKTexture* texture = ((SKSpriteNode*)node).texture;
if (texture) [allTextures addObject: texture];
}];
[SKTexture preloadTextures:allTextures withCompletionHandler:^{
//nop
}];
}
I am aware that the loading is carried out asynchronously and that preload will return instantly (which is intended), so I could cause a transition before loading is finished, but it does not matter how long I wait, it always lags.
Any tips/help is greatly appreciated!

Turns out the issue wasn't the textures after all. I misspelled the name of a font, which made the system look for a suitable substitution, causing the delay.
Here's the same issue:
How to cache or preload SKLabelNode font?

Related

Can't display a texture loaded asynchronously

In my game i have to change background of the scene during gameplay time. When i set new texture for the background the game slows down for a moment. In order to escape this i'm trying to preload a texture asynchronously and then show it on main thread. This is how i do that:
NSString *filename = [NSString stringWithFormat:#"res/src/level_%i/background.png", [GameLevel sharedGameLevel].currentLevelIndex + 1];
__block CCTexture2D *texture;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"FILENAME %#", filename);
[[CCTextureCache sharedTextureCache] addImage:filename];
NSLog(#"%#", [CCTextureCache sharedTextureCache]);
dispatch_async(dispatch_get_main_queue(), ^{
texture = [[CCTextureCache sharedTextureCache] textureForKey:filename];
[spareBackground setTexture:texture];
[dayBackground runAction:[CCSequence actions:fadeOut,[CCCallBlockN actionWithBlock:^(CCNode *node)
{
NSLog(#" TEXTURE %#", texture);
[dayBackground setTexture:texture];
CCFadeIn *fadeIn = [[[CCFadeIn alloc] initWithDuration:5] autorelease];
[dayBackground runAction:fadeIn];
}], nil]];
});
});
but instead of background i always receive a blank screen despite the texture has been successfully loaded, it's not nil. This code works just fine if the texture is loaded on the main thread without using gcd. What am i missing?
My suspect is that CCTextureCache is not thread-safe (and being a shared object it would need to be thread safe in order to be safely called from another thread).
Cocos2d, on the other hand already provides mechanism to load a texture asynchronously, so you might use them instead. This should be the signature:
[[CCTextureCache sharedTextureCache] addImageAsync:filename target:self selector:#selector(textureLoaded:)];
If you can afford to trade 'space' for 'time' , ie your game's memory footprint is reasonable, preload both textures at the start of the scene, and during game play, just flip the texture's visibility.
Also, i found that loading pvr textures instead of png's will run considerably faster (ie consume less cpu i guess). You could try that as a first attempt - hoping that the 'pause' would be acceptable from a user experience standpoint.
ps. any comment i make on performance are based on actual measurements and tests on real devices. The simulator is useless. Make certain this 'pause' is present on devices before spending any effort on optimizing.

Single ANIMATED UIImageView background throughout app

I've been reading everything I can find on here about this topic but am still not sure the best way to proceed. I have a heavy UIImageView that uses an array of fat UIImages acting as an animated loop. This UIImageView is serving as the background for every screen in the app. (Client's request, not mine, so don't hate.) We've optimized the png files as small as they can go but it's still a pretty heavy load.
I've read several posts about how UIImage searches the cache for an existing image of that name (Ex. Shared UIImageView for background image throughout app - Singleton Property) but this doesn't seem to be happening. I've also been reading here about singletons and instantiating in the appdelegate but I'm not sure if either of these are the right way to proceed.
What's the best way to load the UIImageView once over the life of the app and use it in the background of every viewcontroller? Btw, because it takes several seconds to load, I'm going to be adding a "loading" page at app start that uses a single static image and an activity indicator.
Also, I'm not as familiar with testing and performance tools. Which one should I be using to test performance and make sure this is what is causing the hesitations throughout the app?
Apologies in advance for the noob questions - I generally avoid asking questions at all but sometimes, as in this case, I don't even know where to begin the research.
I'm rather pleased with myself. I went with the AppDelegate technique and it's working beautifully. I declared a UIImageView property in the AppDelegate, then add the image code to its getter:
- (UIImageView *)backgroundImageView {
if (!_backgroundImageView) {
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:150];
for (int imageNum = 0; imageNum < 150; imageNum++) {
[tempArray addObject:[UIImage imageNamed:[NSString stringWithFormat:#"image_00%03i.png",imageNum]]];
}
_backgroundImageView = [[UIImageView alloc] initWithImage:[UIImage animatedImageWithImages:tempArray duration:4.0]];
}
return _backgroundImageView;
}
This also allowed me to kill the object in applicationDidReceiveMemoryWarning.
Then I created a helper class that I can pop into all of my UIViewController classes:
+ (void)placeBackgroundImageUnderView:(UIView *)masterView {
myAppDelegate *appDelegate = (myAppDelegate *)[[UIApplication sharedApplication] delegate];
//Correct for shrinkage
appDelegate.backgroundImageView.frame = masterView.frame;
[masterView addSubview:appDelegate.backgroundImageView];
[masterView sendSubviewToBack:appDelegate.backgroundImageView];
}
This allowed me to add a single line in each viewDidLoad method:
[HelperClass placeBackgroundImageUnderView:self.view];
The thing is, just using UIImage alone should cache the whole thing and speed up load time. But it seems that every now and then the images would have to reload - memory issues? So this allows me more control over it by instantiating the object once and using that same object, while also being to set that object to nil to free up memory if needed.
Since adding it, the whole app has been loading much faster. I added an additional load screen with an activity indicator for that initial load (also works beautifully) and everything after that is instant happy.

AVPlayerItem fails with AVStatusFailed and error code "Cannot Decode"

I'm running into a strange issue, I hope someone can help.
In my iOS app I create a video with a custom soundtrack using MutableComposition by combining a video from the user's photo library and an audio file from the app bundle. I then use an AVPlayer and AVPlayerItem to play the video back to the user using a custom video player I made.
Each time a new composition is created, the assets, the player and the composition are cleared, released and it basically starts from a clean, init state.
All works fine, until after exactly 4 successful videos created this way every other attempt to create the player fails with error Cannot Decode. It does not matter if its the same video I'm recreating, has no relation to the size/length of the video or the audio file it simply always fails exactly on the fifth attempt, like clockwork. Once it fails, it will then always fail!
This is weird, because it just decoded the same video four times with no problem, so all of a sudden it fails? So, if anyone has a clue, please let me know.
Ok everyone, I have the answer to this straight from Apple. I used one of my developer TSI lifelines to ask the question, and I'll summarize the response.
There is a limit on the number of concurrent video players that AVFoundation will allow. It is due to the limitations of iOS hardware. The limit for current devices is 4 players. If you create a 5th player, you will get the "cannot decode" error. It is not a limit on the number of instances of AVPlayer, or AVPlayerItem. Rather,it is the association of AVPlayerItem with an AVPlayer which creates a "render pipeline", and you are limited to 4 of these. For example, this causes a new render pipeline:
AVPlayer *player = [AVPlayer playerWithPlayerItem:somePlayerItem];
// assuming the AVPlayerItem is ready to go with an AVAsset that has been loaded
I was also warned that you cannot assume that you will have 4 pipelines available to you. Another App may be using one or more. Indeed, I have seen this happen on an iPad, but it was not clear which app was using a pipeline.
So, there you go, it was totally undocumented, but that is the story.
I ran into the same error message after creating 4 AVPlayer instances, the fix in my case wasn't exactly the same though. Perhaps this will help anyone else who comes across this problem.
What I eventually found is that the AVPlayers were not being released when I had thought they were. In my case I was pushing my AVPlayer View Controller onto a Navigation Controller. Even though I was only creating one AVPlayer instance at a time, when the View Controllers are popped off a nav controller they were not being released immediately. It was then very easy for me to reach 4 AVPlayer instances before the old View Controllers were cleaned up.
It wasn't until I made sure that the previous players were released that this problem went away. To be complete I released the AVPlayerItem, AVPlayer and set the player on the AVPlayerLayer to nil before releasing.
I have to wonder if there is some limit on AVPlayer instances, unintentional or not. A related bit of info from the docs:
https://developer.apple.com/library/ios/#documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/02_Playback.html
"Multiple player layers: You can create arbitrarily many AVPlayerLayer objects from a single AVPlayer instance, but only the most-recently-created such layer will display any video content on-screen."
This one was absolutely killing me until I figured it out, picking up clues from this thread and a few others. The biggest single problem in my code was that I was instantiating my video player controller every time I wanted to play a video. Now, it gets instantiated once in the primary controller (in this case, my DetailViewContoller):
#interface DetailViewController () {
VideoPlayerViewController *videoPlayerViewController;
}
- (void) viewDidLoad
{
[super viewDidLoad];
videoPlayerViewController = [[VideoPlayerViewController alloc] initWithNibName: nil bundle: nil];
}
When I want to show a video, I call my DetailViewController's startVideoPlayback method:
- (void) startVideoPlayback: (NSString *)videoUID
{
videoPlayerViewController.videoUID = videoUID;
[self presentModalViewController: videoPlayerViewController animated: YES];
}
(NOTE: I'm passing it 'videoUID' -- a unique identified that was used to create the video in another part of the app.)
In the VideoPlayerViewController (which is largely cribbed from Apple's AVPlayerDemo sample), the one-time screen setup (initializing the AVPlayer, setting up the toolbar, etc.) is done in viewDidLoad -- which now only get's called once, and all per-video setup gets done within viewWillAppear, which then calls prepareToPlay:
- (void) prepareToPlay
{
[self initScrubberTimer];
[self syncPlayPauseButtons];
[self syncScrubber];
NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//*** Retrieve and play video at associated with this videoUID
NSString *destinationPath = [documentsDirectory stringByAppendingFormat: #"/%#.mov", videoUID];
if ([self fileExists: destinationPath]) {
//*** Show the activity indicator spinny thing
[pleaseWait startAnimating];
[self setURL: [NSURL fileURLWithPath: destinationPath]];
//*** Get things going with the first video in this session
if (isFirst) {
isFirst = NO;
//*** Subseqeunt videos replace the first one
} else {
[self.mPlayer replaceCurrentItemWithPlayerItem: [AVPlayerItem playerItemWithURL: [NSURL fileURLWithPath: destinationPath]]];
}
}
}
OK, I figured out a solution, I hope this is helpful to anyone who may stumble on something similar to this problem.
The solution in my case was to initialize the asset for the AVPlayer and the AVPlayerItem on the main thread and make sure I don't create the actual AVPlayerLayer before the playerItem and the player objects return with status "ReadyToPlay".
This proved to be tricky to isolate and I still don't know why it worked the first 4 times and then failed consistently on the 5th time.
Till, I couldn't really include the code, it wasn't a matter of one line or even a few functions. It was a complex problem that I couldn't isolate to begin with. Thanks for the comments though.
It seems like that issue can be caused by any decoding tasks, not only actual players.
I randomly had this problem when I implemented a background task to extract frames from currently playing videos with generateCGImagesAsynchronously
I need to display 4 videos on screen and a race condition would sometime cause the frame extraction to start before the video started playing and I would wait for isReadyForDisplay forever.
Not sure what a good recover strategy is if you can't avoid the condition in the first place, I would probably try to replaceCurrentItem

AssetsLibrary and ImageView -setImage Slowness

So this one is pretty odd ad I'm not sure if the trouble is with the AssetsLibrary API, but I can't figure out what else might be happening.
I am loading an array with ALAssets using the -enumerateAssetsUsingBlock method on ALAssetsGroup. When it completes, I am loading a custom image scroller. As the scroller finishes scrolling, I use NSInvocationOperations to load the images for the currently visible views (pages) from the photo library on disk. Once the image is loaded and is cached, it notifies the delegate which then grabs the image from the cache and displays it in an image view in the scroller.
Everything works fine, but the time it takes from when -setImage: actually gets called to the time it actually shows up visibly on the screen is unbearable--sometimes 10 seconds or more to actually show up.
I have tried it both with and without image resizing which adds almost nothing to the processing time when I do the resizing. As I said, the slowdown is somewhere after I call -setImage on the image view. Is anyone aware of some sort of aspect of the AssetLibrary API that might cause this?
Here's some relevant code:
- (void)setImagesForVisiblePages;
{
for (MomentImageView *page in visiblePages)
{
int index = [page index];
ALAsset *asset = [photos objectAtIndex:index];
UIImage *image = [assetImagesDictionary objectForKey:[self idForAsset:asset]];
// If the image has already been cached, load it into the
// image view. Otherwise, request the image be loaded from disk.
if (image)
{
[[page imageView] setImage:image];
}
else {
[self requestLoadImageForAsset:asset];
[[page imageView] setImage:nil];
}
}
}
This will probably mess up any web searches looking to solve problems with the AssetsLibrary, so for that I apologize. It turns out that the problem wasn't the AssetsLibrary at all, but rather my use of multi-threading. Once the image finished loading, I was posting a notification using the default NSNotificationCenter. It was posting it on the background thread which was then updating (or trying to update, at least) the UIImageView with -setImage. Once I changed it to use -performSelectorOnMainThread and had that selector set the image instead, all was well.
Seems no matter how familiar I get with multi-threading, I still forget the little gotchas from time to time.

Received memory warning. Level=1 when showing a UIImagePickerController

This is driving me crazy!!!
I'm getting a "Received memory warning. Level=1" whenever I attempt to show a UIImagePickerController with a sourceType = UIImagePickerControllerSourceTypeCamera.
Here is the code from my viewDidLoad where I set things up:
- (void)viewDidLoad {
[super viewDidLoad];
// Set card table green felt background
self.view.backgroundColor = [UIColor colorWithPatternImage: [UIImage imageNamed:#"green_felt_bg.jpg"]];
// Init UIImagePickerController
// Instantiate a UIImagePickerController for use throughout app and set delegate
self.playerImagePicker = [[UIImagePickerController alloc] init];
self.playerImagePicker.delegate = self;
self.playerImagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
}
And here is how I present it modally ..
- (IBAction) addPlayers: (id)sender{
[self presentModalViewController:self.playerImagePicker animated:YES];
}
The result ... UIImagePicker starts to show and then boom ... I get the memory warning ... EVERY TIME! Interestingly enough, if I switch to sourceType = UIImagePickerControllerSourceTypePhotoLibrary ... everything works fine.
What in the heck am I missing or doing wrong? All I want to do is show the camera, take and save a picture.
FYI - I'm testing on my 3GS device.
Thanks to anyone who can help :)
This is very common. As long as you handle the memory warning without crashing and have enough space to keep going, don't let it drive you crazy.
It is not about how much memory your app has used, because it will probably happen even when you write a very simple app which have only one view with one button, clicking the button and then open camera.
I have tested on iPhone 3GS, iPad 2 and iPod touch 3G. It only happened in iPhone 3GS.
I found it will not happen anymore if you restart you device before you execute you app.
Another real solution is to comment the code, [super didReceiveMemoryWarning], in your viewController.
- (void)didReceiveMemoryWarning
{
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
After lots of test on iPhone 3GS with iOS 4.3.2, I found the logic might like that:
-> Open as much as app running on background
-> Presenting a imagePicker of UIImagePickerController, clicking "Back" or "Save" from imagePicker
-> ApplicationDelegate's method, applicationDidReceiveMemoryWarning:(UIApplication *)application, will be invoked
-> Then ViewController's method, didReceiveMemoryWarning:, will be invoked
-> Then viewDidUnload
-> Then viewDidLoad
Then you could find some views have been released and the current view has been pointed to a unexpected one.
By default, [super didReceiveMemoryWarning] will run when ViewController's didReceiveMemoryWarning method is invoked. Commenting it, and viewDidUnload: and viewDidLoad: methods will not be invoked. It means the mem warning has been totally ignored. That's what we expected.
Now after I upgraded to 4.0 it happens to my app too - before in 3.1 there were no warnings.
Actually as you said before, there should be no issue. However, this causes the view that comes after it to load again and viewDidLoad is being called. This messes up my app, since I initialize the view in viewDidLoad - now it gets initialized all over again - even though it shouldn't.
Just as a comment, this might also happen to many other apps that rely on loading the view only once!
It did happen in my app Did I Do That on iOS 4.0 too. It was not consistent, but the most common cause was creating a UIImagePickerController instance and navigating to some large photo stored in one of the albums.
Fixed by persisting state in the didReceiveMemoryWarning method, and loading from state in the viewDidLoad method. One caveat is to remember to clear the state-persisted file in the correct point for your application. For me it was leaving the relevant UIViewController under normal circumstances.
I'm getting the memory warning when opening a UIImagePickerController as well. I'm on 4.01 as well.
But in addition, the UIImagePickerController is running the close shutter animation and stalling there, with the closed shutter on screen.
It seems like the UIImagePickerController's behavior on memory warnings is to close itself.
I could dismiss the UIImagePickerController from the parent ViewController in the didReceiveMemoryWarning method, but that would make for a terrible user experience.
Has anyone seen this problem?
Is there a way to handle the memory warning so that the UIImagePickerController doesn't shut itself down?
I have been struggling with the same problem for some days now. However, resetting my iPhone 4 (clearing out memory) solves the problem so it's not really an app problem.
It appears that a level 1 or 2 memory warning triggers the UIimgPickerController delegate to offload itself. The same happens in my app with the delegate of the delegate (yes it can). After the memory warning however, it will load the delegate (and it's delegate) again causing the viewDidLoad to execute any code that's in there.
I am not sure this happens only while using the UIimgPickerController because testing all that is very time consuming.
I could write some extra code to prevent the code in viewDidLoad en viewWillAppear from execuring while showing the UIimgPickerController but that's not classy, right?
Here's food for thought: it could be
that you are running out of memory
because you are testing your app. With
some memoryleaks it is very well
possible that you are working towards
this problem every time you debug.
The UIImagePickerControllerDelegate is a memory hog because you are capturing high memory assets, be that an image or video. So from the start be sure to specify the medium capture settings, as a start point, reduce this if you don't need the quality:
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.videoQuality=UIImagePickerControllerQualityTypeMedium;
Then after capturing and using these assets. Remove any temp files from the applications temp folder. Could be an extra obsessive step but its a good habit:
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:[lastCapturedFile substringFromIndex:7] ]) {
NSError *error;
// Attempt to delete the folder containing globalDel.videoPath
if ([fileManager removeItemAtPath:[lastCapturedFile substringFromIndex:7] error:&error] != YES) {
NSLog(#"Unable to delete recorded file: %#", [error localizedDescription]);
} else {
NSLog(#"deleted file");
}
}
With above it is clearing the file that was created by the delegate. In some instances if you are transcoding or creating you own assets delete the folder with that file. Note above I am removing the 'file://' part of the url string as the file manager doesn't like it:
[lastCapturedFile substringFromIndex:7]
Other things to consider are covered in the various documentation for what you are doing with that asset - transcoding, image size reduction and more. Beware that any transcoding using the AVFoundation will crash if the UIImagePickerViewController is displaying.