loading image asynchronously in viewDidLoad, with activity indicator - ios

When i'm switching from a view (by clicking on a cell in a table) to a view that displays some images, i'm loading a couple of images from a couple of urls.
I want to display an activity indicator animation while the loading occurs.
I'm doing this loading in viewDidLoad of the new view.
If i'm loading the images synchronously, then (not surprisingly) the animation doesn't work since the request is blocking...
If i'm loading the images asynchronously, then (also not surprisingly) the view is opened with blanks instead of images, without waiting for the images to be fetched, which i don't want.
I tried to put all that in the segue code that transforms from the old view to the new one, because i hoped that the view will be switched only after the loading will complete but it didn't matter.
How can I enjoy both worlds? how can i not block the app, display the animation, but transition to the next view only when all the images have been loaded?

If you don't want to use third party libraries, you can use GCD (Grand central dispatch) like this:
dispatch_queue_t imageLoadingQueue = dispatch_queue_create("imageLoadingQueue", NULL);
// start the loading spinner here
dispatch_async(imageLoadingQueue, ^{
NSString * urlString = // your URL string here
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];
UIImage * image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
// stop the loading spinner here and place the image in your view
});
});

Have a look at AFNetworking and their UIImageView Extension:
setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholderImage;
Asynchronously downloads an image from the specified URL, and sets it
once the request is finished.
If the image is cached locally, the
image is set immediately, otherwise the specified placeholder image
will be set immediately, and then the remote image will be set once
the request is finished.

You should take a look at AsyncImageView. It does exactly what you want to do.
You can find it at: https://github.com/nicklockwood/AsyncImageView

If you don’t want to show certain views while you loading images you can simply hide it, like this self.imageView.hidden = YES;
After loading you can set your images, and then set hidden to NO.
If you, for some reason, don’t want to segue to another view, before loading completes, then I think you can make intermediate view, where you’ll download all images, after that segue to your images view, and send that images through prepareForSegue:sender:.
And answer from Aurelien Cobb cleared out how to load images asynchronously.

Check SDWebImage (https://github.com/rs/SDWebImage),
it has something called prefetcher - which will download the images and save them in cache,
after which you will receive a callback, and can transition to the new view.
Needless to say that you will have to show a spinner (SVProgressHUD or MBProgressHUD),
and stop it once prefetch callback returns.
https://github.com/rs/SDWebImage/blob/master/SDWebImage/SDWebImagePrefetcher.h
/**
* Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching,
* currently one image is downloaded at a time,
* and skips images for failed downloads and proceed to the next image in the list
*
* #param urls list of URLs to prefetch
*/
- (void)prefetchURLs:(NSArray *)urls;
To show those images in your imageView, just use setImageWithURL: method (UIImageView+WebCache category).

Related

How to quickly open a view controller without executing it's processes?

I have to show various folders which contain a heavy amount of images and videos , but when I click on the folder it takes few seconds to load which seems like it's lagging or hanging ...
I wanna open view controller first and then show the process of loading....
How to do that ???
you can load data in background like this
DispatchQueue.global(qos: .background).async {
// load data here
}
after that need to load UI in main thread
DispatchQueue.main.sync {
// show data here
}
you can solve this problem by using SDWebImage for problem of photos or images loading...
myimageview.sd_setImage(with: imageURL, placeholderImage: UIImage(named: "empty_image_icon"))
//imageURL is URL of image.
//empty_image_icon id the default image which will show during process.
Fetch data in viewWillAppear() method not in viewDidLoad() method.
You can fetch your data/media in viewDidAppear() method and start animating your loading indicator in viewDidLoad() method.
If you load data in viewWillAppear() than your viewController take a time with few second.
You can call your fetching data function after your view created. The viewDidAppear() method is working after your view created but also, this method works whenever the view you create is visible on the screen so it could not be the best solution for every case. It means that, your data could be fetching unnecessarily. For example, viewDidAppear() method is working whenever you change your view controller and come back that screen again or switch your application or your app comes foreground.
Therefore, using dispatchQueue or SDWebImage library could be your answer.

How to decide whether to assign image to UIImageView or not, once it finish downloading using SDWebImage?

SDWebImage is an extension to UIImageView. There is a method:
sd_setImageWithURL(NSURL(string: url), placeholderImage: placeholder, options: options, completed: { image, error, cache, url in
//do sth after it is completed
})
But in completed block I cannot decide whether to allow SDWebImage assign the image or not. It is assigned automaticaly without asking developer about permission to do this. The problem arise when I assign the image to UIImageView within UITableViewCell while scrolling the table.
It is obvious that before it finishes downloading, that cell may be reused for different row, and finally there may be a different image.
How to fix it using this library?
In following question: Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling, there is an answer that SDWebImage solve this problem, but it is not true, or I missed sth.
This shouldn't be necessary. If you look inside the implementation of the UIImageView category you will see this:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
What this shows you is that the minute you call setImage it cancels the download associated with the image view instance. Since UITableView reuses its views this should not be an issue. See how they associate the imageView and URL together.
Your issue is probably the fact that you are not resetting your imageView's image property to nil everytime. So inside cellForRow you probably want:
cell.imageView.image = nil
cell.imageView.sd_setImageWithURL(...)
When you start a new load the image view cancels any current load. So, if the image view is reused this problem is avoided.
If you don't have a new image to set for some reason then you can call sd_cancelCurrentImageLoad and explicitly set your placeholder image.

Access already created view controllers in a UIPageViewController

I'm using a UIPageViewController for displaying images, and I download the images asynchronously and throw them into the gallery when they're ready (but you're still able to go to the page in the gallery where the image will be).
My issue is that, when the image does load, I need to supply it for the view controller it corresponds to. Right now, I add it to an NSCache instance, and when UIPageViewController's data source method viewControllerAfterViewController: is called, I check if the image for the view controller being requested has already been downloaded (is in the cache) and if it is, I call initWithImage: on the specific view controller, passing the downloaded image.
My issue is with when viewControllerAfterViewController: is called and the image hasn't been downloaded yet. Right now I just load the view controller without the image, and when the imageDidFinishDownloading: delegate method is called, I try to supply the view controller with the image.
However, it seems that even though UIPageViewController asks for the next view controller the previous gets displayed (i.e.: when I get to the 3rd page, it requests the 4th page's view controller) I'm not able to access this requested view controller that I hand off.
If I access pageViewController.viewControllers, the count of the NSArray is never more than 1. So even though it seems like it should have 2 (the currently showing view controller and the next one that it requested), it only ever has one, the currently visible view controller.
The problem is that I need the other one. The image finishes downloading, and I really only have two options.
Give it to my NSCache, so when the init method is called for the view controller, it is handed off.
In the event that it has already been inited, hand it off to the view controller that's already been inited.
But 1 doesn't always work, as sometimes the init method is called when there's no image yet (it hasn't finished downloading), so we'd sometimes depend on 2, but pageViewController.viewControllers only holds the current visible view controller.
So there's basically this third situation where the view controller has already been inited without an image, and the image finishes downloading and I want to assign it to that view controller, but I'm currently on the one before it, and my only reference to the view controllers in the UIPageViewController is on the current visible one.
How do I handle 3? I need to assign something to a view controller but I can't find any way to access that view controller.
I'd use something like a future from PMConcurrency to do this. A future is just an object that promises to return your image as soon as it's available. So by giving the future to your view controller instead of the image, you don't need to worry about when the image arrives.
Your view controller's initializer would look something like this:
- initWithImageFuture:(PMFuture *)future
{
if (self = [super init]) {
[[future onMainThread] onComplete:^(id result, NSError *error) {
if (!error) {
_image = result;
}
}];
}
return self;
}
By adding an onComplete block to the future, the view controller will receive the image as soon as it's available (immediately if the image is already downloaded). Futures run in the background by default, so you'd use onMainThread to have the result returned on the main thread.
So instead of populating your cache with images, you'd populate it with image futures. There are several ways to create futures, but the block-based one is pretty simple:
PMFuture *imageFuture = [PMFuture futureWithBlock:^id{
UIImage *image = ...; // Your image fetching code here
return image;
}];
[myCache setObject:imageFuture forKey:myImageKey];
I have a similar sort of function but I load the image in the view controller that is displaying the image (the VC handed to the UIPageViewController). In your context I guess I'd had the image ID info and the NSCache object to the VC. That isn't what you want to do of course, but just something to think about.
Originally I tried to handle image loading in the parent like you are. As I recall, I kept a weak pointer to the target view controller. When the image arrived if the VC was still alive I could set the UIImageView property.

Best way to mirror a UIWebView

I need to mirror a UIWebView's CALayers to a smaller CALayer. The smaller CALayer is essentially a pip of the larger UIWebView. I'm having difficulty in doing this. The only thing that comes close is CAReplicatorLayer, but given the original and the copy have to have CAReplicatorLayer as a parent, I cannot split the original and copy on different screens.
An illustration of what I'm trying to do:
The user needs to be able to interact with the smaller CALayer and both need to be in sync.
I've tried doing this with renderInContext and CADisplayLink. Unfortunately there is some lag/stutter because it's trying to re-draw every frame, 60 times a second. I need a way to do the mirroring without re-drawing on each frame, unless something has actually changed. So I need a way of knowing when the CALayer (or child CALayers) become dirty.
I cannot simply have two UIWebView's because two pages may be different (timing is off, different background, etc...). I have no control over the web page being displayed. I also cannot display the entire iPad screen as there are other elements on the screen that should not show on the external screen.
Both the larger CALayer and smaller "pip" CALayer need to match smoothly frame-for-frame in iOS 6. I do not need to support earlier versions.
The solution needs to be app-store passable.
As written in comments, if the main needing is to know WHEN to update the layer (and not How), I move my original answer after the "OLD ANSWER" line and add what discussed in the comments:
First (100% Apple Review Safe ;-)
You can take periodic "screenshots" of your original UIView and compare the resulting NSData (old and new) --> if the data is different, the layer content changed. There is no need to compare the FULL RESOLUTION screenshots, but you can do it with smaller one, to have better performance
Second: performance friendly and "theorically" review safe...but not sure :-/
At this link http://www.lombax.it/documents/DirtyLayer.zip you can find a sample project that alert you every time the UIWebView layer becomes dirty ;-)
I try to explain how I arrived to this code:
The main goal is to understand when TileLayer (a private subclass of CALayer used by UIWebView) becomes dirty.
The problem is that you can't access it directly. But, you can use method swizzle to change the behavior of the layerSetNeedsDisplay: method in every CALayer and subclasses.
You must be sure to avoid a radical change in the original behavior, and do only the necessary to add a "notification" when the method is called.
When you have successfully detected each layerSetNeedsDisplay: call, the only remaining thing is to understand "which is" the involved CALayer --> if it's the internal UIWebView TileLayer, we trigger an "isDirty" notification.
But we can't iterate through the UIWebView content and find the TileLayer, for example simply using "isKindOfClass:[TileLayer class]" will sure give you a rejection (Apple uses a static analyzer to check the use of private API). What can you do?
Something tricky like...for example...compare the involved layer size (the one that is calling layerSetNeedsDisplay:) with the UIWebView size? ;-)
Moreover, sometimes the UIWebView changes the child TileLayer and use a new one, so you have to do this check more times.
Last thing: layerSetNeedsDisplay: is not always called when you simply scroll the UIWebView (if the layer is already built), so you have to use UIWebViewDelegate to intercept the scrolling / zooming.
You will find that method swizzle it's the reason of rejection in some apps, but it has been always motivated with "you changed the behavior of an object". In this case you don't change the behavior of something, but simply intercept when a method is called.
I think that you can give it a try or contact Apple Support to check if it's legal, if you are not sure.
OLD ANSWER
I'm not sure this is performance friendly enough, I tried it only with both view on the same device and it works pretty good... you should try it using Airplay.
The solution is quite simple: you take a "screenshot" of the UIWebView / MKMapView using UIGraphicsGetImageFromCurrentImageContext. You do this 30/60 times a second, and copy the result in an UIImageView (visible on the second display, you can move it wherever you want).
To detect if the view changed and avoid doing traffic on the wireless link, you can compare the two uiimages (the old frame and the new frame) byte by byte, and set the new only if it's different from the previous. (yeah, it works! ;-)
The only thing I didn't manage this evening is to make this comparison fast: if you look at the sample code attached, you'll see that the comparison is really cpu intensive (because it uses UIImagePNGRepresentation() to convert UIImage in NSData) and makes the whole app going so slow. If you don't use the comparison (copying every frame) the app is fast and smooth (at least on my iPhone 5).
But I think that there are very much possibility to solve it...for example making the comparison every 4-5 frames, or optimizing the NSData creation in background
I attach a sample project: http://www.lombax.it/documents/ImageMirror.zip
In the project the frame comparison is disabled (an if commented)
I attach the code here for future reference:
// here you start a timer, 50fps
// the timer is started on a background thread to avoid blocking it when you scroll the webview
- (IBAction)enableMirror:(id)sender {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); //0ul --> unsigned long
dispatch_async(queue, ^{
// 0.04f --> 25 fps
NSTimer __unused *timer = [NSTimer scheduledTimerWithTimeInterval:0.02f target:self selector:#selector(copyImageIfNeeded) userInfo:nil repeats:YES];
// need to start a run loop otherwise the thread stops
CFRunLoopRun();
});
}
// this method create an UIImage with the content of the given view
- (UIImage *) imageWithView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
// the method called by the timer
-(void)copyImageIfNeeded
{
// this method is called from a background thread, so the code before the dispatch is executed in background
UIImage *newImage = [self imageWithView:self.webView];
// the copy is made only if the two images are really different (compared byte to byte)
// this comparison method is cpu intensive
// UNCOMMENT THE IF AND THE {} to enable the frame comparison
//if (!([self image:self.mirrorView.image isEqualTo:newImage]))
//{
// this must be called on the main queue because it updates the user interface
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
self.mirrorView.image = newImage;
});
//}
}
// method to compare the two images - not performance friendly
// it can be optimized, because you can "store" the old image and avoid
// converting it more and more...until it's changed
// you can even try to generate the nsdata in background when the frame
// is created?
- (BOOL)image:(UIImage *)image1 isEqualTo:(UIImage *)image2
{
NSData *data1 = UIImagePNGRepresentation(image1);
NSData *data2 = UIImagePNGRepresentation(image2);
return [data1 isEqual:data2];
}
I think your idea of using CADisplayLink is good. The main problem is that you're trying to refresh every frame. You can use the frameInterval property to decrease the frame rate automatically. Alternatively, you can use the timestamp property to know when the last update happened.
Another option that might just work: to know if the layers are dirty, why don't you have an object be the delegate of all the layers, which would get its drawLayer:inContext: triggered whenever each layer needs drawing? Then just update the other layers accordingly.

Slow scroll on UITableView images

I'm displaying lots of images loaded directly from my app (not downloaded). My table view is slow when I scroll it the first time. It becomes smooth after all my cell has been displayed. I don't really know why.
I have an array of UIImage that I'm loading in the viewDidLoad. Then in my tableview delegate I just get the image at a given index path and set it to an UIImageView of my cell.
Do you know how I can improve performances ?
just to share I have fixed and it worked very well Steps I followed.
1) Set the performSelectorInBAckground function with Cell as parameter passed that holds the scroll view or uiview to put many iamges.
2) In the background function load the image stored from application bundle or local file using imagewithContents of file.
3) Set the image to the imageView using this code.
//// Start of optimisation - for iamges to load dynamically in cell with delay , make sure you call this function in performSelectorinBackground./////
//Setting nil if any for safety
imageViewItem.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = // Load from file or Bundle as you want
dispatch_sync(dispatch_get_main_queue(), ^{
//Set the image to image view not, wither cell.imageview or [cell add subviw:imageview later ]
[imageViewItem setImage:image];
[imageViewItem setNeedsLayout];
});
});
//// End of optimisation/////
This will load all images dynamically and also scroll the table view quite smoothly than previous slow and jerky behaviour.
All the best
You can read the answer I have just submitted here:
Loading image from CoreData at cellForRowAtIndexPath slows down scrolling
The basic idea is to use Grand Central Despatch to move your table-view-image-getting code to a separate thread, filling in your cells back on the main thread as the images become available. Your scrolling will be super-smooth even if there's a delay loading the images into memory from the filesystem.
What I understand from your question is that your images are all set to go and that they are loaded into RAM (stored in an array which is populated in viewDidLoad). In this case, I would want to see the exact code in cellForRowAtIndexPath in order to help out. My instinct tells me that something is being done there that shouldn't be done on the main thread (as He Was suggests). The thing is - if it's only a fetch from an NSArray (worst case O(log(n))), you shouldn't be seeing a performance hit.
I know you're not downloading the images but I would still recommend to do ANY non-UI operation on a background thread. I wrote something that might help you out.

Resources