Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add categories for alternative image types that can be decoded using registered custom decoders #602

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions PINRemoteImage.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
938E98DA2224775B00029E4D /* PINRemoteImageManagerConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 939546BF2220AF84006031BB /* PINRemoteImageManagerConfiguration.m */; };
939546C02220AF84006031BB /* PINRemoteImageManagerConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 939546BE2220AF84006031BB /* PINRemoteImageManagerConfiguration.h */; };
939546C12220AF84006031BB /* PINRemoteImageManagerConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 939546BF2220AF84006031BB /* PINRemoteImageManagerConfiguration.m */; };
9A079DD826826EFD00F2E70A /* PINImage+AlternativeTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD726826EFD00F2E70A /* PINImage+AlternativeTypes.m */; };
9A079DE1268270F900F2E70A /* PINImage+AlternativeTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 9A079DDB26826F0E00F2E70A /* PINImage+AlternativeTypes.h */; settings = {ATTRIBUTES = (Public, ); }; };
9DD47F9D1C699F4B00F12CA0 /* PINButton+PINRemoteImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9DD47F991C699F4B00F12CA0 /* PINButton+PINRemoteImage.m */; };
9DD47F9F1C699F4B00F12CA0 /* PINImageView+PINRemoteImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9DD47F9B1C699F4B00F12CA0 /* PINImageView+PINRemoteImage.m */; };
9DD47FA41C699FDC00F12CA0 /* PINImage+DecodedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 9DD47FA01C699FDC00F12CA0 /* PINImage+DecodedImage.h */; };
Expand Down Expand Up @@ -252,6 +254,8 @@
926E015D1F0DFCAE00874D01 /* PINRequestRetryStrategy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PINRequestRetryStrategy.m; sourceTree = "<group>"; };
939546BE2220AF84006031BB /* PINRemoteImageManagerConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PINRemoteImageManagerConfiguration.h; sourceTree = "<group>"; };
939546BF2220AF84006031BB /* PINRemoteImageManagerConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PINRemoteImageManagerConfiguration.m; sourceTree = "<group>"; };
9A079DD726826EFD00F2E70A /* PINImage+AlternativeTypes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "PINImage+AlternativeTypes.m"; sourceTree = "<group>"; };
9A079DDB26826F0E00F2E70A /* PINImage+AlternativeTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PINImage+AlternativeTypes.h"; sourceTree = "<group>"; };
9DD47F991C699F4B00F12CA0 /* PINButton+PINRemoteImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "PINButton+PINRemoteImage.m"; sourceTree = "<group>"; };
9DD47F9B1C699F4B00F12CA0 /* PINImageView+PINRemoteImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "PINImageView+PINRemoteImage.m"; sourceTree = "<group>"; };
9DD47FA01C699FDC00F12CA0 /* PINImage+DecodedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PINImage+DecodedImage.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -525,6 +529,8 @@
686D48CE1ED38FC0003DB4C2 /* PINRemoteImageTask+Subclassing.h */,
ACD28EAE81695DDF84BB76B8 /* NSHTTPURLResponse+MaxAge.m */,
ACD28A0374E664CFF0BB3297 /* NSHTTPURLResponse+MaxAge.h */,
9A079DD726826EFD00F2E70A /* PINImage+AlternativeTypes.m */,
9A079DDB26826F0E00F2E70A /* PINImage+AlternativeTypes.h */,
);
path = Categories;
sourceTree = "<group>";
Expand Down Expand Up @@ -633,6 +639,7 @@
FBC820B22526277E007FD40D /* PINRemoteImageManager.h in Headers */,
FBC820A62526277E007FD40D /* PINAnimatedImageView+PINRemoteImage.h in Headers */,
FBC820A22526277E007FD40D /* PINProgressiveImage.h in Headers */,
9A079DE1268270F900F2E70A /* PINImage+AlternativeTypes.h in Headers */,
939546C02220AF84006031BB /* PINRemoteImageManagerConfiguration.h in Headers */,
FBC820BA2526277E007FD40D /* PINButton+PINRemoteImage.h in Headers */,
FBC820B82526277E007FD40D /* PINRemoteImageMacros.h in Headers */,
Expand Down Expand Up @@ -879,6 +886,7 @@
9DD47F9D1C699F4B00F12CA0 /* PINButton+PINRemoteImage.m in Sources */,
6858C0761C9CC5BA00E420EB /* PINRemoteLock.m in Sources */,
F1B919191BCF23C900710963 /* PINRemoteImageManagerResult.m in Sources */,
9A079DD826826EFD00F2E70A /* PINImage+AlternativeTypes.m in Sources */,
F1B9191D1BCF23C900710963 /* PINRemoteImageTask.m in Sources */,
68B1F2821E679D7A00ED87C4 /* PINRemoteImageDownloadQueue.m in Sources */,
F1B919011BCF23C900710963 /* NSData+ImageDetectors.m in Sources */,
Expand Down
40 changes: 40 additions & 0 deletions Source/Classes/Categories/PINImage+AlternativeTypes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// PINImage+AlternativeTypes.h
// PINRemoteImage
//
// Created by Brandon Li on 6/22/21.
//

#import <Foundation/Foundation.h>

#import "PINRemoteImageMacros.h"

#if PIN_TARGET_IOS
#import <UIKit/UIKit.h>
#elif PIN_TARGET_MAC
#import <Cocoa/Cocoa.h>
#endif

// A decoder that knows how to convert image data into a PINImage.
@protocol PINImageCustomDecoder

- (PINImage *)imageFromData:(NSData *)imageData targetSize:(CGSize)size;

- (BOOL)canRender:(NSData *)imageData;

@end

@interface PINImage (AlternativeTypes)

// The encoded / compressed form of the image data. The data will only be used
// at least one custom decoder that recognizes this data is registered using
// pin_registerCustomDecoder below.
@property(nonatomic, nullable) NSData *pin_encodedImageData;

// Returns an image of the target size.
- (nullable PINImage *)pin_decodedImageUsingCustomDecodersWithSize:(CGSize)size;

// Registers a custom image decoder type.
+ (void)pin_registerCustomDecoder:(id<PINImageCustomDecoder>)customDecoder;

@end
46 changes: 46 additions & 0 deletions Source/Classes/Categories/PINImage+AlternativeTypes.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// PINImage+AlternativeTypes.m
// PINRemoteImage
//
// Created by Brandon Li on 6/22/21.
//

#import "PINImage+AlternativeTypes.h"

#import <objc/runtime.h>

@implementation PINImage (AlternativeTypes)

+ (void)pin_registerCustomDecoder:(id<PINImageCustomDecoder>)customDecoder {
NSMutableArray<id<PINImageCustomDecoder>> *decoders = [PINImage pin_decoders];
[decoders addObject:customDecoder];
objc_setAssociatedObject(self, @selector(pin_registerCustomDecoder:), decoders, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to be using nonatomic here and below? Especially for the class method this seems like it could result in race conditions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good question! It's because I actually do the locking further up the call chain internally where this is used, but your point totally makes sense for more general use

Do you prefer using the atomic keyword or some other locking mechanism in PINRemoteImage?

The priority point is a good one as well. A couple options:

  1. Can simply add a comment that in the case where multiple decoders can handle a given type, the first decoder that was registered will be used (current behavior)
  2. Can have decoders self declare their priority with a new property on PINImageCustomDecoder (in which case there may still be decoders with equal priorities)
  3. Can have pin_decodedImageUsingCustomDecodersWithSize centrally decide some policy on how to assess priority

WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For associated objects, atomic seems like the easiest in this case and it's used elsewhere in the framework.

I think I like # 2 for the options you presented with a comment about equal priorities. I think that would give enough flexibility without complicating the API too much?

Thanks again for thinking all this through!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some internal discussion I updated the PR to remove the custom decoder registry from PINRemoteImage for now and instead change it to a property with a single decoder to be used that is assigned from outside of PINRemoteImage from the place that invokes this logic. In other words, the decision for which decoder to use for the encoded data is the responsibility of the caller, which simplifies the logic for now.

As far as the locking, I changed the associated object setting to be RETAIN instead of RETAIN_NONATOMIC

The thinking is that we could defer the final design of the decoder registry until we actually have a full solution for the remote image scenario, since for now in our solution we'd like the selection of the decoder to be context-specific, and it's not yet clear how to achieve this in PINRemoteImage which doesn't have knowledge of such contexts

How does that sound?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to bump this!


}

- (nullable NSData *)pin_encodedImageData {
return (NSData *)objc_getAssociatedObject(self, @selector(pin_encodedImageData));
}

- (void)setPin_encodedImageData:(NSData *)data {
objc_setAssociatedObject(self, @selector(pin_encodedImageData), data, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (nullable PINImage *)pin_decodedImageUsingCustomDecodersWithSize:(CGSize)size {
NSMutableArray<id<PINImageCustomDecoder>> *decoders = [PINImage pin_decoders];
for (id<PINImageCustomDecoder> decoder in decoders) {
if ([decoder canRender:self.pin_encodedImageData]) {
return [decoder imageFromData:self.pin_encodedImageData targetSize:size];
}
}

return nil;
}

#pragma mark - Private

+ (NSMutableArray<id<PINImageCustomDecoder>> *)pin_decoders {
return objc_getAssociatedObject(self, @selector(pin_registerCustomDecoder:)) ?: [NSMutableArray array];
}

@end