UIView+WebCache.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. #import "UIView+WebCache.h"
  9. #import "objc/runtime.h"
  10. #import "UIView+WebCacheOperation.h"
  11. #import "SDWebImageError.h"
  12. #import "SDInternalMacros.h"
  13. const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
  14. @implementation UIView (WebCache)
  15. - (nullable NSURL *)sd_imageURL {
  16. return objc_getAssociatedObject(self, @selector(sd_imageURL));
  17. }
  18. - (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL {
  19. objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  20. }
  21. - (nullable NSString *)sd_latestOperationKey {
  22. return objc_getAssociatedObject(self, @selector(sd_latestOperationKey));
  23. }
  24. - (void)setSd_latestOperationKey:(NSString * _Nullable)sd_latestOperationKey {
  25. objc_setAssociatedObject(self, @selector(sd_latestOperationKey), sd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC);
  26. }
  27. - (NSProgress *)sd_imageProgress {
  28. NSProgress *progress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
  29. if (!progress) {
  30. progress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
  31. self.sd_imageProgress = progress;
  32. }
  33. return progress;
  34. }
  35. - (void)setSd_imageProgress:(NSProgress *)sd_imageProgress {
  36. objc_setAssociatedObject(self, @selector(sd_imageProgress), sd_imageProgress, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  37. }
  38. - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
  39. placeholderImage:(nullable UIImage *)placeholder
  40. options:(SDWebImageOptions)options
  41. context:(nullable SDWebImageContext *)context
  42. setImageBlock:(nullable SDSetImageBlock)setImageBlock
  43. progress:(nullable SDImageLoaderProgressBlock)progressBlock
  44. completed:(nullable SDInternalCompletionBlock)completedBlock {
  45. context = [context copy]; // copy to avoid mutable object
  46. NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
  47. if (!validOperationKey) {
  48. validOperationKey = NSStringFromClass([self class]);
  49. }
  50. self.sd_latestOperationKey = validOperationKey;
  51. [self sd_cancelImageLoadOperationWithKey:validOperationKey];
  52. self.sd_imageURL = url;
  53. if (!(options & SDWebImageDelayPlaceholder)) {
  54. dispatch_main_async_safe(^{
  55. [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
  56. });
  57. }
  58. if (url) {
  59. // reset the progress
  60. self.sd_imageProgress.totalUnitCount = 0;
  61. self.sd_imageProgress.completedUnitCount = 0;
  62. #if SD_UIKIT || SD_MAC
  63. // check and start image indicator
  64. [self sd_startImageIndicator];
  65. id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
  66. #endif
  67. SDWebImageManager *manager = context[SDWebImageContextCustomManager];
  68. if (!manager) {
  69. manager = [SDWebImageManager sharedManager];
  70. }
  71. @weakify(self);
  72. SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
  73. @strongify(self);
  74. NSProgress *imageProgress = self.sd_imageProgress;
  75. imageProgress.totalUnitCount = expectedSize;
  76. imageProgress.completedUnitCount = receivedSize;
  77. #if SD_UIKIT || SD_MAC
  78. if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
  79. double progress = imageProgress.fractionCompleted;
  80. dispatch_async(dispatch_get_main_queue(), ^{
  81. [imageIndicator updateIndicatorProgress:progress];
  82. });
  83. }
  84. #endif
  85. if (progressBlock) {
  86. progressBlock(receivedSize, expectedSize, targetURL);
  87. }
  88. };
  89. 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) {
  90. @strongify(self);
  91. if (!self) { return; }
  92. // if the progress not been updated, mark it to complete state
  93. if (finished && !error && self.sd_imageProgress.totalUnitCount == 0 && self.sd_imageProgress.completedUnitCount == 0) {
  94. self.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
  95. self.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
  96. }
  97. #if SD_UIKIT || SD_MAC
  98. // check and stop image indicator
  99. if (finished) {
  100. [self sd_stopImageIndicator];
  101. }
  102. #endif
  103. BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
  104. BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
  105. (!image && !(options & SDWebImageDelayPlaceholder)));
  106. SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
  107. if (!self) { return; }
  108. if (!shouldNotSetImage) {
  109. [self sd_setNeedsLayout];
  110. }
  111. if (completedBlock && shouldCallCompletedBlock) {
  112. completedBlock(image, data, error, cacheType, finished, url);
  113. }
  114. };
  115. // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
  116. // OR
  117. // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
  118. if (shouldNotSetImage) {
  119. dispatch_main_async_safe(callCompletedBlockClojure);
  120. return;
  121. }
  122. UIImage *targetImage = nil;
  123. NSData *targetData = nil;
  124. if (image) {
  125. // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
  126. targetImage = image;
  127. targetData = data;
  128. } else if (options & SDWebImageDelayPlaceholder) {
  129. // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
  130. targetImage = placeholder;
  131. targetData = nil;
  132. }
  133. #if SD_UIKIT || SD_MAC
  134. // check whether we should use the image transition
  135. SDWebImageTransition *transition = nil;
  136. if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
  137. transition = self.sd_imageTransition;
  138. }
  139. #endif
  140. dispatch_main_async_safe(^{
  141. #if SD_UIKIT || SD_MAC
  142. [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
  143. #else
  144. [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
  145. #endif
  146. callCompletedBlockClojure();
  147. });
  148. }];
  149. [self sd_setImageLoadOperation:operation forKey:validOperationKey];
  150. } else {
  151. #if SD_UIKIT || SD_MAC
  152. [self sd_stopImageIndicator];
  153. #endif
  154. dispatch_main_async_safe(^{
  155. if (completedBlock) {
  156. NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
  157. completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
  158. }
  159. });
  160. }
  161. }
  162. - (void)sd_cancelCurrentImageLoad {
  163. [self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey];
  164. }
  165. - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
  166. #if SD_UIKIT || SD_MAC
  167. [self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL];
  168. #else
  169. // watchOS does not support view transition. Simplify the logic
  170. if (setImageBlock) {
  171. setImageBlock(image, imageData, cacheType, imageURL);
  172. } else if ([self isKindOfClass:[UIImageView class]]) {
  173. UIImageView *imageView = (UIImageView *)self;
  174. [imageView setImage:image];
  175. }
  176. #endif
  177. }
  178. #if SD_UIKIT || SD_MAC
  179. - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
  180. UIView *view = self;
  181. SDSetImageBlock finalSetImageBlock;
  182. if (setImageBlock) {
  183. finalSetImageBlock = setImageBlock;
  184. } else if ([view isKindOfClass:[UIImageView class]]) {
  185. UIImageView *imageView = (UIImageView *)view;
  186. finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
  187. imageView.image = setImage;
  188. };
  189. }
  190. #if SD_UIKIT
  191. else if ([view isKindOfClass:[UIButton class]]) {
  192. UIButton *button = (UIButton *)view;
  193. finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
  194. [button setImage:setImage forState:UIControlStateNormal];
  195. };
  196. }
  197. #endif
  198. #if SD_MAC
  199. else if ([view isKindOfClass:[NSButton class]]) {
  200. NSButton *button = (NSButton *)view;
  201. finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
  202. button.image = setImage;
  203. };
  204. }
  205. #endif
  206. if (transition) {
  207. #if SD_UIKIT
  208. [UIView transitionWithView:view duration:0 options:0 animations:^{
  209. // 0 duration to let UIKit render placeholder and prepares block
  210. if (transition.prepares) {
  211. transition.prepares(view, image, imageData, cacheType, imageURL);
  212. }
  213. } completion:^(BOOL finished) {
  214. [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
  215. if (finalSetImageBlock && !transition.avoidAutoSetImage) {
  216. finalSetImageBlock(image, imageData, cacheType, imageURL);
  217. }
  218. if (transition.animations) {
  219. transition.animations(view, image);
  220. }
  221. } completion:transition.completion];
  222. }];
  223. #elif SD_MAC
  224. [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
  225. // 0 duration to let AppKit render placeholder and prepares block
  226. prepareContext.duration = 0;
  227. if (transition.prepares) {
  228. transition.prepares(view, image, imageData, cacheType, imageURL);
  229. }
  230. } completionHandler:^{
  231. [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
  232. context.duration = transition.duration;
  233. context.timingFunction = transition.timingFunction;
  234. context.allowsImplicitAnimation = (transition.animationOptions & SDWebImageAnimationOptionAllowsImplicitAnimation);
  235. if (finalSetImageBlock && !transition.avoidAutoSetImage) {
  236. finalSetImageBlock(image, imageData, cacheType, imageURL);
  237. }
  238. if (transition.animations) {
  239. transition.animations(view, image);
  240. }
  241. } completionHandler:^{
  242. if (transition.completion) {
  243. transition.completion(YES);
  244. }
  245. }];
  246. }];
  247. #endif
  248. } else {
  249. if (finalSetImageBlock) {
  250. finalSetImageBlock(image, imageData, cacheType, imageURL);
  251. }
  252. }
  253. }
  254. #endif
  255. - (void)sd_setNeedsLayout {
  256. #if SD_UIKIT
  257. [self setNeedsLayout];
  258. #elif SD_MAC
  259. [self setNeedsLayout:YES];
  260. #elif SD_WATCH
  261. // Do nothing because WatchKit automatically layout the view after property change
  262. #endif
  263. }
  264. #if SD_UIKIT || SD_MAC
  265. #pragma mark - Image Transition
  266. - (SDWebImageTransition *)sd_imageTransition {
  267. return objc_getAssociatedObject(self, @selector(sd_imageTransition));
  268. }
  269. - (void)setSd_imageTransition:(SDWebImageTransition *)sd_imageTransition {
  270. objc_setAssociatedObject(self, @selector(sd_imageTransition), sd_imageTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  271. }
  272. #pragma mark - Indicator
  273. - (id<SDWebImageIndicator>)sd_imageIndicator {
  274. return objc_getAssociatedObject(self, @selector(sd_imageIndicator));
  275. }
  276. - (void)setSd_imageIndicator:(id<SDWebImageIndicator>)sd_imageIndicator {
  277. // Remove the old indicator view
  278. id<SDWebImageIndicator> previousIndicator = self.sd_imageIndicator;
  279. [previousIndicator.indicatorView removeFromSuperview];
  280. objc_setAssociatedObject(self, @selector(sd_imageIndicator), sd_imageIndicator, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  281. // Add the new indicator view
  282. UIView *view = sd_imageIndicator.indicatorView;
  283. if (CGRectEqualToRect(view.frame, CGRectZero)) {
  284. view.frame = self.bounds;
  285. }
  286. // Center the indicator view
  287. #if SD_MAC
  288. CGPoint center = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
  289. NSRect frame = view.frame;
  290. view.frame = NSMakeRect(center.x - NSMidX(frame), center.y - NSMidY(frame), NSWidth(frame), NSHeight(frame));
  291. #else
  292. view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  293. #endif
  294. view.hidden = NO;
  295. [self addSubview:view];
  296. }
  297. - (void)sd_startImageIndicator {
  298. id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
  299. if (!imageIndicator) {
  300. return;
  301. }
  302. dispatch_main_async_safe(^{
  303. [imageIndicator startAnimatingIndicator];
  304. });
  305. }
  306. - (void)sd_stopImageIndicator {
  307. id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
  308. if (!imageIndicator) {
  309. return;
  310. }
  311. dispatch_main_async_safe(^{
  312. [imageIndicator stopAnimatingIndicator];
  313. });
  314. }
  315. #endif
  316. @end