1/* 2 * This file is part of the SDWebImage package. 3 * (c) Olivier Poitrey <rs@dailymotion.com> 4 * 5 * For the full copyright and license information, please view the LICENSE 6 * file that was distributed with this source code. 7 */ 8 9#import "UIView+WebCache.h" 10#import "objc/runtime.h" 11#import "UIView+WebCacheOperation.h" 12#import "SDWebImageError.h" 13#import "SDInternalMacros.h" 14#import "SDWebImageTransitionInternal.h" 15 16const int64_t SDWebImageProgressUnitCountUnknown = 1LL; 17 18@implementation UIView (WebCache) 19 20- (nullable NSURL *)sd_imageURL { 21 return objc_getAssociatedObject(self, @selector(sd_imageURL)); 22} 23 24- (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL { 25 objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 26} 27 28- (nullable NSString *)sd_latestOperationKey { 29 return objc_getAssociatedObject(self, @selector(sd_latestOperationKey)); 30} 31 32- (void)setSd_latestOperationKey:(NSString * _Nullable)sd_latestOperationKey { 33 objc_setAssociatedObject(self, @selector(sd_latestOperationKey), sd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC); 34} 35 36- (NSProgress *)sd_imageProgress { 37 NSProgress *progress = objc_getAssociatedObject(self, @selector(sd_imageProgress)); 38 if (!progress) { 39 progress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; 40 self.sd_imageProgress = progress; 41 } 42 return progress; 43} 44 45- (void)setSd_imageProgress:(NSProgress *)sd_imageProgress { 46 objc_setAssociatedObject(self, @selector(sd_imageProgress), sd_imageProgress, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 47} 48 49- (void)sd_internalSetImageWithURL:(nullable NSURL *)url 50 placeholderImage:(nullable UIImage *)placeholder 51 options:(SDWebImageOptions)options 52 context:(nullable SDWebImageContext *)context 53 setImageBlock:(nullable SDSetImageBlock)setImageBlock 54 progress:(nullable SDImageLoaderProgressBlock)progressBlock 55 completed:(nullable SDInternalCompletionBlock)completedBlock { 56 if (context) { 57 // copy to avoid mutable object 58 context = [context copy]; 59 } else { 60 context = [NSDictionary dictionary]; 61 } 62 NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; 63 if (!validOperationKey) { 64 // pass through the operation key to downstream, which can used for tracing operation or image view class 65 validOperationKey = NSStringFromClass([self class]); 66 SDWebImageMutableContext *mutableContext = [context mutableCopy]; 67 mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey; 68 context = [mutableContext copy]; 69 } 70 self.sd_latestOperationKey = validOperationKey; 71 [self sd_cancelImageLoadOperationWithKey:validOperationKey]; 72 self.sd_imageURL = url; 73 74 if (!(options & SDWebImageDelayPlaceholder)) { 75 dispatch_main_async_safe(^{ 76 [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url]; 77 }); 78 } 79 80 if (url) { 81 // reset the progress 82 NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress)); 83 if (imageProgress) { 84 imageProgress.totalUnitCount = 0; 85 imageProgress.completedUnitCount = 0; 86 } 87 88#if SD_UIKIT || SD_MAC 89 // check and start image indicator 90 [self sd_startImageIndicator]; 91 id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator; 92#endif 93 SDWebImageManager *manager = context[SDWebImageContextCustomManager]; 94 if (!manager) { 95 manager = [SDWebImageManager sharedManager]; 96 } else { 97 // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager) 98 SDWebImageMutableContext *mutableContext = [context mutableCopy]; 99 mutableContext[SDWebImageContextCustomManager] = nil; 100 context = [mutableContext copy]; 101 } 102 103 SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { 104 if (imageProgress) { 105 imageProgress.totalUnitCount = expectedSize; 106 imageProgress.completedUnitCount = receivedSize; 107 } 108#if SD_UIKIT || SD_MAC 109 if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) { 110 double progress = 0; 111 if (expectedSize != 0) { 112 progress = (double)receivedSize / expectedSize; 113 } 114 progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0 115 dispatch_async(dispatch_get_main_queue(), ^{ 116 [imageIndicator updateIndicatorProgress:progress]; 117 }); 118 } 119#endif 120 if (progressBlock) { 121 progressBlock(receivedSize, expectedSize, targetURL); 122 } 123 }; 124 @weakify(self); 125 id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { 126 @strongify(self); 127 if (!self) { return; } 128 // if the progress not been updated, mark it to complete state 129 if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) { 130 imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown; 131 imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown; 132 } 133 134#if SD_UIKIT || SD_MAC 135 // check and stop image indicator 136 if (finished) { 137 [self sd_stopImageIndicator]; 138 } 139#endif 140 141 BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); 142 BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || 143 (!image && !(options & SDWebImageDelayPlaceholder))); 144 SDWebImageNoParamsBlock callCompletedBlockClosure = ^{ 145 if (!self) { return; } 146 if (!shouldNotSetImage) { 147 [self sd_setNeedsLayout]; 148 } 149 if (completedBlock && shouldCallCompletedBlock) { 150 completedBlock(image, data, error, cacheType, finished, url); 151 } 152 }; 153 154 // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set 155 // OR 156 // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set 157 if (shouldNotSetImage) { 158 dispatch_main_async_safe(callCompletedBlockClosure); 159 return; 160 } 161 162 UIImage *targetImage = nil; 163 NSData *targetData = nil; 164 if (image) { 165 // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set 166 targetImage = image; 167 targetData = data; 168 } else if (options & SDWebImageDelayPlaceholder) { 169 // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set 170 targetImage = placeholder; 171 targetData = nil; 172 } 173 174#if SD_UIKIT || SD_MAC 175 // check whether we should use the image transition 176 SDWebImageTransition *transition = nil; 177 BOOL shouldUseTransition = NO; 178 if (options & SDWebImageForceTransition) { 179 // Always 180 shouldUseTransition = YES; 181 } else if (cacheType == SDImageCacheTypeNone) { 182 // From network 183 shouldUseTransition = YES; 184 } else { 185 // From disk (and, user don't use sync query) 186 if (cacheType == SDImageCacheTypeMemory) { 187 shouldUseTransition = NO; 188 } else if (cacheType == SDImageCacheTypeDisk) { 189 if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) { 190 shouldUseTransition = NO; 191 } else { 192 shouldUseTransition = YES; 193 } 194 } else { 195 // Not valid cache type, fallback 196 shouldUseTransition = NO; 197 } 198 } 199 if (finished && shouldUseTransition) { 200 transition = self.sd_imageTransition; 201 } 202#endif 203 dispatch_main_async_safe(^{ 204#if SD_UIKIT || SD_MAC 205 [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL]; 206#else 207 [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL]; 208#endif 209 callCompletedBlockClosure(); 210 }); 211 }]; 212 [self sd_setImageLoadOperation:operation forKey:validOperationKey]; 213 } else { 214#if SD_UIKIT || SD_MAC 215 [self sd_stopImageIndicator]; 216#endif 217 dispatch_main_async_safe(^{ 218 if (completedBlock) { 219 NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}]; 220 completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url); 221 } 222 }); 223 } 224} 225 226- (void)sd_cancelCurrentImageLoad { 227 [self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey]; 228 self.sd_latestOperationKey = nil; 229} 230 231- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL { 232#if SD_UIKIT || SD_MAC 233 [self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL]; 234#else 235 // watchOS does not support view transition. Simplify the logic 236 if (setImageBlock) { 237 setImageBlock(image, imageData, cacheType, imageURL); 238 } else if ([self isKindOfClass:[UIImageView class]]) { 239 UIImageView *imageView = (UIImageView *)self; 240 [imageView setImage:image]; 241 } 242#endif 243} 244 245#if SD_UIKIT || SD_MAC 246- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL { 247 UIView *view = self; 248 SDSetImageBlock finalSetImageBlock; 249 if (setImageBlock) { 250 finalSetImageBlock = setImageBlock; 251 } else if ([view isKindOfClass:[UIImageView class]]) { 252 UIImageView *imageView = (UIImageView *)view; 253 finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { 254 imageView.image = setImage; 255 }; 256 } 257#if SD_UIKIT 258 else if ([view isKindOfClass:[UIButton class]]) { 259 UIButton *button = (UIButton *)view; 260 finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { 261 [button setImage:setImage forState:UIControlStateNormal]; 262 }; 263 } 264#endif 265#if SD_MAC 266 else if ([view isKindOfClass:[NSButton class]]) { 267 NSButton *button = (NSButton *)view; 268 finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { 269 button.image = setImage; 270 }; 271 } 272#endif 273 274 if (transition) { 275 NSString *originalOperationKey = view.sd_latestOperationKey; 276 277#if SD_UIKIT 278 [UIView transitionWithView:view duration:0 options:0 animations:^{ 279 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { 280 return; 281 } 282 // 0 duration to let UIKit render placeholder and prepares block 283 if (transition.prepares) { 284 transition.prepares(view, image, imageData, cacheType, imageURL); 285 } 286 } completion:^(BOOL finished) { 287 [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{ 288 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { 289 return; 290 } 291 if (finalSetImageBlock && !transition.avoidAutoSetImage) { 292 finalSetImageBlock(image, imageData, cacheType, imageURL); 293 } 294 if (transition.animations) { 295 transition.animations(view, image); 296 } 297 } completion:^(BOOL finished) { 298 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { 299 return; 300 } 301 if (transition.completion) { 302 transition.completion(finished); 303 } 304 }]; 305 }]; 306#elif SD_MAC 307 [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) { 308 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { 309 return; 310 } 311 // 0 duration to let AppKit render placeholder and prepares block 312 prepareContext.duration = 0; 313 if (transition.prepares) { 314 transition.prepares(view, image, imageData, cacheType, imageURL); 315 } 316 } completionHandler:^{ 317 [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { 318 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { 319 return; 320 } 321 context.duration = transition.duration; 322#pragma clang diagnostic push 323#pragma clang diagnostic ignored "-Wdeprecated-declarations" 324 CAMediaTimingFunction *timingFunction = transition.timingFunction; 325#pragma clang diagnostic pop 326 if (!timingFunction) { 327 timingFunction = SDTimingFunctionFromAnimationOptions(transition.animationOptions); 328 } 329 context.timingFunction = timingFunction; 330 context.allowsImplicitAnimation = SD_OPTIONS_CONTAINS(transition.animationOptions, SDWebImageAnimationOptionAllowsImplicitAnimation); 331 if (finalSetImageBlock && !transition.avoidAutoSetImage) { 332 finalSetImageBlock(image, imageData, cacheType, imageURL); 333 } 334 CATransition *trans = SDTransitionFromAnimationOptions(transition.animationOptions); 335 if (trans) { 336 [view.layer addAnimation:trans forKey:kCATransition]; 337 } 338 if (transition.animations) { 339 transition.animations(view, image); 340 } 341 } completionHandler:^{ 342 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { 343 return; 344 } 345 if (transition.completion) { 346 transition.completion(YES); 347 } 348 }]; 349 }]; 350#endif 351 } else { 352 if (finalSetImageBlock) { 353 finalSetImageBlock(image, imageData, cacheType, imageURL); 354 } 355 } 356} 357#endif 358 359- (void)sd_setNeedsLayout { 360#if SD_UIKIT 361 [self setNeedsLayout]; 362#elif SD_MAC 363 [self setNeedsLayout:YES]; 364#elif SD_WATCH 365 // Do nothing because WatchKit automatically layout the view after property change 366#endif 367} 368 369#if SD_UIKIT || SD_MAC 370 371#pragma mark - Image Transition 372- (SDWebImageTransition *)sd_imageTransition { 373 return objc_getAssociatedObject(self, @selector(sd_imageTransition)); 374} 375 376- (void)setSd_imageTransition:(SDWebImageTransition *)sd_imageTransition { 377 objc_setAssociatedObject(self, @selector(sd_imageTransition), sd_imageTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 378} 379 380#pragma mark - Indicator 381- (id<SDWebImageIndicator>)sd_imageIndicator { 382 return objc_getAssociatedObject(self, @selector(sd_imageIndicator)); 383} 384 385- (void)setSd_imageIndicator:(id<SDWebImageIndicator>)sd_imageIndicator { 386 // Remove the old indicator view 387 id<SDWebImageIndicator> previousIndicator = self.sd_imageIndicator; 388 [previousIndicator.indicatorView removeFromSuperview]; 389 390 objc_setAssociatedObject(self, @selector(sd_imageIndicator), sd_imageIndicator, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 391 392 // Add the new indicator view 393 UIView *view = sd_imageIndicator.indicatorView; 394 if (CGRectEqualToRect(view.frame, CGRectZero)) { 395 view.frame = self.bounds; 396 } 397 // Center the indicator view 398#if SD_MAC 399 [view setFrameOrigin:CGPointMake(round((NSWidth(self.bounds) - NSWidth(view.frame)) / 2), round((NSHeight(self.bounds) - NSHeight(view.frame)) / 2))]; 400#else 401 view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 402#endif 403 view.hidden = NO; 404 [self addSubview:view]; 405} 406 407- (void)sd_startImageIndicator { 408 id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator; 409 if (!imageIndicator) { 410 return; 411 } 412 dispatch_main_async_safe(^{ 413 [imageIndicator startAnimatingIndicator]; 414 }); 415} 416 417- (void)sd_stopImageIndicator { 418 id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator; 419 if (!imageIndicator) { 420 return; 421 } 422 dispatch_main_async_safe(^{ 423 [imageIndicator stopAnimatingIndicator]; 424 }); 425} 426 427#endif 428 429@end 430