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