I'm developing a contact app for the iPhone using MonoTouch. I am using a custom UITableViewCell, which shows the contact image (ABPerson.Image), contact name and some other info.
I am using the following code when the contact image is be loaded:
//CustomTableViewDataSource class
if (person.HasImage)
customCellController.LoadImage (person.Image);
//Custom cell controller class
public void LoadImage(NSData data)
{
ThreadPool.QueueUserWorkItem (p => this.loadImage (data));
}
private void loadImage(NSData data)
{
UIImage image = UIImage.LoadFromData(data);
InvokeOnMainThread(delegate
{
this.imageView.Image = image;
});
}
This code works fine, but scrolling is way to slow. Does anybody have a better idea to load the contact images?
Thanks,
Danny
There could be a lot of problems with your implementation, the code snippet is not enough to figure out what is wrong.
You can look at the image downloader that is part of MonoTouch.Dialog as it supports this scenario, while loading the images from the network or a local file system cache.
There is an excellent blog on this topic which can be found here.
Which covers this topic - it appears what you're doing is very similar, except your actually loading the images in the customCellController itself. In this example, they're called the LoadImage() and loadImage in the datasource and passing in the cell object, rather than doing the computation in the cell itself. You may find this will help speed up your scrolling.
Related
I have a working UIScroll view with local Images in my app. I want however, that my Images will be downloaded and stored in Cache from a URL. I have seen several example libraries that do this like sdwebimage, kingfisher etc. but the examples use UITableview and cells. I am using a UIImage Array for my scroll view. What I actually want is that I download and cache my images and store them in a Array IconsArray = [icon1, icon2, icon3] where icon1 to icon3 are the downloaded images from URLs. How would I do this? Any nice tutorials out there or someone kind enough to show a rookie some code?
Thanks in advance
If you are downloading many images you will have memory problems, and your work will also get thrown away when your array goes out of scope, but what you probably would want to do, if you want to implement your proposed solution, is to use a dictionary rather than an array. It'll just make it much easier to find the image you're looking for. So you could implement the dictionary like this:
var images = [String : UIImage]()
For the key you can just use the URL string (easy enough solution) so accessing an image safely would look like this:
let urlString = object.imageUrl.absoluteString //or wherever you're getting your url from
if let img = self.images[urlString] {
//Do whatever you want with the image - no need to download as you've already downloaded it.
cell.image = img
} else {
//You need to download the image, because it doesn't exist in your dict
...[DOWNLOAD CODE HERE]...
//Add the image to your dictionary here
self.images[object.imageUrl.absoluteString] = downloadedImage
//And do whatever else you need with it
cell.image = downloadedImage
}
As I said, this has some downsides, but it's a quick implementation of what you're asking for.
I'm making an app in Swift 2.0 and I'm using a table view with an image view in the prototype cell. The app downloads some images from the facebook server and I want to display them. The images are all downloaded from a different url, so I am using a program to loop trough them and download, just like I did with the names to get them from the internet and display, but I'm a little bit stuck..
func fetchPicture(identifier: String, completion: (image: UIImage) -> Void)
{
let url2 = NSURL (string: "http://graph.facebook.com/" + identifier + "/picture?type=normal")
let urlRequest = NSURLRequest(URL: url2!)
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) {
(response, data, error) -> Void in
if error != nil
{
print(error)
}
else
{
if let pf = UIImage(data: data!)
{
dispatch_async(dispatch_get_main_queue())
{
completion(image: pf)
self.tableView.reloadData()
}
}
}
}}
var images = [UIImage]()
let queue2 = dispatch_queue_create("images", DISPATCH_QUEUE_SERIAL)
dispatch_apply(newArray.count, queue2) { index in
let identifier = newArray[index]
fetchPicture(identifier) {
image in dispatch_async(queue2)
{
}
}
}
I set the imageview in the cell equal to the variable 'image' so basically I will need to do something like self.image = pf but it needs to be different for each user. With names, I did this with an array, but this isn't working with images I assume..
Help is really appreciated guys!
Thanks!
You would generally want to use a framework that specializes on that kind of stuff. I would not recommend using SDWebImage, it's outdated (no NSURLSession), has a lot of open issues and doesn't work well with Swift (no nullability annotations).
Take a look at those two libraries that are up to date with iOS platform:
DFImageManager - advanced framework written in Objective-C but featuring nullability annotations (works great with Swift). Here's a list of things that make it better, than SDWebImage. Disclosure it's written by me, opinion might be biased.
Kingfisher - lightweight library written in Swift. Similar to SDWebImage, but has much less features that SDWebImage and DFImageManager.
You generally would not want to load all images like this. You definitely don't want to reload the whole table after every image (because that causes it to scroll back to the top of the tableview).
Generally it would be advised to fetch the images lazily (i.e. not until they're needed). You might call fetchPicture from cellForRowAtindexPath and in the completion handler update the cell's image view accordingly.
There are a bunch of details you have to worry about, though:
First, the cell may have been reused by the time the fetch is done, so you have to check to make sure the cell is still visible.
Second, rather than using an array, I'd suggest using a NSCache, and make sure to purge this cache upon memory pressure. You might want to also cache to persistent storage, though some people prefer to rely upon the NSURLCache mechanisms
Third, if the cell scrolls out of view, you might want to cancel the request (so that, for example, if you quickly scroll to the 100th row, that image request doesn't get backlogged behind 99 other image requests for cells that are no longer visible).
Personally, given all the work necessary to do this properly, I might suggest that you consider using a well established asynchronous image fetching mechanism, such as SDWebImage, DFImageManager or Kingfisher. Look at the UIImageView categories/extensions that offer asynchronous image loading. To do all of this properly yourself is a non-trivial exercise, so it's better to use some existing library that provides an asynchronous image view retrieval mechanism.
I see questions regarding long delays in displaying UIImageViews after downloading, but my question involves long delays when
reading from local storage.
After archiving my hierarchy of UIImageViews to a local file (as per narohi's answer in
How to output a view hierarchy & contents to file? ),
I find that if I want to reload them, it takes 5 to 20 seconds for the views to actually appear on screen,
despite my setting setNeedsDiplay() on the main view and all the subviews.
I can immediately query the data contained in the
custom subclasses of UIView that get loaded -- showing that NSKeyedUnarchiver and all the NS-decoding and all the init()'s have completed -- however
the images just don't appear on the screen for a long time. Surely the next redraw cycle is shorter than 5-20 seconds...?
It seems odd that images from PhotoLibrary appear instantly, but anything loaded from local file storage using NSKeyedUnarchiver takes "forever."
What's going on here, and how can I speed this up?
.
.
To be explicit, the relevant part of my Swift code looks like this:
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
myMainView.addSubview(view)
view.setNeedsDisplay()
// now do things with the data in view ...which all works fine
I find that, even if I add something like...
for subview in view.subviews {
subview.setNeedsDisplay()
}
...it doesn't speed up the operations.
We are not talking huge datasets either, it could be just a single imageview that's being reloaded.
Now, I do also notice these delays occurring when downloading from the internet using a downloader like the one shown in
https://stackoverflow.com/a/28221670/4259243
...but I have the downloader print a completion message after not only the download but when the (synchronous operation)
data.writeToFile() is complete (and before I try to load it using NSKeyedUnarchiver), so this indicates that the delay
in UIImageView redraws is NOT because the download is still commencing....and like I say, you can query the properties of the data and it's all in memory, just not displaying on the screen.
UPDATE: As per comments, I have enclosed the needsDisplay code in dispatch_async as per Leo Dabus's advice, and done some Time Profiling as per Paulw11's. Link to Time Profiling results is here: https://i.imgur.com/sa5qfRM.png I stopped the profiling immediately after the image appeared on the screen at around 1:00, but it was actually 'loaded' during the bump around 20s. During that period it seems like nothing's happening...? The code is literally just waiting around for a while?
Just to be clear how I'm implementing the dispatch_async, see here:
func addViewToMainView(path: String) {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
...Since posting this I've found a few posts where people are complaining about how slow NSKeyedUnarchiver is. Could it just be that? If so, :-(.
SECOND UPDATE: Ahh, the "let view = " needs to be in the dispatch_async. In fact, if you just put the whole thing in the dispatch_async, it works beautifully! so...
func addViewToMainView(path: String) {
dispatch_async(dispatch_get_main_queue(), {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
This works instantly. Wow.. Credit to Leo Dabus. Leaving this here for others...
I've been trying to figure out UIImage for some time now. I've been trying to figure out an approach to having one view the 'Main Game View' and showing either 2/3/4 different images depending on the 'level' variable. I'm just trying to be sure of logic. So for example level 1 would display 4 pictures and level 2 might display 3 different pictures. I don't want to hinder performance of the app but because the game is to be played offline and archiving won't make much of a difference all the images (several hundred optimised images) are being stored locally in the main app bundle.
I'm just wondering if my logic for trying to implement this so far is sound or not. For level 1 I would implement the 4 UIImageViews needed and initialise them with images, then display them on screen at set positions. I would then preload the next levels images using GCD. When a continue button is pressed I will set the UIImages and the UIImageViews to nil and display level 2's (or the next level) on screen.
I'm not confident in my approach and was wondering if there was something that would make it simpler or something I've missed or even if in practice it will work accordingly to the theory.
Thank you in advance for you time and any help.
Sorry if this is unclear.
I'm assuming you use a single view controller class for all your levels and load the levels from some .plist file or other format.
Don't bother using GCD. Loading 4 images once per level during loading costs practically nothing.
If all you need to do is have 2-4 images on the screen for each level (in addition to any other level elements), simply create 4 UIImageView instances and add them to your view. Then when you load a level, create new UIImage objects from the required image files and set your UIImageViews' image property to them. The UIImage objects of the old level will be released and deallocated at that moment, since you (and the UIImageViews) don't have a strong reference to them anymore.
Pseudo implementation:
- (void)loadLevel:(int)levelNumber {
// Assuming you have your image views in an array imageViews.
// Determine the image files required for this level.
// Put them into an array imageNames.
for (int i = 0; i < 4; i++) {
if (imageNames.count < i) {
UIImage *image = [UIImage imageNamed:imageNames[i]];
[self.imageViews[i] setImage:image];
} else {
[self.imageViews[i] setImage:nil];
}
}
*UPDATE***
My first description (now deleted) was'nt the real situation. It is a simplified version of the problem.
The Complete situation:
I want to change the UIImage within a UIImageView with a picture which I select from the iphone photo gallery.
For this I used a MediaPicker (part of the Xamarin library).
When I click a button, the Mediapicker will be created. Then I call a function to take a picture from the photo gallery. This methods expects 2 arguments. The Mediapicker and a callback function. --> PictureFromGallery(mediaPicker, CallbackPhotoMade);
This callback function will be trigger after a user selected a picture in the photo library to upload.
Within this callback function I want to change the UIImage of the UIImageView.
void CallbackPhotoMade(MediaFile obj)
{
imageviewPhoto1.Image = UIImage.FromFile("Images/image2.PNG");
//To test I just use a file from a folder in my project
}
When I breakpoint the above function (CallbackPhotoMade) and I put my mouse at the text ".Image =", the following message apears:
MonoTouch.UIKit.UIKitThreadAccessException: UIKit Consistency error: you are calling an UIKit method that can only be invoked from the UI thread
I think this is the problem why the UIImage within the UIImageView doesn't change.
Does anyone knows how to solve this?
*UPDATE2***
I read in another topic that this could be solved by setting CheckForIllegalCrossThreadCalls to false like:
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
UIApplication.CheckForIllegalCrossThreadCalls = false;
}
Unfortunately the image still doesn't change.
Although, the specific error dissapeared.
Try this one:
void CallbackPhotoMade(MediaFile obj)
{
InvokeOnMainThread(() => {
imageviewPhoto1.Image = UIImage.FromFile("Images/image2.PNG");
});
//To test I just use a file from a folder in my project
}
The setting you made, just disables checking, but does not prevent the exception.
The callback is done on another thread (async). And you can/should only update the user interface on/from the main thread. InvokeOnMainThread() makes your code execute on the main thread, which is just what you need.
Ideally the code should run.
Check whether imageviewPhoto1 is nill or not in changeImage method.
If nill then your object is probably released.
If not nill then try writing [self.view setNeedsDisplay]; after setting image , just to see redrawing view can update it or not. Or try whether view is present by setting imageviewPhoto1's background color to something else.