/* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * 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" NSString * const SDWebImageInternalSetImageGroupKey = @"internalSetImageGroup"; NSString * const SDWebImageExternalCustomManagerKey = @"externalCustomManager"; const int64_t SDWebImageProgressUnitCountUnknown = 1LL; static char imageURLKey; #if SD_UIKIT static char TAG_ACTIVITY_INDICATOR; static char TAG_ACTIVITY_STYLE; static char TAG_ACTIVITY_SHOW; #endif @implementation UIView (WebCache) - (nullable NSURL *)sd_imageURL { return objc_getAssociatedObject(self, &imageURLKey); } - (NSProgress *)sd_imageProgress { NSProgress *progress = objc_getAssociatedObject(self, @selector(sd_imageProgress)); if (!progress) { progress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; self.sd_imageProgress = progress; } return progress; } - (void)setSd_imageProgress:(NSProgress *)sd_imageProgress { objc_setAssociatedObject(self, @selector(sd_imageProgress), sd_imageProgress, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options operationKey:(nullable NSString *)operationKey setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil]; } - (void)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options operationKey:(nullable NSString *)operationKey setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock context:(nullable NSDictionary *)context { SDInternalSetImageBlock internalSetImageBlock; if (setImageBlock) { internalSetImageBlock = ^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { if (setImageBlock) { setImageBlock(image, imageData); } }; } [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey internalSetImageBlock:internalSetImageBlock progress:progressBlock completed:completedBlock context:context]; } - (void)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options operationKey:(nullable NSString *)operationKey internalSetImageBlock:(nullable SDInternalSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock context:(nullable NSDictionary *)context { NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]); [self sd_cancelImageLoadOperationWithKey:validOperationKey]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey]; if (!(options & SDWebImageDelayPlaceholder)) { if (group) { dispatch_group_enter(group); } dispatch_main_async_safe(^{ [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url]; }); } if (url) { #if SD_UIKIT // check if activityView is enabled or not if ([self sd_showActivityIndicatorView]) { [self sd_addActivityIndicator]; } #endif // reset the progress self.sd_imageProgress.totalUnitCount = 0; self.sd_imageProgress.completedUnitCount = 0; SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey]; if (!manager) { manager = [SDWebImageManager sharedManager]; } __weak __typeof(self)wself = self; SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { wself.sd_imageProgress.totalUnitCount = expectedSize; wself.sd_imageProgress.completedUnitCount = receivedSize; if (progressBlock) { progressBlock(receivedSize, expectedSize, targetURL); } }; id operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { __strong __typeof (wself) sself = wself; if (!sself) { return; } #if SD_UIKIT [sself sd_removeActivityIndicator]; #endif // if the progress not been updated, mark it to complete state if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) { sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown; sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown; } BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || (!image && !(options & SDWebImageDelayPlaceholder))); SDWebImageNoParamsBlock callCompletedBlockClojure = ^{ if (!sself) { return; } if (!shouldNotSetImage) { [sself sd_setNeedsLayout]; } if (completedBlock && shouldCallCompletedBlock) { completedBlock(image, error, cacheType, 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(callCompletedBlockClojure); 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; if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) { transition = sself.sd_imageTransition; } #endif dispatch_main_async_safe(^{ if (group) { dispatch_group_enter(group); } #if SD_UIKIT || SD_MAC [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL]; #else [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL]; #endif if (group) { // compatible code for FLAnimatedImage, because we assume completedBlock called after image was set. This will be removed in 5.x BOOL shouldUseGroup = [objc_getAssociatedObject(group, &SDWebImageInternalSetImageGroupKey) boolValue]; if (shouldUseGroup) { dispatch_group_notify(group, dispatch_get_main_queue(), callCompletedBlockClojure); } else { callCompletedBlockClojure(); } } else { callCompletedBlockClojure(); } }); }]; [self sd_setImageLoadOperation:operation forKey:validOperationKey]; } else { dispatch_main_async_safe(^{ #if SD_UIKIT [self sd_removeActivityIndicator]; #endif if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); } } - (void)sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:NSStringFromClass([self class])]; } - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDInternalSetImageBlock)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:(SDInternalSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL { UIView *view = self; SDInternalSetImageBlock 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 (transition) { #if SD_UIKIT [UIView transitionWithView:view duration:0 options:0 animations:^{ // 0 duration to let UIKit render placeholder and prepares block if (transition.prepares) { transition.prepares(view, image, imageData, cacheType, imageURL); } } completion:^(BOOL finished) { [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{ if (finalSetImageBlock && !transition.avoidAutoSetImage) { finalSetImageBlock(image, imageData, cacheType, imageURL); } if (transition.animations) { transition.animations(view, image); } } completion:transition.completion]; }]; #elif SD_MAC [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) { // 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) { context.duration = transition.duration; context.timingFunction = transition.timingFunction; context.allowsImplicitAnimation = (transition.animationOptions & SDWebImageAnimationOptionAllowsImplicitAnimation); if (finalSetImageBlock && !transition.avoidAutoSetImage) { finalSetImageBlock(image, imageData, cacheType, imageURL); } if (transition.animations) { transition.animations(view, image); } } completionHandler:^{ 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); } #if SD_UIKIT #pragma mark - Activity indicator - (UIActivityIndicatorView *)activityIndicator { return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR); } - (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator { objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN); } - (void)sd_setShowActivityIndicatorView:(BOOL)show { objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, @(show), OBJC_ASSOCIATION_RETAIN); } - (BOOL)sd_showActivityIndicatorView { return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue]; } - (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{ objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN); } - (int)sd_getIndicatorStyle{ return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue]; } - (void)sd_addActivityIndicator { dispatch_main_async_safe(^{ if (!self.activityIndicator) { self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:[self sd_getIndicatorStyle]]; self.activityIndicator.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:self.activityIndicator]; [self addConstraint:[NSLayoutConstraint constraintWithItem:self.activityIndicator attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]]; [self addConstraint:[NSLayoutConstraint constraintWithItem:self.activityIndicator attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]]; } [self.activityIndicator startAnimating]; }); } - (void)sd_removeActivityIndicator { dispatch_main_async_safe(^{ if (self.activityIndicator) { [self.activityIndicator removeFromSuperview]; self.activityIndicator = nil; } }); } #endif #endif @end