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