ComponentKit/Components/CKNetworkImageComponent.mm (170 lines of code) (raw):

/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ #import "CKNetworkImageComponent.h" #import <Availability.h> @interface CKNetworkImageSpecifier : NSObject - (instancetype)initWithURL:(NSURL *)url defaultImage:(UIImage *)defaultImage imageDownloader:(id<CKNetworkImageDownloading>)imageDownloader cropRect:(CGRect)cropRect; @property (nonatomic, copy, readonly) NSURL *url; @property (nonatomic, strong, readonly) UIImage *defaultImage; @property (nonatomic, strong, readonly) id<CKNetworkImageDownloading> imageDownloader; @property (nonatomic, assign, readonly) CGRect cropRect; @end @interface CKNetworkImageComponentView : UIImageView @property (nonatomic, strong) CKNetworkImageSpecifier *specifier; - (void)didEnterReusePool; - (void)willLeaveReusePool; @end @implementation CKNetworkImageComponent + (instancetype)newWithURL:(NSURL *)url imageDownloader:(id<CKNetworkImageDownloading>)imageDownloader size:(const RCComponentSize &)size options:(const CKNetworkImageComponentOptions &)options attributes:(const CKViewComponentAttributeValueMap &)passedAttributes { CGRect cropRect = options.cropRect; if (CGRectIsEmpty(cropRect)) { cropRect = CGRectMake(0, 0, 1, 1); } CKViewComponentAttributeValueMap attributes(passedAttributes); attributes.insert({ {@selector(setSpecifier:), [[CKNetworkImageSpecifier alloc] initWithURL:url defaultImage:options.defaultImage imageDownloader:imageDownloader cropRect:cropRect]}, }); #if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0) if (@available(iOS 11.0, tvOS 11.0, *)) { attributes.insert({@selector(setAccessibilityIgnoresInvertColors:), @YES}); } #endif return [super newWithView:{ {[CKNetworkImageComponentView class], @selector(didEnterReusePool), @selector(willLeaveReusePool)}, std::move(attributes) } size:size]; } @end @implementation CKNetworkImageSpecifier - (instancetype)initWithURL:(NSURL *)url defaultImage:(UIImage *)defaultImage imageDownloader:(id<CKNetworkImageDownloading>)imageDownloader cropRect:(CGRect)cropRect { if (self = [super init]) { _url = [url copy]; _defaultImage = defaultImage; _imageDownloader = imageDownloader; _cropRect = cropRect; } return self; } - (NSUInteger)hash { return [_url hash]; } - (BOOL)isEqual:(id)object { if (self == object) { return YES; } else if ([object isKindOfClass:[self class]]) { CKNetworkImageSpecifier *other = object; return RCObjectIsEqual(_url, other->_url) && RCObjectIsEqual(_defaultImage, other->_defaultImage) && RCObjectIsEqual(_imageDownloader, other->_imageDownloader) && CGRectEqualToRect(_cropRect, other->_cropRect); } return NO; } @end @implementation CKNetworkImageComponentView { BOOL _inReusePool; id _download; } - (void)dealloc { if (_download) { [_specifier.imageDownloader cancelImageDownload:_download]; } } - (void)didDownloadImage:(CGImageRef)image error:(NSError *)error { if (image) { self.image = [UIImage imageWithCGImage:image]; [self updateContentsRect]; } _download = nil; } - (void)setSpecifier:(CKNetworkImageSpecifier *)specifier { if (RCObjectIsEqual(specifier, _specifier)) { return; } if (!CGRectEqualToRect(_specifier.cropRect, specifier.cropRect)) { [self setNeedsLayout]; } BOOL urlIsDifferent = !RCObjectIsEqual(_specifier.url, specifier.url); BOOL isShowingCurrentDefaultImage = RCObjectIsEqual(self.image, _specifier.defaultImage); if (urlIsDifferent || isShowingCurrentDefaultImage) { self.image = specifier.defaultImage; } if (urlIsDifferent && _download != nil) { [specifier.imageDownloader cancelImageDownload:_download]; _download = nil; } _specifier = specifier; if (urlIsDifferent) { [self _startDownloadIfNotInReusePool]; } } - (void)didEnterReusePool { _inReusePool = YES; if (_download) { [_specifier.imageDownloader cancelImageDownload:_download]; _download = nil; } // Release the downloaded image that we're holding to lower memory usage. self.image = _specifier.defaultImage; } - (void)willLeaveReusePool { _inReusePool = NO; [self _startDownloadIfNotInReusePool]; } - (void)_startDownloadIfNotInReusePool { if (_inReusePool) { return; } if (_specifier.url == nil) { return; } __weak CKNetworkImageComponentView *weakSelf = self; _download = [_specifier.imageDownloader downloadImageWithURL:_specifier.url caller:self callbackQueue:dispatch_get_main_queue() downloadProgressBlock:nil completion:^(CGImageRef image, NSError *error) { [weakSelf didDownloadImage:image error:error]; }]; } - (void)updateContentsRect { if (CGRectIsEmpty(self.bounds)) { return; } // If we're about to crop the width or height, make sure the cropped version won't be upscaled CGFloat croppedWidth = self.image.size.width * _specifier.cropRect.size.width; CGFloat croppedHeight = self.image.size.height * _specifier.cropRect.size.height; if ((_specifier.cropRect.size.width == 1 || croppedWidth >= self.bounds.size.width) && (_specifier.cropRect.size.height == 1 || croppedHeight >= self.bounds.size.height)) { self.layer.contentsRect = _specifier.cropRect; } } #pragma mark - UIView - (void)layoutSubviews { [super layoutSubviews]; [self updateContentsRect]; } @end