NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[getImage objectAtIndex:indexPath.row]]];
cell.imageView.image = [UIImage imageWithData:imageData];
I am fetching images from a server and displaying them on a UITableView. I'm finding that the scrolling is very slow. How do I remove this?
When you call this code:
[[NSData alloc] initWithContentsOfURL:...]
this is reaching out over the network to get data. This can take several seconds (or longer), whereas you want to maintain 60 frames per second drawing. This means that your tableView:cellForRowAtIndexPath: method should execute in under 1/60 seconds, or 16 milliseconds, so network calls are out of the question.
The appropriate way to write this code is to load data asynchronously (using Grand Central Dispatch or another mechanism), cache it either on disk or using NSCache, and then informing the table view that it should reload to display newly-available data.
That datasource method will be called frequently, even for recently displayed cells, as iOS wants to reduce the total number of cells in the tableview, so it will destroy cells that are no longer visible and ask the tablview datasource method to recreate them when they become visible.
If you are fetching from the server for every cell population call then it's bound to be slow (an understatement).
Implement some caching and populate your content from the cache.
Related
I'm making an app that uses a UITableViewController, and fills that table view with data from a webserver.
Inside my viewDidLoad I have a method that loads data from said webserver, stores it in an array as custom objects, and then loads that into cells. This is working fine.
Problem:
However, every time I navigate away from that UITableViewController, and then back, it loads everything again. This is very unnecessary, so what I did was store a boolean in the NSUserDefaults, which keeps track of whether or not this is the first time starting the app. If it is (you just logged in), load the data from the server. If not, don't.
However, as I noticed, the tableView resets every time I navigate away from (or back to) the Controller. Also, all the arrays I stored the custom objects in are now empty, so I can't load it back from the arrays either.
(Every time I navigate back to the TableViewController, it's empty)
I tried storing the arrays in the NSUserDefaults, and then just populate the tableView with that data every time, but it turns out I can't store custom objects in the NSUserDefaults.
What I want to achieve is this:
Whenever I navigate away from and back to said TableViewController (I use the SWRevealViewController), I don't want the tableView to empty out. I want all the cells to stay, that way there is no wait time between when the view is loaded and the tableview is filled.
If this is impossible, I want the second best. To store the array somewhere in the app, and then reload that data into the tableview as soon as the user navigates back to the TableViewController. This is slower than my preferred solution, but still quicker and less data-consuming than loading everything from my server.
Any help is appreciated!
Thanks.
You should create a separate object that manages fetching the data from the web service and then stores it locally. This object can be created in the app delegate or wherever appropriate and passed to the table view controller. The data object should provide an array that the view controller can then use to populate the table. You can even have that data object start pulling from the web service as soon as the app opens instead of waiting for the table view controller to be displayed.
I do not recommend keeping the view in memory just to save the very minimal amount of time it takes to load up a table view (using locally stored data). Unless you are talking about thousands and thousands of entries, you will not notice a lag time in the loading of the view. If you are talking about thousands and thousands of entries, I recommend you load a few hundred at a time into the table.
As far as storing the data, the simplest might be just writing the raw response of the web request to a file. A more elegant solution would probably include creating some objects to represent the data and using NSKeyedArchiver. Keeping data stored locally is a huge topic with countless options so I recommend doing some googling on your own to find the best solution for you. You might start at these places:
NSKeyedArchiver: http://www.raywenderlich.com/1914/nscoding-tutorial-for-ios-how-to-save-your-app-data
Other Options: https://developer.apple.com/technologies/ios/data-management.html
If you go with the NSKeyedArchiver option, you can generate a file path by doing the following:
+ (NSString *)dataFilePath
{
NSURL *directory = [[NSFileManager defaultManager]
URLForDirectory:NSLibraryDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:YES
error:nil
];
NSURL *fullURL = [cachesDirectory URLByAppendingPathComponent:#"a_file_name"];
return [fullURL relativePath];
}
You need to store all the data in cache at first time when user is calling data from server. And after that whenever user navigate and comeback to the page load data from cache.
My table view loads pictures through SDWebImage (async image downloading/cache category for UIImageView) in each tableView:cellForIndexPath:.
// 'spot' is a dictionary of this cell's attributes
NSURL* imageURL = [NSURL URLWithString:(NSString*)[spot objectForKey:#"Image"]];
UIImage* placeholderImage = [UIImage imageNamed:#"placeholder.png"];
^ Loads the images, but is constantly having to make requests or pull from the cache. So I wanted to see if the rendered cells were stored in memory, so I could only make requests for images when I need them. I went about this by adding a simple check for the image property of the image view.
if (!cell.pictureView.image)
{
NSURL* imageURL = [NSURL URLWithString:(NSString*)[spot objectForKey:#"Image"]];
UIImage* placeholderImage = [UIImage imageNamed:#"placeholder.png"];
[cell.pictureView setImageWithURL:imageURL placeholderImage:placeholderImage options:SDWebImageCacheMemoryOnly];
}
This works and keeps my images from having to reload after exiting and reentering the application. But in one of my cells that has a purposely bad image url (for debugging), the image- which used to remain as the imageNamed:#"placeholder"- is now the same image as my second cell.
I'm curious what kind of cell-reuse/cache magic is causing the cell to take the picture of a previous cell.
What is the best way to retain cell properties and only set them when needed?
It's very likely the issue is with the image package you are using. If you are making repeated URL requests to grab images, it is probably best practice to implement NSURLCache, which is a fairly simple and handles caching perfectly so it is not an issue. This is the NSHipster guide on how to use it. Other solutions tend towards these sorts of issues. Since it is a category on UIImageView, it could be any number of things causing issues with UIImageView's implementation when getting used in the cell.
Instruments is telling me that alot of memory is being allocated when I rapidly set the image name of a UIImageview in my app. I have a UIImageView that changes its image name every frame in my game. When profiled with zombie checking in instruments, the app seems to be constantly gaining live bytes at an enourmous rate. Is there a way that I can deallocate the UIImageView's current image to stop it from doing this? I am using ARC.
My code to assign the UIImageView's image is as follows:
aPlanet.image = [UIImage imageNamed:tempPlanetName];
Where aPlanet is the UIImageView and tempPlanetName is the name of the image. This is called every frame.
[UIImage ImageNamed:] method loads the image into image view and adds this newly created uiimage object to autorelease pool. To get rid of this problem you should use -
NSString *imgPath = [NSBundle mainbundle] pathForResource:#"imageName" ofType:#"png"];
aPlanet.image = [[UIImage alloc] ]initWithContentsOfFile:imgPath];
if you are using arc then you don't need to bother about releasing this newly allocated object of uiimage which was created using initWithContentsOfFile: method.
When you use UIImage imageNamed: it will load and cache that image file. This is intended for reuse of icons and other image resources that will be utilized more than once in your application.
Apart from it seeming somewhat unusual to update an image view with a new image every frame, you should look into alternative means of loading images that you will not need more than once - or even if you do, when you need more control over its lifecycle.
For example have a look at UIImage imageWithContentsOfFile: (documented here: Apple Developer Library Reference). It explicitly states that this method will not do any caching of the image contents.
I hope this helps you, but for every frame I doubt that your performance will be good enough with this approach, but this is probably the topic of a different question should the need arise.
I am trying to write an iPad app that loads an image from a URL. I am using the following image loading code:
url = [NSURL URLWithString:theURLString];
NSData *data = [NSData dataWithContentsOfURL:url];
img = [[UIImage alloc] initWithData:data];
[imageView setImage:img];
[img release];
NSLog(#"Image reloaded");
All of that code gets added to a NSOperationQueue as an operation so it will load asynchronously and not cause my app to lock up if the image's websever is slow. I added the NSLog line so I could see in the console when this code finished executing.
I have noticed consistently that the image is updated in my app about 5 seconds AFTER the code finishes executing. However if I use this code on it's own without putting it in the NSOperationQUeue it seems to update the image almost immediately.
The lag is not caused entirely by a slow web server... I can load the image URL in Safari and it takes less than a second to load, or I can load it with the same code without the NSOperationQueue and it loads much more quickly.
Is there any way to reduce the lag before my image is displayed but keep using a NSOperationQueue?
According to the documentation, the code you have written is invalid. UIKit objects may not be called anywhere but on the main thread. I'll bet that what you're doing happens to work in most respects but doesn't successfully alter the display, with the screen being updated by coincidence for some other reason.
Apple strongly recommend that threads are not the way to perform asynchronous URL fetches if you want to remain battery efficient. Instead you should be using NSURLConnection and allowing the runloop to organise asynchronous behaviour. It's not that hard to write a quick method that just accumulates data to an NSData as it comes then posts the whole thing on to a delegate when the connection is complete but assuming you'd rather stick with what you've got I'd recommend:
url = [NSURL URLWithString:theURLString];
NSData *data = [NSData dataWithContentsOfURL:url];
[self performSelectorOnMainThread:#selector(setImageViewImage:) withObject:data waitUntilDone:YES];
...
- (void)setImageViewImage:(NSData *)data
{
img = [[UIImage alloc] initWithData:data];
[imageView setImage:img];
[img release];
NSLog(#"Image reloaded");
}
performSelectorOnMainThread does what the name says — the object is sent to will schedule the selector requested with the object given as a single parameter on the main thread as soon as the run loop can get to it. In this case 'data' is an autoreleased object on the pool in the thread implicitly created by the NSOperation. Because you need it to remain valid until it has been used, I've used waitUntilDone:YES. An alternative would be to make data something that you explicitly own and have the main thread method release it.
The main disadvantage of this method is that if the image returns in a compressed form (such as a JPEG or a PNG), it'll be decompressed on the main thread. To avoid that without making empirical guesses about the behaviour of UIImage that go above and beyond what is documented to be safe, you'd need to drop to the C level and use CoreGraphics. But I'm taking it as given that doing so is beyond the scope of this question.
Tommy is correct about needing to do all UIKit stuff on the main thread. However, if you're running the fetch on a background operation queue, there's no need to use the NSURLConnection asynchronous loading. Also, by keeping the image decoding work on the background operation, you'll keep the main thread from blocking while decoding the image.
You should be able to use your original code as is, but just change [imgView setImage:img] to:
[imageView performSelectorOnMainThread:#selector(setImage:)
withObject:img
waitUntilDone:NO];
I'm loading a very big file in an NSimage with this code :
[[NSImage alloc] initWithContentsOfFile:aFile]
This operation take a few time. I want to display the loading status on my UI.
How can it's possible to read or calculate a progression ?
Thanks.
Initialize the image by referencing the file, which will not load it immediately. Then, set yourself as the image's delegate and respond to the incremental-loading messages that are part of the NSImageDelegate protocol. Then, attempt to ask the image for some information about itself (asking for its representations would probably be a good way), to cause the image to start loading.
I think this will still block your UI, though: You'll be able to display progress, but not to enable the user to work on other things while the image loads. I'm not sure how you would do that.