@depup/react-native-fast-image 8.6.3-depup.0 → 8.13.1-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/RNFastImage.podspec +5 -2
  3. package/android/build.gradle +13 -0
  4. package/android/src/main/AndroidManifestNew.xml +3 -0
  5. package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +84 -0
  6. package/android/src/main/java/com/dylanvann/fastimage/FastImageEvent.java +45 -0
  7. package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +60 -0
  8. package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +110 -0
  9. package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +65 -5
  10. package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +65 -22
  11. package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +21 -0
  12. package/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +21 -1
  13. package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +237 -0
  14. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +14 -5
  15. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +66 -22
  16. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +130 -24
  17. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +359 -39
  18. package/android/src/main/java/com/dylanvann/fastimage/FastImageWebGlideUrl.java +13 -0
  19. package/changes.json +1 -1
  20. package/dist/index.cjs.js +166 -130
  21. package/dist/index.cjs.js.flow +33 -2
  22. package/dist/index.d.ts +77 -22
  23. package/dist/index.js +141 -121
  24. package/dist/index.js.flow +33 -2
  25. package/ios/FastImage/FFFDownsampledImage.h +28 -0
  26. package/ios/FastImage/FFFDownsampledImage.m +169 -0
  27. package/ios/FastImage/FFFastImageSource.h +6 -0
  28. package/ios/FastImage/FFFastImageSource.m +13 -0
  29. package/ios/FastImage/FFFastImageView.h +17 -0
  30. package/ios/FastImage/FFFastImageView.m +498 -71
  31. package/ios/FastImage/FFFastImageViewManager.m +106 -8
  32. package/ios/FastImage/RCTConvert+FFFastImage.m +4 -1
  33. package/package.json +31 -43
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.test.d.ts +0 -2
  36. package/dist/index.test.d.ts.map +0 -1
@@ -1,6 +1,9 @@
1
1
  #import "FFFastImageView.h"
2
2
  #import <SDWebImage/UIImage+MultiFormat.h>
3
3
  #import <SDWebImage/UIView+WebCache.h>
4
+ #import <React/RCTUtils.h>
5
+ #import <SDWebImage/SDWebImageError.h>
6
+ #import "FFFDownsampledImage.h"
4
7
 
5
8
  @interface FFFastImageView ()
6
9
 
@@ -11,48 +14,175 @@
11
14
  @property(nonatomic, assign) BOOL needsReload;
12
15
 
13
16
  @property(nonatomic, strong) NSDictionary* onLoadEvent;
17
+ @property(nonatomic, strong) NSDictionary* onErrorEvent;
18
+ // The image before tinting, kept while a tint is applied so the tint can be
19
+ // changed or removed. nil when there's no tint (super.image is untinted).
20
+ @property(nonatomic, strong) UIImage* untintedImage;
21
+ // Whether the current load was already restarted after the app came back
22
+ // from the background (see downloadImage:).
23
+ @property(nonatomic, assign) BOOL retriedAfterBackground;
24
+ // Waits for the app to be active again, to restart a load.
25
+ @property(nonatomic, strong) id activeObserver;
26
+ // Whether the view shows an image that loaded (not defaultSource or nothing).
27
+ // A new source then keeps it until the new image has loaded (see reloadImage).
28
+ @property(nonatomic, assign) BOOL showsLoadedImage;
29
+ // downsample: a load waits for the view's size (see didSetProps).
30
+ @property(nonatomic, assign) BOOL waitsForSize;
31
+ // The size (in pixels) the image showing or loading was decoded for, and
32
+ // whether it covers it; zero for a full-size image.
33
+ @property(nonatomic, assign) CGSize decodedBox;
34
+ @property(nonatomic, assign) BOOL decodedCover;
35
+ // Counts loads, so a quiet reload's completion can tell if it's still current.
36
+ @property(nonatomic, assign) NSUInteger loadCount;
14
37
 
15
38
  @end
16
39
 
40
+ // When the app last went to the background (CACurrentMediaTime), or 0.
41
+ static CFTimeInterval FFFEnteredBackgroundAt = 0;
42
+
17
43
  @implementation FFFastImageView
18
44
 
45
+ + (void) initialize {
46
+ if (self != [FFFastImageView class]) {
47
+ return;
48
+ }
49
+ [[NSNotificationCenter defaultCenter] addObserverForName: UIApplicationDidEnterBackgroundNotification
50
+ object: nil
51
+ queue: [NSOperationQueue mainQueue]
52
+ usingBlock: ^(NSNotification* notification) {
53
+ FFFEnteredBackgroundAt = CACurrentMediaTime();
54
+ }];
55
+ }
56
+
19
57
  - (id) init {
20
58
  self = [super init];
21
59
  self.resizeMode = RCTResizeModeCover;
22
60
  self.clipsToBounds = YES;
61
+ _loopCount = -1;
23
62
  return self;
24
63
  }
25
64
 
65
+ - (void) setImageRendering: (NSString*)imageRendering {
66
+ _imageRendering = [imageRendering copy];
67
+ if ([imageRendering isEqualToString: @"smooth"]) {
68
+ // Trilinear: drawn smaller from mipmaps, so a large image drawn much
69
+ // smaller is smoothed instead of aliased (#445). The mipmaps take
70
+ // about a third more memory than the decoded image.
71
+ self.layer.minificationFilter = kCAFilterTrilinear;
72
+ self.layer.magnificationFilter = kCAFilterLinear;
73
+ } else if ([imageRendering isEqualToString: @"pixelated"]) {
74
+ // Nearest neighbor: sharp pixels, e.g. for pixel art (#926).
75
+ self.layer.minificationFilter = kCAFilterNearest;
76
+ self.layer.magnificationFilter = kCAFilterNearest;
77
+ } else {
78
+ self.layer.minificationFilter = kCAFilterLinear;
79
+ self.layer.magnificationFilter = kCAFilterLinear;
80
+ }
81
+ }
82
+
83
+ - (void) setLoopCount: (NSInteger)loopCount {
84
+ if (_loopCount == loopCount) {
85
+ return;
86
+ }
87
+ _loopCount = loopCount;
88
+ // SDAnimatedImageView uses animationRepeatCount (0 is forever) instead of
89
+ // the file's loop count when shouldCustomLoopCount is set, for the next
90
+ // image it shows.
91
+ self.shouldCustomLoopCount = loopCount >= 0;
92
+ if (loopCount >= 0) {
93
+ self.animationRepeatCount = loopCount;
94
+ } else if (self.player && [self.image conformsToProtocol: @protocol(SDAnimatedImage)]) {
95
+ self.player.totalLoopCount = [(id<SDAnimatedImage>) self.image animatedImageLoopCount];
96
+ }
97
+ // Apply it to the image that's showing, and play it again (unless it's
98
+ // paused: then it waits on the first frame).
99
+ if (self.player) {
100
+ [self.player seekToFrameAtIndex: 0 loopCount: 0];
101
+ if (!_paused) {
102
+ [self startAnimating];
103
+ }
104
+ }
105
+ }
106
+
107
+ - (void) setPaused: (BOOL)paused {
108
+ if (_paused == paused) {
109
+ return;
110
+ }
111
+ _paused = paused;
112
+ // SDAnimatedImageView starts an animated image when it's shown, and again
113
+ // when the view comes back on screen, unless autoPlayAnimatedImage is off.
114
+ // Stopping keeps the frame it's on (resetFrameIndexWhenStopped is off).
115
+ self.autoPlayAnimatedImage = !paused;
116
+ if (paused) {
117
+ [self stopAnimating];
118
+ } else {
119
+ [self startAnimating];
120
+ }
121
+ }
122
+
26
123
  - (void) setResizeMode: (RCTResizeMode)resizeMode {
27
124
  if (_resizeMode != resizeMode) {
28
125
  _resizeMode = resizeMode;
29
- self.contentMode = (UIViewContentMode) resizeMode;
126
+ [self updateContentMode];
127
+ }
128
+ }
129
+
130
+ // The content mode for resizeMode. `center` scales an image larger than the
131
+ // view down to fit, as React Native's Image and Android do (#866); only a
132
+ // smaller one is shown at its own size. UIViewContentModeCenter alone showed
133
+ // large images at full size, cropped. So it depends on the image and the
134
+ // view's size.
135
+ - (void) updateContentMode {
136
+ UIViewContentMode contentMode = (UIViewContentMode) _resizeMode;
137
+ if (_resizeMode == RCTResizeModeCenter) {
138
+ CGSize imageSize = super.image.size;
139
+ CGSize viewSize = self.bounds.size;
140
+ if (imageSize.width > viewSize.width || imageSize.height > viewSize.height) {
141
+ contentMode = UIViewContentModeScaleAspectFit;
142
+ }
143
+ }
144
+ if (self.contentMode != contentMode) {
145
+ self.contentMode = contentMode;
146
+ }
147
+ }
148
+
149
+ - (void) layoutSubviews {
150
+ [super layoutSubviews];
151
+ [self updateContentMode];
152
+ if (self.waitsForSize) {
153
+ if ([self hasSize]) {
154
+ [self reloadImage];
155
+ }
156
+ } else {
157
+ [self reloadIfResized];
30
158
  }
31
159
  }
32
160
 
33
161
  - (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
34
162
  _onFastImageLoadEnd = onFastImageLoadEnd;
35
- if (self.hasCompleted) {
163
+ if (self.hasCompleted && _onFastImageLoadEnd) {
36
164
  _onFastImageLoadEnd(@{});
37
165
  }
38
166
  }
39
167
 
40
168
  - (void) setOnFastImageLoad: (RCTDirectEventBlock)onFastImageLoad {
41
169
  _onFastImageLoad = onFastImageLoad;
42
- if (self.hasCompleted) {
170
+ if (self.hasCompleted && _onFastImageLoad) {
43
171
  _onFastImageLoad(self.onLoadEvent);
44
172
  }
45
173
  }
46
174
 
47
175
  - (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError {
48
176
  _onFastImageError = onFastImageError;
49
- if (self.hasErrored) {
50
- _onFastImageError(@{});
177
+ if (self.hasErrored && _onFastImageError) {
178
+ _onFastImageError(self.onErrorEvent);
51
179
  }
52
180
  }
53
181
 
54
182
  - (void) setOnFastImageLoadStart: (RCTDirectEventBlock)onFastImageLoadStart {
55
- if (_source && !self.hasSentOnLoadStart) {
183
+ // Send it for a load that has already started. When a reload is pending
184
+ // (e.g. source set in the same update), reloadImage sends it.
185
+ if (_source && !_needsReload && !self.hasSentOnLoadStart && onFastImageLoadStart) {
56
186
  _onFastImageLoadStart = onFastImageLoadStart;
57
187
  onFastImageLoadStart(@{});
58
188
  self.hasSentOnLoadStart = YES;
@@ -63,36 +193,91 @@
63
193
  }
64
194
 
65
195
  - (void) setImageColor: (UIColor*)imageColor {
66
- if (imageColor != nil) {
67
- _imageColor = imageColor;
68
- if (super.image) {
69
- super.image = [self makeImage: super.image withTint: self.imageColor];
70
- }
196
+ _imageColor = imageColor;
197
+ // Re-apply to the untinted image, so the tint can change or be removed.
198
+ UIImage* image = self.untintedImage ?: super.image;
199
+ if (image) {
200
+ [self setImage: image];
71
201
  }
72
202
  }
73
203
 
74
204
  - (UIImage*) makeImage: (UIImage*)image withTint: (UIColor*)color {
75
- UIImage* newImage = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate];
76
- UIGraphicsBeginImageContextWithOptions(image.size, NO, newImage.scale);
77
- [color set];
78
- [newImage drawInRect: CGRectMake(0, 0, image.size.width, newImage.size.height)];
79
- newImage = UIGraphicsGetImageFromCurrentImageContext();
80
- UIGraphicsEndImageContext();
205
+ // FIX: Prevent crash on zero/invalid image dimensions
206
+ if (!image || image.size.width <= 0 || image.size.height <= 0) {
207
+ return image;
208
+ }
209
+
210
+ UIImage* templateImage = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate];
211
+ CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
212
+ UIImage* newImage;
213
+ if (@available(iOS 10.0, tvOS 10.0, *)) {
214
+ // UIGraphicsBeginImageContextWithOptions is deprecated since iOS 17.
215
+ // Keep the source image's scale and a standard-range (8-bit) bitmap,
216
+ // matching what it produced.
217
+ UIGraphicsImageRendererFormat* format = [[UIGraphicsImageRendererFormat alloc] init];
218
+ format.scale = image.scale;
219
+ format.opaque = NO;
220
+ if (@available(iOS 12.0, tvOS 12.0, *)) {
221
+ format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
222
+ } else {
223
+ format.prefersExtendedRange = NO;
224
+ }
225
+ UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize: image.size format: format];
226
+ newImage = [renderer imageWithActions: ^(UIGraphicsImageRendererContext* context) {
227
+ [color set];
228
+ [templateImage drawInRect: rect];
229
+ }];
230
+ } else {
231
+ // iOS/tvOS 9. Remove this branch once the minimum is iOS 10+.
232
+ UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
233
+ [color set];
234
+ [templateImage drawInRect: rect];
235
+ newImage = UIGraphicsGetImageFromCurrentImageContext();
236
+ UIGraphicsEndImageContext();
237
+ }
81
238
  return newImage;
82
239
  }
83
240
 
84
241
  - (void) setImage: (UIImage*)image {
85
242
  if (self.imageColor != nil) {
243
+ self.untintedImage = image;
86
244
  super.image = [self makeImage: image withTint: self.imageColor];
87
245
  } else {
246
+ self.untintedImage = nil;
88
247
  super.image = image;
89
248
  }
249
+ [self updateContentMode];
250
+ }
251
+
252
+ // The error's description, with the HTTP status code when there is one (as on
253
+ // Android): SDWebImage keeps the code out of the description. Also for preload
254
+ // results.
255
+ NSString *FFFErrorMessage(NSError *error)
256
+ {
257
+ NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
258
+ if (statusCode) {
259
+ return [NSString stringWithFormat: @"%@, status code: %@", error.localizedDescription, statusCode];
260
+ }
261
+ return error.localizedDescription ?: @"Failed to load the image";
262
+ }
263
+
264
+ - (void) sendOnError: (nullable NSString*)message {
265
+ self.hasErrored = YES;
266
+ self.onErrorEvent = @{ @"error": message ?: @"Failed to load the image" };
267
+ if (self.onFastImageError) {
268
+ self.onFastImageError(self.onErrorEvent);
269
+ }
90
270
  }
91
271
 
92
272
  - (void) sendOnLoad: (UIImage*)image {
273
+ // The full image's size, also when it was decoded smaller.
274
+ CGSize size = [FFFDownsampledImage sourceSizeOfImage: image];
275
+ if (CGSizeEqualToSize(size, CGSizeZero)) {
276
+ size = image.size;
277
+ }
93
278
  self.onLoadEvent = @{
94
- @"width": [NSNumber numberWithDouble: image.size.width],
95
- @"height": [NSNumber numberWithDouble: image.size.height]
279
+ @"width": [NSNumber numberWithDouble: size.width],
280
+ @"height": [NSNumber numberWithDouble: size.height]
96
281
  };
97
282
  if (self.onFastImageLoad) {
98
283
  self.onFastImageLoad(self.onLoadEvent);
@@ -106,6 +291,21 @@
106
291
  }
107
292
  }
108
293
 
294
+ - (void) setRecyclingKey: (NSString*)recyclingKey {
295
+ if (_recyclingKey == recyclingKey || [_recyclingKey isEqualToString: recyclingKey]) {
296
+ return;
297
+ }
298
+ BOOL changed = _recyclingKey != nil;
299
+ _recyclingKey = [recyclingKey copy];
300
+ if (changed) {
301
+ // The view shows other content now: don't keep the current image
302
+ // while the next one loads (reloadImage clears it), even if the
303
+ // source is the same.
304
+ self.showsLoadedImage = NO;
305
+ _needsReload = YES;
306
+ }
307
+ }
308
+
109
309
  - (void) setDefaultSource: (UIImage*)defaultSource {
110
310
  if (_defaultSource != defaultSource) {
111
311
  _defaultSource = defaultSource;
@@ -115,12 +315,101 @@
115
315
 
116
316
  - (void) didSetProps: (NSArray<NSString*>*)changedProps {
117
317
  if (_needsReload) {
318
+ // With downsample on, the image is decoded for the view's size, so
319
+ // a view that hasn't been laid out yet loads once it has. Props and
320
+ // layout are applied in the same update, so that's before the next
321
+ // frame (in layoutSubviews). A view that still has no size then
322
+ // (e.g. one sized from onLoad) loads at full size.
323
+ if ([self downsamples] && ![self hasSize]) {
324
+ if (!self.waitsForSize) {
325
+ self.waitsForSize = YES;
326
+ __weak typeof(self) weakSelf = self;
327
+ dispatch_async(dispatch_get_main_queue(), ^{
328
+ if (weakSelf.waitsForSize) {
329
+ [weakSelf reloadImage];
330
+ }
331
+ });
332
+ }
333
+ return;
334
+ }
118
335
  [self reloadImage];
336
+ } else {
337
+ [self reloadIfResized];
338
+ }
339
+ }
340
+
341
+ // Whether images are decoded at about the view's size (downsample).
342
+ // Not for `repeat`, which tiles the image at its own size, or SDWebImage
343
+ // before 5.19.
344
+ - (BOOL) downsamples {
345
+ return _downsample && _resizeMode != RCTResizeModeRepeat && [FFFDownsampledImage isSupported];
346
+ }
347
+
348
+ // Whether the view has been laid out with an area. One that's 0 wide or tall
349
+ // (e.g. sized from onLoad) has no size to decode the image for.
350
+ - (BOOL) hasSize {
351
+ return self.bounds.size.width > 0 && self.bounds.size.height > 0;
352
+ }
353
+
354
+ // The size in pixels to decode the image for, or zero for full size.
355
+ - (CGSize) decodeBox {
356
+ if (![self downsamples] || ![self hasSize]) {
357
+ return CGSizeZero;
119
358
  }
359
+ CGFloat scale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
360
+ CGSize size = self.bounds.size;
361
+ return CGSizeMake(ceil(size.width * scale), ceil(size.height * scale));
362
+ }
363
+
364
+ // Whether the image has to cover the box, rather than fit in it.
365
+ - (BOOL) decodeCovers {
366
+ return _resizeMode == RCTResizeModeCover || _resizeMode == RCTResizeModeStretch;
367
+ }
368
+
369
+ // The view grew (by more than a fifth, as React Native's Image reloads), or
370
+ // now needs a covering or full-size image: loads the image again for its
371
+ // size, keeping the current one until then. It comes from the disk cache
372
+ // (the downloaded file is kept there whatever size it's decoded at).
373
+ - (void) reloadIfResized {
374
+ if (!_source || _needsReload || self.hasErrored || CGSizeEqualToSize(self.decodedBox, CGSizeZero)) {
375
+ return;
376
+ }
377
+ // Already at full size (it's no larger than the view it was decoded for).
378
+ UIImage* image = self.untintedImage ?: super.image;
379
+ if (self.hasCompleted && CGSizeEqualToSize(image.size, [FFFDownsampledImage sourceSizeOfImage: image])) {
380
+ return;
381
+ }
382
+ CGSize box = [self decodeBox];
383
+ BOOL cover = [self decodeCovers];
384
+ if ([self downsamples]) {
385
+ if (![self hasSize]) {
386
+ return;
387
+ }
388
+ BOOL grew = box.width > self.decodedBox.width * 1.2 || box.height > self.decodedBox.height * 1.2;
389
+ if (!grew && (self.decodedCover || !cover)) {
390
+ return;
391
+ }
392
+ }
393
+ SDWebImageOptions options = [self loadOptions];
394
+ if (self.showsLoadedImage) {
395
+ options |= SDWebImageDelayPlaceholder;
396
+ }
397
+ // Without events once it has loaded: it's the same image. If it's still
398
+ // loading, it restarts for the new size, and sends its events as usual.
399
+ BOOL events = !self.hasCompleted;
400
+ [self downloadImage: _source options: options context: [self loadContext] events: events];
120
401
  }
121
402
 
122
403
  - (void) reloadImage {
123
404
  _needsReload = NO;
405
+ self.waitsForSize = NO;
406
+ self.decodedBox = CGSizeZero;
407
+ // The previous load, if it's still running, is for a source the view no
408
+ // longer shows: cancel it (a new download would, but a data uri or no
409
+ // source doesn't start one), and ignore what it still sends (SDWebImage
410
+ // completes a cancelled load with an error; see downloadImage:).
411
+ self.loadCount++;
412
+ [self sd_cancelCurrentImageLoad];
124
413
 
125
414
  if (_source) {
126
415
  // Load base64 images.
@@ -134,6 +423,16 @@
134
423
  }
135
424
  // Use SDWebImage API to support external format like WebP images
136
425
  UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
426
+ if (!image) {
427
+ // Not decodable: fail like a remote image, showing defaultSource.
428
+ [self setImage: _defaultSource];
429
+ self.showsLoadedImage = NO;
430
+ [self sendOnError: @"Failed to decode the image"];
431
+ if (self.onFastImageLoadEnd) {
432
+ self.onFastImageLoadEnd(@{});
433
+ }
434
+ return;
435
+ }
137
436
  [self setImage: image];
138
437
  if (self.onFastImageProgress) {
139
438
  self.onFastImageProgress(@{
@@ -142,6 +441,7 @@
142
441
  });
143
442
  }
144
443
  self.hasCompleted = YES;
444
+ self.showsLoadedImage = YES;
145
445
  [self sendOnLoad: image];
146
446
 
147
447
  if (self.onFastImageLoadEnd) {
@@ -150,41 +450,14 @@
150
450
  return;
151
451
  }
152
452
 
153
- // Set headers.
154
- NSDictionary* headers = _source.headers;
155
- SDWebImageDownloaderRequestModifier* requestModifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock: ^NSURLRequest* _Nullable (NSURLRequest* _Nonnull request) {
156
- NSMutableURLRequest* mutableRequest = [request mutableCopy];
157
- for (NSString* header in headers) {
158
- NSString* value = headers[header];
159
- [mutableRequest setValue: value forHTTPHeaderField: header];
160
- }
161
- return [mutableRequest copy];
162
- }];
163
- SDWebImageContext* context = @{SDWebImageContextDownloadRequestModifier: requestModifier};
164
-
165
- // Set priority.
166
- SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
167
- switch (_source.priority) {
168
- case FFFPriorityLow:
169
- options |= SDWebImageLowPriority;
170
- break;
171
- case FFFPriorityNormal:
172
- // Priority is normal by default.
173
- break;
174
- case FFFPriorityHigh:
175
- options |= SDWebImageHighPriority;
176
- break;
177
- }
453
+ SDWebImageOptions options = [self loadOptions];
178
454
 
179
- switch (_source.cacheControl) {
180
- case FFFCacheControlWeb:
181
- options |= SDWebImageRefreshCached;
182
- break;
183
- case FFFCacheControlCacheOnly:
184
- options |= SDWebImageFromCacheOnly;
185
- break;
186
- case FFFCacheControlImmutable:
187
- break;
455
+ // Keep showing the loaded image until the new one has loaded, instead
456
+ // of clearing it to defaultSource (or nothing) while it loads, which
457
+ // flashed (#747). defaultSource shows if the new image fails. As React
458
+ // Native's Image does; a `key` that changes starts from blank instead.
459
+ if (self.showsLoadedImage) {
460
+ options |= SDWebImageDelayPlaceholder;
188
461
  }
189
462
 
190
463
  if (self.onFastImageLoadStart) {
@@ -195,40 +468,164 @@
195
468
  }
196
469
  self.hasCompleted = NO;
197
470
  self.hasErrored = NO;
471
+ self.retriedAfterBackground = NO;
472
+ [self stopWaitingForActive];
198
473
 
199
- [self downloadImage: _source options: options context: context];
474
+ [self downloadImage: _source options: options context: [self loadContext] events: YES];
200
475
  } else if (_defaultSource) {
201
476
  [self setImage: _defaultSource];
477
+ self.showsLoadedImage = NO;
202
478
  }
203
479
  }
204
480
 
205
- - (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context {
481
+ - (SDWebImageOptions) loadOptions {
482
+ SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
483
+ switch (_source.priority) {
484
+ case FFFPriorityLow:
485
+ options |= SDWebImageLowPriority;
486
+ break;
487
+ case FFFPriorityNormal:
488
+ // Priority is normal by default.
489
+ break;
490
+ case FFFPriorityHigh:
491
+ options |= SDWebImageHighPriority;
492
+ break;
493
+ }
494
+
495
+ switch (_source.cacheControl) {
496
+ case FFFCacheControlWeb:
497
+ options |= SDWebImageRefreshCached;
498
+ break;
499
+ case FFFCacheControlCacheOnly:
500
+ options |= SDWebImageFromCacheOnly;
501
+ break;
502
+ case FFFCacheControlImmutable:
503
+ break;
504
+ }
505
+ return options;
506
+ }
507
+
508
+ // Headers, and the size to decode at (see FFFDownsampledImage), which it
509
+ // records as decodedBox.
510
+ - (SDWebImageContext*) loadContext {
511
+ SDWebImageMutableContext* context = [NSMutableDictionary dictionary];
512
+ context[SDWebImageContextDownloadRequestModifier] = _source.requestModifier;
513
+ CGSize box = [self decodeBox];
514
+ self.decodedBox = box;
515
+ self.decodedCover = [self decodeCovers];
516
+ if (CGSizeEqualToSize(box, CGSizeZero)) {
517
+ context[SDWebImageContextAnimatedImageClass] = [SDAnimatedImage class];
518
+ return context;
519
+ }
520
+ [FFFDownsampledImage addToContext: context forURL: _source.url box: box cover: self.decodedCover];
521
+ return context;
522
+ }
523
+
524
+ // events: NO for a quiet reload at a new size (see reloadIfResized), which
525
+ // sends no events and keeps the current image if it fails.
526
+ - (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context events: (BOOL)events {
206
527
  __weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks
207
- [self sd_setImageWithURL: _source.url
208
- placeholderImage: _defaultSource
209
- options: options
210
- context: context
211
- progress: ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) {
212
- if (weakSelf.onFastImageProgress) {
213
- weakSelf.onFastImageProgress(@{
214
- @"loaded": @(receivedSize),
215
- @"total": @(expectedSize)
216
- });
217
- }
218
- } completed: ^(UIImage* _Nullable image,
528
+ NSUInteger load = ++self.loadCount;
529
+ // Most images have no onProgress, so only ask SDWebImage for progress when
530
+ // there's a handler as the load starts. A handler added while loading is
531
+ // used from the next load.
532
+ SDImageLoaderProgressBlock progress = nil;
533
+ if (events && self.onFastImageProgress) {
534
+ progress = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) {
535
+ // Without a Content-Length the total is unknown (-1 or 0), and a
536
+ // percentage can't be worked out from it, so don't send those.
537
+ if (expectedSize <= 0) {
538
+ return;
539
+ }
540
+ // SDWebImage calls this on its download queue, while React Native
541
+ // sets onFastImageProgress (and deallocates the view) on the main
542
+ // queue. Read and call it there, so it can't change or be released
543
+ // in between (EXC_BAD_ACCESS).
544
+ dispatch_async(dispatch_get_main_queue(), ^{
545
+ RCTDirectEventBlock onProgress = weakSelf.onFastImageProgress;
546
+ if (onProgress) {
547
+ onProgress(@{
548
+ @"loaded": @(receivedSize),
549
+ @"total": @(expectedSize)
550
+ });
551
+ }
552
+ });
553
+ };
554
+ }
555
+ CFTimeInterval startedAt = CACurrentMediaTime();
556
+ NSURL* url = context[SDWebImageContextImageThumbnailPixelSize] ? [FFFDownsampledImage loadURLForURL: source.url] : source.url;
557
+ if (!events) {
558
+ // Only this load's image: SDWebImage would clear the view (to the
559
+ // placeholder) if it fails, and a cancelled load can still complete.
560
+ SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
561
+ if (image && weakSelf.loadCount == load) {
562
+ weakSelf.image = image;
563
+ }
564
+ };
565
+ [self sd_internalSetImageWithURL: url
566
+ placeholderImage: nil
567
+ options: options
568
+ context: context
569
+ setImageBlock: setImage
570
+ progress: nil
571
+ completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
572
+ if (error && weakSelf.loadCount == load) {
573
+ // Keeps the image it has, without trying again on every
574
+ // layout (e.g. while offline).
575
+ weakSelf.decodedBox = CGSizeZero;
576
+ }
577
+ }];
578
+ return;
579
+ }
580
+ // Like SDAnimatedImageView's sd_setImageWithURL, which sets the image
581
+ // class to SDAnimatedImage (loadContext sets it). Only while this is the
582
+ // current load: SDWebImage sets the placeholder when a load it cancelled
583
+ // completes, which could replace a newer image.
584
+ SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
585
+ if (weakSelf.loadCount == load) {
586
+ weakSelf.image = image;
587
+ }
588
+ };
589
+ [self sd_internalSetImageWithURL: url
590
+ placeholderImage: _defaultSource
591
+ options: options
592
+ context: context
593
+ setImageBlock: setImage
594
+ progress: progress
595
+ completed: ^(UIImage* _Nullable image,
596
+ NSData* _Nullable data,
219
597
  NSError* _Nullable error,
220
598
  SDImageCacheType cacheType,
599
+ BOOL finished,
221
600
  NSURL* _Nullable imageURL) {
601
+ // Replaced by another load (a new source, or the same one
602
+ // restarted for a new size), which sends the events. This
603
+ // one was cancelled, which SDWebImage reports as an error.
604
+ if (weakSelf.loadCount != load) {
605
+ return;
606
+ }
607
+ // The download was running when the app went to the
608
+ // background, and failed: iOS suspends it there, and its
609
+ // timeout keeps counting, so it times out as the app comes
610
+ // back (NSURLErrorTimedOut), or loses its connection. Load it
611
+ // again once the app is active, as Android does (#758).
612
+ if (error && [error.domain isEqualToString: NSURLErrorDomain] &&
613
+ FFFEnteredBackgroundAt > startedAt && !weakSelf.retriedAfterBackground &&
614
+ weakSelf.source == source) {
615
+ weakSelf.retriedAfterBackground = YES;
616
+ [weakSelf downloadWhenActive: source options: options context: context];
617
+ return;
618
+ }
222
619
  if (error) {
223
- weakSelf.hasErrored = YES;
224
- if (weakSelf.onFastImageError) {
225
- weakSelf.onFastImageError(@{});
226
- }
620
+ // SDWebImage shows the placeholder (defaultSource or nothing).
621
+ weakSelf.showsLoadedImage = NO;
622
+ [weakSelf sendOnError: FFFErrorMessage(error)];
227
623
  if (weakSelf.onFastImageLoadEnd) {
228
624
  weakSelf.onFastImageLoadEnd(@{});
229
625
  }
230
626
  } else {
231
627
  weakSelf.hasCompleted = YES;
628
+ weakSelf.showsLoadedImage = YES;
232
629
  [weakSelf sendOnLoad: image];
233
630
  if (weakSelf.onFastImageLoadEnd) {
234
631
  weakSelf.onFastImageLoadEnd(@{});
@@ -237,7 +634,37 @@
237
634
  }];
238
635
  }
239
636
 
637
+ // Downloads the source now if the app is active, or once it is (unless
638
+ // another load started meanwhile).
639
+ - (void) downloadWhenActive: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context {
640
+ // nil in app extensions, which don't get these notifications.
641
+ UIApplication* application = RCTSharedApplication();
642
+ if (!application || application.applicationState == UIApplicationStateActive) {
643
+ [self downloadImage: source options: options context: context events: YES];
644
+ return;
645
+ }
646
+ [self stopWaitingForActive];
647
+ __weak typeof(self) weakSelf = self;
648
+ self.activeObserver = [[NSNotificationCenter defaultCenter] addObserverForName: UIApplicationDidBecomeActiveNotification
649
+ object: nil
650
+ queue: [NSOperationQueue mainQueue]
651
+ usingBlock: ^(NSNotification* notification) {
652
+ [weakSelf stopWaitingForActive];
653
+ if (weakSelf.source == source) {
654
+ [weakSelf downloadImage: source options: options context: context events: YES];
655
+ }
656
+ }];
657
+ }
658
+
659
+ - (void) stopWaitingForActive {
660
+ if (self.activeObserver) {
661
+ [[NSNotificationCenter defaultCenter] removeObserver: self.activeObserver];
662
+ self.activeObserver = nil;
663
+ }
664
+ }
665
+
240
666
  - (void) dealloc {
667
+ [self stopWaitingForActive];
241
668
  [self sd_cancelCurrentImageLoad];
242
669
  }
243
670