CustomImageView updates :

-> The MediaLoader is not not anymore cancelled when an Image URL is set. The media downloaded is done in background

-> check if there is a pending download to the image URL before starting a new one.
The medias could have been downloaded several times with the UITableViewCell reuse management or when the image was zoomed in full screen.
The MediaManager warns by now when the media download is ended.
This commit is contained in:
ylecollen 2015-01-07 17:26:37 +01:00
parent 10ea385d6b
commit 9ec7818640
3 changed files with 205 additions and 80 deletions

View file

@ -24,16 +24,21 @@ extern NSString *const kMediaManagerProgressStringKey;
extern NSString *const kMediaManagerProgressRemaingTimeKey;
extern NSString *const kMediaManagerProgressDownloadRateKey;
// provide the download progress
// provide the download/upload progress
// object: URL
// userInfo: kMediaManagerProgressRateKey : progress value nested in a NSNumber (range 0->1)
// : kMediaManagerProgressStringKey : progress string XXX KB / XXX MB"
// : kMediaManagerProgressRemaingTimeKey : remaining time string "XX s left"
// : kMediaManagerProgressDownloadRateKey : string like XX MB/s
// : kMediaManagerProgressStringKey : progress string XXX KB / XXX MB" (optional)
// : kMediaManagerProgressRemaingTimeKey : remaining time string "XX s left" (optional)
// : kMediaManagerProgressDownloadRateKey : string like XX MB/s (optional)
extern NSString *const kMediaDownloadProgressNotification;
extern NSString *const kMediaUploadProgressNotification;
// notify when a media download is finished
// object: URL
extern NSString *const kMediaDownloadDidFinishNotification;
extern NSString *const kMediaDownloadDidFailNotification;
// The callback blocks
typedef void (^blockMediaManager_onImageReady)(UIImage *image);
typedef void (^blockMediaManager_onMediaReady)(NSString *cacheFilePath);
@ -45,6 +50,10 @@ typedef void (^blockMediaManager_onError)(NSError *error);
+ (UIImage *)resize:(UIImage *)image toFitInSize:(CGSize)size;
// get a picture from the local cache
// do not start any remote requests
+ (UIImage*)loadCachePicture:(NSString*)pictureURL;
// Load a picture from the local cache or download it if it is not available yet.
// In this second case a mediaLoader reference is returned in order to let the user cancel this action.
+ (id)loadPicture:(NSString *)pictureURL
@ -56,6 +65,14 @@ typedef void (^blockMediaManager_onError)(NSError *error);
mimeType:(NSString *)mimeType
success:(blockMediaManager_onMediaReady)success
failure:(blockMediaManager_onError)failure;
// try to find out a media loder from a media URL
+ (id)mediaLoaderForURL:(NSString*)url;
// same dictionary as the kMediaDownloadProgressNotification one
+ (NSDictionary*)downloadStatsDict:(id)mediaLoader;
// cancel a media loader
+ (void)cancel:(id)mediaLoader;
+ (NSString *)cacheMediaData:(NSData *)mediaData forURL:(NSString *)mediaURL mimeType:(NSString *)mimeType;

View file

@ -22,6 +22,9 @@ NSString *const kMediaManagerPrefixForDummyURL = @"dummyUrl-";
NSString *const kMediaDownloadProgressNotification = @"kMediaDownloadProgressNotification";
NSString *const kMediaUploadProgressNotification = @"kMediaUploadProgressNotification";
NSString *const kMediaDownloadDidFinishNotification = @"kMediaDownloadDidFinishNotification";
NSString *const kMediaDownloadDidFailNotification = @"kMediaDownloadDidFailNotification";
NSString *const kMediaManagerProgressRateKey = @"kMediaManagerProgressRateKey";
NSString *const kMediaManagerProgressStringKey = @"kMediaManagerProgressStringKey";
NSString *const kMediaManagerProgressRemaingTimeKey = @"kMediaManagerProgressRemaingTimeKey";
@ -49,15 +52,47 @@ static MediaManager *sharedMediaManager = nil;
CFAbsoluteTime downloadStartTime;
CFAbsoluteTime lastProgressEventTimeStamp;
NSTimer* progressCheckTimer;
// keep to a copy of the upload dict
NSMutableDictionary* uploadDict;
}
+ (MediaLoader*)mediaLoaderForURL:(NSString*)url;
@property (strong, nonatomic) NSMutableDictionary* downloadStatsDict;
@end
#pragma mark - MediaLoader
@implementation MediaLoader
@synthesize downloadStatsDict;
// find a MediaLoader
static NSMutableDictionary* pendingMediaLoadersByURL = nil;
+ (MediaLoader*)mediaLoaderForURL:(NSString*)url {
MediaLoader* res = nil;
if (pendingMediaLoadersByURL && url) {
res = [pendingMediaLoadersByURL valueForKey:url];
}
return res;
}
+ (void) setMediaLoader:(MediaLoader*)mediaLoader forURL:(NSString*)url {
if (!pendingMediaLoadersByURL) {
pendingMediaLoadersByURL = [[NSMutableDictionary alloc] init];
}
// sanity check
if (mediaLoader && url) {
[pendingMediaLoadersByURL setValue:mediaLoader forKey:url];
}
}
+ (void) removeMediaLoaderWithUrl:(NSString*)url {
if (url) {
[pendingMediaLoadersByURL removeObjectForKey:url];
}
}
- (NSString*)validateContentURL:(NSString*)contentURL {
// Detect matrix content url
@ -70,27 +105,37 @@ static MediaManager *sharedMediaManager = nil;
return contentURL;
}
- (void)downloadPicture:(NSString*)pictureURL
success:(blockMediaManager_onImageReady)success
failure:(blockMediaManager_onError)failure {
[MediaLoader setMediaLoader:self forURL:pictureURL];
// Download picture content
[self downloadMedia:pictureURL mimeType:@"image/jpeg" success:^(NSString *cacheFilePath) {
[MediaLoader removeMediaLoaderWithUrl:pictureURL];
if (success) {
NSData* imageContent = [NSData dataWithContentsOfFile:cacheFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
if (imageContent) {
UIImage *image = [UIImage imageWithData:imageContent];
if (image) {
success(image);
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFinishNotification object:pictureURL userInfo:nil];
} else {
NSLog(@"ERROR: picture download failed: %@", pictureURL);
if (failure){
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:pictureURL userInfo:nil];
failure(nil);
}
}
}
}
} failure:^(NSError *error) {
[MediaLoader removeMediaLoaderWithUrl:pictureURL];
failure(error);
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:pictureURL userInfo:nil];
}];
}
@ -107,6 +152,8 @@ static MediaManager *sharedMediaManager = nil;
downloadStartTime = statsStartTime = CFAbsoluteTimeGetCurrent();
lastProgressEventTimeStamp = -1;
[MediaLoader setMediaLoader:self forURL:mediaURL];
// Start downloading
NSURL *url = [NSURL URLWithString:[self validateContentURL:aMediaURL]];
downloadData = [[NSMutableData alloc] init];
@ -114,6 +161,7 @@ static MediaManager *sharedMediaManager = nil;
}
- (void)cancel {
[MediaLoader removeMediaLoaderWithUrl:mediaURL];
// Reset blocks
onMediaReady = nil;
onError = nil;
@ -138,9 +186,13 @@ static MediaManager *sharedMediaManager = nil;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"ERROR: media download failed: %@, %@", error, mediaURL);
[MediaLoader removeMediaLoaderWithUrl:mediaURL];
// send the latest known upload info
[self progressCheckTimeout:nil];
uploadDict = nil;
downloadStatsDict = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:mediaURL userInfo:nil];
if (onError) {
onError (error);
@ -184,15 +236,13 @@ static MediaManager *sharedMediaManager = nil;
statsStartTime = currentTime;
// only one parameter by now
// build the user info dictionary
NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
[dict setValue:[NSNumber numberWithFloat:rate] forKey:kMediaManagerProgressRateKey];
NSString* progressString = [NSString stringWithFormat:@"%@ / %@", [NSByteCountFormatter stringFromByteCount:downloadData.length countStyle:NSByteCountFormatterCountStyleFile], [NSByteCountFormatter stringFromByteCount:expectedSize countStyle:NSByteCountFormatterCountStyleFile]];
[dict setValue:progressString forKey:kMediaManagerProgressStringKey];
NSMutableString* remaingTimeStr = [[NSMutableString alloc] init];
if (dataRemainingTime < 1) {
@ -218,7 +268,7 @@ static MediaManager *sharedMediaManager = nil;
NSString* downloadRateStr = [NSString stringWithFormat:@"%@/s", [NSByteCountFormatter stringFromByteCount:meanRate * 1024 countStyle:NSByteCountFormatterCountStyleFile]];
[dict setValue:downloadRateStr forKey:kMediaManagerProgressDownloadRateKey];
uploadDict = dict;
downloadStatsDict = dict;
// after 0.1s, resend the progress info
// the upload can be stuck
@ -228,8 +278,7 @@ static MediaManager *sharedMediaManager = nil;
// trigger the event only each 0.1s to avoid send to many events
if ((lastProgressEventTimeStamp == -1) || ((currentTime - lastProgressEventTimeStamp) > 0.1)) {
lastProgressEventTimeStamp = currentTime;
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:uploadDict];
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:downloadStatsDict];
}
}
}
@ -237,18 +286,19 @@ static MediaManager *sharedMediaManager = nil;
- (IBAction) progressCheckTimeout:(id)sender
{
// remove the bitrate -> can be invalid
[uploadDict removeObjectForKey:kMediaManagerProgressDownloadRateKey];
[downloadStatsDict removeObjectForKey:kMediaManagerProgressDownloadRateKey];
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:uploadDict];
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:downloadStatsDict];
[progressCheckTimer invalidate];
progressCheckTimer = nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[MediaLoader removeMediaLoaderWithUrl:mediaURL];
// send the latest known upload info
[self progressCheckTimeout:nil];
uploadDict = nil;
downloadStatsDict = nil;
if (downloadData.length) {
// Cache the downloaded data
@ -257,11 +307,14 @@ static MediaManager *sharedMediaManager = nil;
if (onMediaReady) {
onMediaReady(cacheFilePath);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFinishNotification object:mediaURL userInfo:nil];
} else {
NSLog(@"ERROR: media download failed: %@", mediaURL);
if (onError){
onError(nil);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:mediaURL userInfo:nil];
}
downloadData = nil;
@ -375,6 +428,15 @@ static MediaManager *sharedMediaManager = nil;
return ret;
}
// try to find out a media loder from a media URL
+ (id)mediaLoaderForURL:(NSString*)url {
return [MediaLoader mediaLoaderForURL:url];
}
+ (NSDictionary*)downloadStatsDict:(id)mediaLoader {
return ((MediaLoader*)mediaLoader).downloadStatsDict;
}
+ (void)cancel:(id)mediaLoader {
[((MediaLoader*)mediaLoader) cancel];
}

View file

@ -79,13 +79,13 @@
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil];
[self stopActivityIndicator];
if (imageLoader) {
[MediaManager cancel:imageLoader];
imageLoader = nil;
}
imageLoader = nil;
if (loadingView) {
[loadingView removeFromSuperview];
loadingView = nil;
@ -289,6 +289,8 @@
[super removeFromSuperview];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil];
if (useFullScreen) {
[UIApplication sharedApplication].statusBarHidden = NO;
@ -435,13 +437,13 @@
// it could be triggered after a screen rotation, new message ...
return;
}
// reinit parameters
imageLoader = nil;
downloadingImageURL = nil;
// Cancel media loader in progress (if any)
if (imageLoader) {
[MediaManager cancel:imageLoader];
imageLoader = nil;
downloadingImageURL = nil;
}
// remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
// preview image until the image is loaded
self.image = previewImage;
@ -459,21 +461,102 @@
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadProgress:) name:kMediaDownloadProgressNotification object:nil];
imageLoader = [MediaManager loadPicture:downloadingImageURL
success:^(UIImage *anImage) {
downloadingImageURL = nil;
[self stopActivityIndicator];
self.image = anImage;
loadedImageURL = anImageURL;
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
}
failure:^(NSError *error) {
[self stopActivityIndicator];
downloadingImageURL = nil;
NSLog(@"Failed to download image (%@): %@", anImageURL, error);
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
}];
id loader = [MediaManager mediaLoaderForURL:downloadingImageURL];
// there is no pending request to this URL
if (!loader) {
imageLoader = [MediaManager loadPicture:downloadingImageURL
success:^(UIImage *anImage) {
downloadingImageURL = nil;
[self stopActivityIndicator];
self.image = anImage;
loadedImageURL = anImageURL;
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
}
failure:^(NSError *error) {
[self stopActivityIndicator];
downloadingImageURL = nil;
NSLog(@"Failed to download image (%@): %@", anImageURL, error);
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
}];
} else {
// update the progress UI with the current info
[self updateProgressUI:[MediaManager downloadStatsDict:loader]];
// wait that the download is ended
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
}
}
}
- (void)onMediaDownloadEnd:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:downloadingImageURL]) {
[self stopActivityIndicator];
// update the image
UIImage* image = [MediaManager loadCachePicture:downloadingImageURL];
if (image) {
self.image = image;
}
// updates the statuses
loadedImageURL = downloadingImageURL;
downloadingImageURL = nil;
// remove the observers
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil];
}
}
}
- (void)updateProgressUI:(NSDictionary*)downloadStatsDict {
NSNumber* progressNumber = [downloadStatsDict valueForKey:kMediaManagerProgressRateKey];
if (progressNumber) {
pieChartView.progress = progressNumber.floatValue;
waitingDownloadSpinner.hidden = YES;
}
if (progressInfoLabel) {
NSString* downloadRate = [downloadStatsDict valueForKey:kMediaManagerProgressDownloadRateKey];
NSString* remaingTime = [downloadStatsDict valueForKey:kMediaManagerProgressRemaingTimeKey];
NSString* progressString = [downloadStatsDict valueForKey:kMediaManagerProgressStringKey];
NSMutableString* text = [[NSMutableString alloc] init];
[text appendString:progressString];
if (remaingTime) {
[text appendFormat:@" (%@)", remaingTime];
}
if (downloadRate) {
[text appendFormat:@"\n %@", downloadRate];
}
progressInfoLabel.text = text;
// on multilines, sizeToFit uses the current width
// so reset it
progressInfoLabel.frame = CGRectZero;
[progressInfoLabel sizeToFit];
//
CGRect progressInfoLabelFrame = progressInfoLabel.frame;
progressInfoLabelFrame.origin.x = self.center.x - (progressInfoLabelFrame.size.width / 2);
progressInfoLabelFrame.origin.y = 10 + loadingView.frame.origin.y + loadingView.frame.size.height;
progressInfoLabel.frame = progressInfoLabelFrame;
}
}
@ -483,44 +566,7 @@
NSString* url = notif.object;
if ([url isEqualToString:downloadingImageURL]) {
NSNumber* progressNumber = [notif.userInfo valueForKey:kMediaManagerProgressRateKey];
if (progressNumber) {
pieChartView.progress = progressNumber.floatValue;
waitingDownloadSpinner.hidden = YES;
}
if (progressInfoLabel) {
NSString* downloadRate = [notif.userInfo valueForKey:kMediaManagerProgressDownloadRateKey];
NSString* remaingTime = [notif.userInfo valueForKey:kMediaManagerProgressRemaingTimeKey];
NSString* progressString = [notif.userInfo valueForKey:kMediaManagerProgressStringKey];
NSMutableString* text = [[NSMutableString alloc] init];
[text appendString:progressString];
if (remaingTime) {
[text appendFormat:@" (%@)", remaingTime];
}
if (downloadRate) {
[text appendFormat:@"\n %@", downloadRate];
}
progressInfoLabel.text = text;
// on multilines, sizeToFit uses the current width
// so reset it
progressInfoLabel.frame = CGRectZero;
[progressInfoLabel sizeToFit];
//
CGRect progressInfoLabelFrame = progressInfoLabel.frame;
progressInfoLabelFrame.origin.x = self.center.x - (progressInfoLabelFrame.size.width / 2);
progressInfoLabelFrame.origin.y = 10 + loadingView.frame.origin.y + loadingView.frame.size.height;
progressInfoLabel.frame = progressInfoLabelFrame;
}
[self updateProgressUI:notif.userInfo];
}
}
}