461 lines
19 KiB
Objective-C
461 lines
19 KiB
Objective-C
/*
|
|
* This file is part of the SDWebImage package.
|
|
* (c) Olivier Poitrey <rs@dailymotion.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
#import "UIView+WebCache.h"
|
|
#import "objc/runtime.h"
|
|
#import "UIView+WebCacheOperation.h"
|
|
#import "SDWebImageError.h"
|
|
#import "SDInternalMacros.h"
|
|
#import "SDWebImageTransitionInternal.h"
|
|
#import "SDImageCache.h"
|
|
|
|
const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
|
|
|
|
@implementation UIView (WebCache)
|
|
|
|
- (nullable NSString *)sd_latestOperationKey {
|
|
return objc_getAssociatedObject(self, @selector(sd_latestOperationKey));
|
|
}
|
|
|
|
- (void)setSd_latestOperationKey:(NSString * _Nullable)sd_latestOperationKey {
|
|
objc_setAssociatedObject(self, @selector(sd_latestOperationKey), sd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
|
}
|
|
|
|
#pragma mark - State
|
|
|
|
- (NSURL *)sd_imageURL {
|
|
return [self sd_imageLoadStateForKey:self.sd_latestOperationKey].url;
|
|
}
|
|
|
|
- (NSProgress *)sd_imageProgress {
|
|
SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:self.sd_latestOperationKey];
|
|
NSProgress *progress = loadState.progress;
|
|
if (!progress) {
|
|
progress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
|
|
self.sd_imageProgress = progress;
|
|
}
|
|
return progress;
|
|
}
|
|
|
|
- (void)setSd_imageProgress:(NSProgress *)sd_imageProgress {
|
|
if (!sd_imageProgress) {
|
|
return;
|
|
}
|
|
SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:self.sd_latestOperationKey];
|
|
if (!loadState) {
|
|
loadState = [SDWebImageLoadState new];
|
|
}
|
|
loadState.progress = sd_imageProgress;
|
|
[self sd_setImageLoadState:loadState forKey:self.sd_latestOperationKey];
|
|
}
|
|
|
|
- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
|
|
placeholderImage:(nullable UIImage *)placeholder
|
|
options:(SDWebImageOptions)options
|
|
context:(nullable SDWebImageContext *)context
|
|
setImageBlock:(nullable SDSetImageBlock)setImageBlock
|
|
progress:(nullable SDImageLoaderProgressBlock)progressBlock
|
|
completed:(nullable SDInternalCompletionBlock)completedBlock {
|
|
if (context) {
|
|
// copy to avoid mutable object
|
|
context = [context copy];
|
|
} else {
|
|
context = [NSDictionary dictionary];
|
|
}
|
|
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
|
|
if (!validOperationKey) {
|
|
// pass through the operation key to downstream, which can used for tracing operation or image view class
|
|
validOperationKey = NSStringFromClass([self class]);
|
|
SDWebImageMutableContext *mutableContext = [context mutableCopy];
|
|
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
|
|
context = [mutableContext copy];
|
|
}
|
|
self.sd_latestOperationKey = validOperationKey;
|
|
if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) {
|
|
// cancel previous loading for the same set-image operation key by default
|
|
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
|
|
}
|
|
SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:validOperationKey];
|
|
if (!loadState) {
|
|
loadState = [SDWebImageLoadState new];
|
|
}
|
|
loadState.url = url;
|
|
[self sd_setImageLoadState:loadState forKey:validOperationKey];
|
|
|
|
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
|
|
if (!manager) {
|
|
manager = [SDWebImageManager sharedManager];
|
|
} else {
|
|
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
|
|
SDWebImageMutableContext *mutableContext = [context mutableCopy];
|
|
mutableContext[SDWebImageContextCustomManager] = nil;
|
|
context = [mutableContext copy];
|
|
}
|
|
|
|
BOOL shouldUseWeakCache = NO;
|
|
if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
|
|
shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
|
|
}
|
|
if (!(options & SDWebImageDelayPlaceholder)) {
|
|
if (shouldUseWeakCache) {
|
|
NSString *key = [manager cacheKeyForURL:url context:context];
|
|
// call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query
|
|
// this unfortunately will cause twice memory cache query, but it's fast enough
|
|
// in the future the weak cache feature may be re-design or removed
|
|
[((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
|
|
}
|
|
dispatch_main_async_safe(^{
|
|
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
|
|
});
|
|
}
|
|
|
|
id <SDWebImageOperation> operation = nil;
|
|
|
|
if (url) {
|
|
// reset the progress
|
|
NSProgress *imageProgress = loadState.progress;
|
|
if (imageProgress) {
|
|
imageProgress.totalUnitCount = 0;
|
|
imageProgress.completedUnitCount = 0;
|
|
}
|
|
|
|
#if SD_UIKIT || SD_MAC
|
|
// check and start image indicator
|
|
[self sd_startImageIndicator];
|
|
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
|
|
#endif
|
|
|
|
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
|
|
if (imageProgress) {
|
|
imageProgress.totalUnitCount = expectedSize;
|
|
imageProgress.completedUnitCount = receivedSize;
|
|
}
|
|
#if SD_UIKIT || SD_MAC
|
|
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
|
|
double progress = 0;
|
|
if (expectedSize != 0) {
|
|
progress = (double)receivedSize / expectedSize;
|
|
}
|
|
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[imageIndicator updateIndicatorProgress:progress];
|
|
});
|
|
}
|
|
#endif
|
|
if (progressBlock) {
|
|
progressBlock(receivedSize, expectedSize, targetURL);
|
|
}
|
|
};
|
|
@weakify(self);
|
|
operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
|
|
@strongify(self);
|
|
if (!self) { return; }
|
|
// if the progress not been updated, mark it to complete state
|
|
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
|
|
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
|
|
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
|
|
}
|
|
|
|
#if SD_UIKIT || SD_MAC
|
|
// check and stop image indicator
|
|
if (finished) {
|
|
[self sd_stopImageIndicator];
|
|
}
|
|
#endif
|
|
|
|
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
|
|
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
|
|
(!image && !(options & SDWebImageDelayPlaceholder)));
|
|
SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
|
|
if (!self) { return; }
|
|
if (!shouldNotSetImage) {
|
|
[self sd_setNeedsLayout];
|
|
}
|
|
if (completedBlock && shouldCallCompletedBlock) {
|
|
completedBlock(image, data, error, cacheType, finished, url);
|
|
}
|
|
};
|
|
|
|
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
|
|
// OR
|
|
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
|
|
if (shouldNotSetImage) {
|
|
dispatch_main_async_safe(callCompletedBlockClosure);
|
|
return;
|
|
}
|
|
|
|
UIImage *targetImage = nil;
|
|
NSData *targetData = nil;
|
|
if (image) {
|
|
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
|
|
targetImage = image;
|
|
targetData = data;
|
|
} else if (options & SDWebImageDelayPlaceholder) {
|
|
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
|
|
targetImage = placeholder;
|
|
targetData = nil;
|
|
}
|
|
|
|
#if SD_UIKIT || SD_MAC
|
|
// check whether we should use the image transition
|
|
SDWebImageTransition *transition = nil;
|
|
BOOL shouldUseTransition = NO;
|
|
if (options & SDWebImageForceTransition) {
|
|
// Always
|
|
shouldUseTransition = YES;
|
|
} else if (cacheType == SDImageCacheTypeNone) {
|
|
// From network
|
|
shouldUseTransition = YES;
|
|
} else {
|
|
// From disk (and, user don't use sync query)
|
|
if (cacheType == SDImageCacheTypeMemory) {
|
|
shouldUseTransition = NO;
|
|
} else if (cacheType == SDImageCacheTypeDisk) {
|
|
if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
|
|
shouldUseTransition = NO;
|
|
} else {
|
|
shouldUseTransition = YES;
|
|
}
|
|
} else {
|
|
// Not valid cache type, fallback
|
|
shouldUseTransition = NO;
|
|
}
|
|
}
|
|
if (finished && shouldUseTransition) {
|
|
transition = self.sd_imageTransition;
|
|
}
|
|
#endif
|
|
dispatch_main_async_safe(^{
|
|
#if SD_UIKIT || SD_MAC
|
|
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
|
|
#else
|
|
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
|
|
#endif
|
|
callCompletedBlockClosure();
|
|
});
|
|
}];
|
|
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
|
|
} else {
|
|
#if SD_UIKIT || SD_MAC
|
|
[self sd_stopImageIndicator];
|
|
#endif
|
|
if (completedBlock) {
|
|
dispatch_main_async_safe(^{
|
|
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
|
|
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
|
|
});
|
|
}
|
|
}
|
|
|
|
return operation;
|
|
}
|
|
|
|
- (void)sd_cancelCurrentImageLoad {
|
|
[self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey];
|
|
}
|
|
|
|
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
|
|
#if SD_UIKIT || SD_MAC
|
|
[self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL];
|
|
#else
|
|
// watchOS does not support view transition. Simplify the logic
|
|
if (setImageBlock) {
|
|
setImageBlock(image, imageData, cacheType, imageURL);
|
|
} else if ([self isKindOfClass:[UIImageView class]]) {
|
|
UIImageView *imageView = (UIImageView *)self;
|
|
[imageView setImage:image];
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if SD_UIKIT || SD_MAC
|
|
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
|
|
UIView *view = self;
|
|
SDSetImageBlock finalSetImageBlock;
|
|
if (setImageBlock) {
|
|
finalSetImageBlock = setImageBlock;
|
|
} else if ([view isKindOfClass:[UIImageView class]]) {
|
|
UIImageView *imageView = (UIImageView *)view;
|
|
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
|
|
imageView.image = setImage;
|
|
};
|
|
}
|
|
#if SD_UIKIT
|
|
else if ([view isKindOfClass:[UIButton class]]) {
|
|
UIButton *button = (UIButton *)view;
|
|
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
|
|
[button setImage:setImage forState:UIControlStateNormal];
|
|
};
|
|
}
|
|
#endif
|
|
#if SD_MAC
|
|
else if ([view isKindOfClass:[NSButton class]]) {
|
|
NSButton *button = (NSButton *)view;
|
|
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
|
|
button.image = setImage;
|
|
};
|
|
}
|
|
#endif
|
|
|
|
if (transition) {
|
|
NSString *originalOperationKey = view.sd_latestOperationKey;
|
|
|
|
#if SD_UIKIT
|
|
[UIView transitionWithView:view duration:0 options:0 animations:^{
|
|
if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
|
|
return;
|
|
}
|
|
// 0 duration to let UIKit render placeholder and prepares block
|
|
if (transition.prepares) {
|
|
transition.prepares(view, image, imageData, cacheType, imageURL);
|
|
}
|
|
} completion:^(BOOL tempFinished) {
|
|
[UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
|
|
if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
|
|
return;
|
|
}
|
|
if (finalSetImageBlock && !transition.avoidAutoSetImage) {
|
|
finalSetImageBlock(image, imageData, cacheType, imageURL);
|
|
}
|
|
if (transition.animations) {
|
|
transition.animations(view, image);
|
|
}
|
|
} completion:^(BOOL finished) {
|
|
if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
|
|
return;
|
|
}
|
|
if (transition.completion) {
|
|
transition.completion(finished);
|
|
}
|
|
}];
|
|
}];
|
|
#elif SD_MAC
|
|
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
|
|
if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
|
|
return;
|
|
}
|
|
// 0 duration to let AppKit render placeholder and prepares block
|
|
prepareContext.duration = 0;
|
|
if (transition.prepares) {
|
|
transition.prepares(view, image, imageData, cacheType, imageURL);
|
|
}
|
|
} completionHandler:^{
|
|
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
|
|
if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
|
|
return;
|
|
}
|
|
context.duration = transition.duration;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
CAMediaTimingFunction *timingFunction = transition.timingFunction;
|
|
#pragma clang diagnostic pop
|
|
if (!timingFunction) {
|
|
timingFunction = SDTimingFunctionFromAnimationOptions(transition.animationOptions);
|
|
}
|
|
context.timingFunction = timingFunction;
|
|
context.allowsImplicitAnimation = SD_OPTIONS_CONTAINS(transition.animationOptions, SDWebImageAnimationOptionAllowsImplicitAnimation);
|
|
if (finalSetImageBlock && !transition.avoidAutoSetImage) {
|
|
finalSetImageBlock(image, imageData, cacheType, imageURL);
|
|
}
|
|
CATransition *trans = SDTransitionFromAnimationOptions(transition.animationOptions);
|
|
if (trans) {
|
|
[view.layer addAnimation:trans forKey:kCATransition];
|
|
}
|
|
if (transition.animations) {
|
|
transition.animations(view, image);
|
|
}
|
|
} completionHandler:^{
|
|
if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
|
|
return;
|
|
}
|
|
if (transition.completion) {
|
|
transition.completion(YES);
|
|
}
|
|
}];
|
|
}];
|
|
#endif
|
|
} else {
|
|
if (finalSetImageBlock) {
|
|
finalSetImageBlock(image, imageData, cacheType, imageURL);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
- (void)sd_setNeedsLayout {
|
|
#if SD_UIKIT
|
|
[self setNeedsLayout];
|
|
#elif SD_MAC
|
|
[self setNeedsLayout:YES];
|
|
#elif SD_WATCH
|
|
// Do nothing because WatchKit automatically layout the view after property change
|
|
#endif
|
|
}
|
|
|
|
#if SD_UIKIT || SD_MAC
|
|
|
|
#pragma mark - Image Transition
|
|
- (SDWebImageTransition *)sd_imageTransition {
|
|
return objc_getAssociatedObject(self, @selector(sd_imageTransition));
|
|
}
|
|
|
|
- (void)setSd_imageTransition:(SDWebImageTransition *)sd_imageTransition {
|
|
objc_setAssociatedObject(self, @selector(sd_imageTransition), sd_imageTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
#pragma mark - Indicator
|
|
- (id<SDWebImageIndicator>)sd_imageIndicator {
|
|
return objc_getAssociatedObject(self, @selector(sd_imageIndicator));
|
|
}
|
|
|
|
- (void)setSd_imageIndicator:(id<SDWebImageIndicator>)sd_imageIndicator {
|
|
// Remove the old indicator view
|
|
id<SDWebImageIndicator> previousIndicator = self.sd_imageIndicator;
|
|
[previousIndicator.indicatorView removeFromSuperview];
|
|
|
|
objc_setAssociatedObject(self, @selector(sd_imageIndicator), sd_imageIndicator, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
|
|
// Add the new indicator view
|
|
UIView *view = sd_imageIndicator.indicatorView;
|
|
if (CGRectEqualToRect(view.frame, CGRectZero)) {
|
|
view.frame = self.bounds;
|
|
}
|
|
// Center the indicator view
|
|
#if SD_MAC
|
|
[view setFrameOrigin:CGPointMake(round((NSWidth(self.bounds) - NSWidth(view.frame)) / 2), round((NSHeight(self.bounds) - NSHeight(view.frame)) / 2))];
|
|
#else
|
|
view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
|
|
#endif
|
|
view.hidden = NO;
|
|
[self addSubview:view];
|
|
}
|
|
|
|
- (void)sd_startImageIndicator {
|
|
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
|
|
if (!imageIndicator) {
|
|
return;
|
|
}
|
|
dispatch_main_async_safe(^{
|
|
[imageIndicator startAnimatingIndicator];
|
|
});
|
|
}
|
|
|
|
- (void)sd_stopImageIndicator {
|
|
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
|
|
if (!imageIndicator) {
|
|
return;
|
|
}
|
|
dispatch_main_async_safe(^{
|
|
[imageIndicator stopAnimatingIndicator];
|
|
});
|
|
}
|
|
|
|
#endif
|
|
|
|
@end
|