/* * 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" #import "SDWebImageError.h" #import "SDInternalMacros.h" const int64_t SDWebImageProgressUnitCountUnknown = 1LL; @implementation UIView (WebCache) - (nullable NSURL *)sd_imageURL { return objc_getAssociatedObject(self, @selector(sd_imageURL)); } - (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL { objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (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); } - (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 context:(nullable SDWebImageContext *)context setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { context = [context copy]; // copy to avoid mutable object NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; if (!validOperationKey) { validOperationKey = NSStringFromClass([self class]); } self.sd_latestOperationKey = validOperationKey; [self sd_cancelImageLoadOperationWithKey:validOperationKey]; self.sd_imageURL = url; if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url]; }); } if (url) { // reset the progress self.sd_imageProgress.totalUnitCount = 0; self.sd_imageProgress.completedUnitCount = 0; #if SD_UIKIT || SD_MAC // check and start image indicator [self sd_startImageIndicator]; id imageIndicator = self.sd_imageIndicator; #endif SDWebImageManager *manager = context[SDWebImageContextCustomManager]; if (!manager) { manager = [SDWebImageManager sharedManager]; } @weakify(self); SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { @strongify(self); NSProgress *imageProgress = self.sd_imageProgress; imageProgress.totalUnitCount = expectedSize; imageProgress.completedUnitCount = receivedSize; #if SD_UIKIT || SD_MAC if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) { double progress = imageProgress.fractionCompleted; dispatch_async(dispatch_get_main_queue(), ^{ [imageIndicator updateIndicatorProgress:progress]; }); } #endif if (progressBlock) { progressBlock(receivedSize, expectedSize, targetURL); } }; id 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 (finished && !error && self.sd_imageProgress.totalUnitCount == 0 && self.sd_imageProgress.completedUnitCount == 0) { self.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown; self.sd_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 callCompletedBlockClojure = ^{ 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(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 = 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 callCompletedBlockClojure(); }); }]; [self sd_setImageLoadOperation:operation forKey:validOperationKey]; } else { #if SD_UIKIT || SD_MAC [self sd_stopImageIndicator]; #endif dispatch_main_async_safe(^{ if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}]; completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url); } }); } } - (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) { #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); } #pragma mark - Indicator - (id)sd_imageIndicator { return objc_getAssociatedObject(self, @selector(sd_imageIndicator)); } - (void)setSd_imageIndicator:(id)sd_imageIndicator { // Remove the old indicator view id 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 CGPoint center = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds)); NSRect frame = view.frame; view.frame = NSMakeRect(center.x - NSMidX(frame), center.y - NSMidY(frame), NSWidth(frame), NSHeight(frame)); #else view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); #endif view.hidden = NO; [self addSubview:view]; } - (void)sd_startImageIndicator { id imageIndicator = self.sd_imageIndicator; if (!imageIndicator) { return; } dispatch_main_async_safe(^{ [imageIndicator startAnimatingIndicator]; }); } - (void)sd_stopImageIndicator { id imageIndicator = self.sd_imageIndicator; if (!imageIndicator) { return; } dispatch_main_async_safe(^{ [imageIndicator stopAnimatingIndicator]; }); } #endif @end