RTCImageStoreManager uses NSData instead of UIImage
Summary: Hi, I'm currently building an app that changes metadata, does some resizes, maybe watermarking ...etc. I want to use RCTImageStoreManager to store the original image in memory and allow me to command different modifications from javascript as it gives me more flexibility. As RCTImageEditingManager does for example. But currently the RTCImageStoreManager uses UIImage to store the image, the problem is that UIImage losses metadata. So i suggest we change it to NSData. Additionally I added a method to remove an image from the store. A related PR can be found here https://github.com/lwansbrough/react-native-camera/pull/100. Closes https://github.com/facebook/react-native/pull/3290 Reviewed By: javache Differential Revision: D2647271 Pulled By: nicklockwood fb-gh-sync-id: e66353ae3005423beee72ec22189dcb117fc719f
This commit is contained in:
parent
83c9741dc9
commit
2b657003b7
|
@ -3,23 +3,34 @@
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
#import "RCTBridge.h"
|
#import "RCTBridge.h"
|
||||||
#import "RCTImageLoader.h"
|
|
||||||
#import "RCTURLRequestHandler.h"
|
#import "RCTURLRequestHandler.h"
|
||||||
|
|
||||||
@interface RCTImageStoreManager : NSObject <RCTImageURLLoader>
|
@interface RCTImageStoreManager : NSObject <RCTURLRequestHandler>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set and get cached images. These must be called from the main thread.
|
* Set and get cached image data asynchronously. It is safe to call these from any
|
||||||
|
* thread. The callbacks will be called on an unspecified thread.
|
||||||
*/
|
*/
|
||||||
- (NSString *)storeImage:(UIImage *)image;
|
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block;
|
||||||
- (UIImage *)imageForTag:(NSString *)imageTag;
|
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block;
|
||||||
|
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set and get cached images asynchronously. It is safe to call these from any
|
* Convenience method to store an image directly (image is converted to data
|
||||||
* thread. The callbacks will be called on the main thread.
|
* internally, so any metadata such as scale or orientation will be lost).
|
||||||
*/
|
*/
|
||||||
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block;
|
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block;
|
||||||
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block;
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface RCTImageStoreManager (Deprecated)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These methods are deprecated - use the data-based alternatives instead.
|
||||||
|
*/
|
||||||
|
- (NSString *)storeImage:(UIImage *)image __deprecated;
|
||||||
|
- (UIImage *)imageForTag:(NSString *)imageTag __deprecated;
|
||||||
|
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block __deprecated;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,21 @@
|
||||||
|
|
||||||
#import "RCTImageStoreManager.h"
|
#import "RCTImageStoreManager.h"
|
||||||
|
|
||||||
|
#import <ImageIO/ImageIO.h>
|
||||||
|
#import <libkern/OSAtomic.h>
|
||||||
|
#import <MobileCoreServices/UTType.h>
|
||||||
|
|
||||||
#import "RCTAssert.h"
|
#import "RCTAssert.h"
|
||||||
|
#import "RCTImageUtils.h"
|
||||||
|
#import "RCTLog.h"
|
||||||
#import "RCTUtils.h"
|
#import "RCTUtils.h"
|
||||||
|
|
||||||
|
static NSString *const RCTImageStoreURLScheme = @"rct-image-store";
|
||||||
|
|
||||||
@implementation RCTImageStoreManager
|
@implementation RCTImageStoreManager
|
||||||
{
|
{
|
||||||
NSMutableDictionary<NSString *, UIImage *> *_store;
|
NSMutableDictionary<NSString *, NSData *> *_store;
|
||||||
|
NSUInteger *_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@synthesize methodQueue = _methodQueue;
|
@synthesize methodQueue = _methodQueue;
|
||||||
|
@ -24,61 +33,76 @@ RCT_EXPORT_MODULE()
|
||||||
- (instancetype)init
|
- (instancetype)init
|
||||||
{
|
{
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
|
|
||||||
// TODO: need a way to clear this store
|
|
||||||
_store = [NSMutableDictionary new];
|
_store = [NSMutableDictionary new];
|
||||||
|
_id = 0;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)storeImage:(UIImage *)image
|
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block
|
||||||
{
|
{
|
||||||
RCTAssertMainThread();
|
dispatch_async(_methodQueue, ^{
|
||||||
NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", _store.count];
|
[self removeImageForTag:imageTag];
|
||||||
_store[tag] = image;
|
if (block) {
|
||||||
return tag;
|
block();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UIImage *)imageForTag:(NSString *)imageTag
|
- (NSString *)_storeImageData:(NSData *)imageData
|
||||||
{
|
{
|
||||||
RCTAssertMainThread();
|
RCTAssertThread(_methodQueue, @"Must be called on RCTImageStoreManager thread");
|
||||||
return _store[imageTag];
|
NSString *imageTag = [NSString stringWithFormat:@"%@://%tu", RCTImageStoreURLScheme, _id++];
|
||||||
|
_store[imageTag] = imageData;
|
||||||
|
return imageTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block
|
||||||
|
{
|
||||||
|
RCTAssertParam(block);
|
||||||
|
dispatch_async(_methodQueue, ^{
|
||||||
|
block([self _storeImageData:imageData]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block
|
||||||
|
{
|
||||||
|
RCTAssertParam(block);
|
||||||
|
dispatch_async(_methodQueue, ^{
|
||||||
|
block(_store[imageTag]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
|
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
|
||||||
{
|
{
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
RCTAssertParam(block);
|
||||||
NSString *imageTag = [self storeImage:image];
|
dispatch_async(_methodQueue, ^{
|
||||||
if (block) {
|
NSString *imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
block(imageTag);
|
block(imageTag);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
|
RCT_EXPORT_METHOD(removeImageForTag:(NSString *)imageTag)
|
||||||
{
|
{
|
||||||
RCTAssert(block != nil, @"block must not be nil");
|
[_store removeObjectForKey:imageTag];
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
block([self imageForTag:imageTag]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedJPEGDataForTag:?
|
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedDataForTag:?
|
||||||
RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
|
RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
|
||||||
successCallback:(RCTResponseSenderBlock)successCallback
|
successCallback:(RCTResponseSenderBlock)successCallback
|
||||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||||
{
|
{
|
||||||
[self getImageForTag:imageTag withBlock:^(UIImage *image) {
|
NSData *imageData = _store[imageTag];
|
||||||
if (!image) {
|
if (!imageData) {
|
||||||
errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
|
errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch_async(_methodQueue, ^{
|
// Dispatching to a background thread to perform base64 encoding
|
||||||
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
|
successCallback(@[[imageData base64EncodedStringWithOptions:0]]);
|
||||||
successCallback(@[[base64 stringByReplacingOccurrencesOfString:@"\n" withString:@""]]);
|
});
|
||||||
});
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
|
RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
|
||||||
|
@ -86,38 +110,114 @@ RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
|
||||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||||
|
|
||||||
{
|
{
|
||||||
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
|
// Dispatching to a background thread to perform base64 decoding
|
||||||
if (imageData) {
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
UIImage *image = [[UIImage alloc] initWithData:imageData];
|
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
|
||||||
[self storeImage:image withBlock:^(NSString *imageTag) {
|
if (imageData) {
|
||||||
successCallback(@[imageTag]);
|
dispatch_async(_methodQueue, ^{
|
||||||
}];
|
successCallback(@[[self _storeImageData:imageData]]);
|
||||||
} else {
|
});
|
||||||
errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String"));
|
} else {
|
||||||
|
errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - RCTURLRequestHandler
|
||||||
|
|
||||||
|
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||||
|
{
|
||||||
|
return [request.URL.scheme caseInsensitiveCompare:RCTImageStoreURLScheme] == NSOrderedSame;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
|
||||||
|
{
|
||||||
|
__block volatile uint32_t cancelled = 0;
|
||||||
|
void (^cancellationBlock)(void) = ^{
|
||||||
|
OSAtomicOr32Barrier(1, &cancelled);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dispatch async to give caller time to cancel the request
|
||||||
|
dispatch_async(_methodQueue, ^{
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *imageTag = request.URL.absoluteString;
|
||||||
|
NSData *imageData = _store[imageTag];
|
||||||
|
if (!imageData) {
|
||||||
|
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
|
||||||
|
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
|
||||||
|
if (!sourceRef) {
|
||||||
|
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Unable to decode data for imageTag: %@", imageTag]);
|
||||||
|
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CFStringRef UTI = CGImageSourceGetType(sourceRef);
|
||||||
|
CFRelease(sourceRef);
|
||||||
|
|
||||||
|
NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
|
||||||
|
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
|
||||||
|
MIMEType:MIMEType
|
||||||
|
expectedContentLength:imageData.length
|
||||||
|
textEncodingName:nil];
|
||||||
|
|
||||||
|
[delegate URLRequest:cancellationBlock didReceiveResponse:response];
|
||||||
|
[delegate URLRequest:cancellationBlock didReceiveData:imageData];
|
||||||
|
[delegate URLRequest:cancellationBlock didCompleteWithError:nil];
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return cancellationBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)cancelRequest:(id)requestToken
|
||||||
|
{
|
||||||
|
if (requestToken) {
|
||||||
|
((void (^)(void))requestToken)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - RCTImageLoader
|
@end
|
||||||
|
|
||||||
- (BOOL)canLoadImageURL:(NSURL *)requestURL
|
@implementation RCTImageStoreManager (Deprecated)
|
||||||
|
|
||||||
|
- (NSString *)storeImage:(UIImage *)image
|
||||||
{
|
{
|
||||||
return [requestURL.scheme.lowercaseString isEqualToString:@"rct-image-store"];
|
RCTAssertMainThread();
|
||||||
|
RCTLogWarn(@"RCTImageStoreManager.storeImage() is deprecated and has poor performance. Use an alternative method instead.");
|
||||||
|
__block NSString *imageTag;
|
||||||
|
dispatch_sync(_methodQueue, ^{
|
||||||
|
imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)];
|
||||||
|
});
|
||||||
|
return imageTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
|
- (UIImage *)imageForTag:(NSString *)imageTag
|
||||||
{
|
{
|
||||||
NSString *imageTag = imageURL.absoluteString;
|
RCTAssertMainThread();
|
||||||
[self getImageForTag:imageTag withBlock:^(UIImage *image) {
|
RCTLogWarn(@"RCTImageStoreManager.imageForTag() is deprecated and has poor performance. Use an alternative method instead.");
|
||||||
if (image) {
|
__block NSData *imageData;
|
||||||
completionHandler(nil, image);
|
dispatch_sync(_methodQueue, ^{
|
||||||
} else {
|
imageData = _store[imageTag];
|
||||||
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag];
|
});
|
||||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
return [UIImage imageWithData:imageData];
|
||||||
completionHandler(error, nil);
|
}
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
return nil;
|
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
|
||||||
|
{
|
||||||
|
RCTAssertParam(block);
|
||||||
|
dispatch_async(_methodQueue, ^{
|
||||||
|
NSData *imageData = _store[imageTag];
|
||||||
|
UIImage *image = [UIImage imageWithData:imageData];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
block(image);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -57,3 +57,12 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
|
||||||
CGSize destSize,
|
CGSize destSize,
|
||||||
CGFloat destScale,
|
CGFloat destScale,
|
||||||
UIViewContentMode resizeMode);
|
UIViewContentMode resizeMode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an image back into data. Images with an alpha channel will be
|
||||||
|
* converted to lossless PNG data. Images without alpha will be converted to
|
||||||
|
* JPEG. The `quality` argument controls the compression ratio of the JPEG
|
||||||
|
* conversion, with 1.0 being maximum quality. It has no effect for images
|
||||||
|
* using PNG compression.
|
||||||
|
*/
|
||||||
|
RCT_EXTERN NSData *RCTGetImageData(CGImageRef image, float quality);
|
||||||
|
|
|
@ -10,9 +10,11 @@
|
||||||
#import "RCTImageUtils.h"
|
#import "RCTImageUtils.h"
|
||||||
|
|
||||||
#import <ImageIO/ImageIO.h>
|
#import <ImageIO/ImageIO.h>
|
||||||
|
#import <MobileCoreServices/UTCoreTypes.h>
|
||||||
#import <tgmath.h>
|
#import <tgmath.h>
|
||||||
|
|
||||||
#import "RCTLog.h"
|
#import "RCTLog.h"
|
||||||
|
#import "RCTUtils.h"
|
||||||
|
|
||||||
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
|
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
|
||||||
{
|
{
|
||||||
|
@ -197,7 +199,7 @@ BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
|
CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
|
||||||
{
|
{
|
||||||
return (CGSize){
|
return (CGSize){
|
||||||
ceil(pointSize.width * scale),
|
ceil(pointSize.width * scale),
|
||||||
|
@ -205,10 +207,10 @@ RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
|
UIImage *RCTDecodeImageWithData(NSData *data,
|
||||||
CGSize destSize,
|
CGSize destSize,
|
||||||
CGFloat destScale,
|
CGFloat destScale,
|
||||||
UIViewContentMode resizeMode)
|
UIViewContentMode resizeMode)
|
||||||
{
|
{
|
||||||
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
||||||
if (!sourceRef) {
|
if (!sourceRef) {
|
||||||
|
@ -251,10 +253,9 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
//adjust scale
|
// adjust scale
|
||||||
size_t actualWidth = CGImageGetWidth(imageRef);
|
size_t actualWidth = CGImageGetWidth(imageRef);
|
||||||
CGFloat scale = actualWidth / targetSize.width;
|
CGFloat scale = actualWidth / targetSize.width;
|
||||||
|
|
||||||
// return image
|
// return image
|
||||||
UIImage *image = [UIImage imageWithCGImage:imageRef
|
UIImage *image = [UIImage imageWithCGImage:imageRef
|
||||||
scale:scale
|
scale:scale
|
||||||
|
@ -262,3 +263,26 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
|
||||||
CGImageRelease(imageRef);
|
CGImageRelease(imageRef);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSData *RCTGetImageData(CGImageRef image, float quality)
|
||||||
|
{
|
||||||
|
NSDictionary *properties;
|
||||||
|
CGImageDestinationRef destination;
|
||||||
|
CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0);
|
||||||
|
if (RCTImageHasAlpha(image)) {
|
||||||
|
// get png data
|
||||||
|
destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL);
|
||||||
|
} else {
|
||||||
|
// get jpeg data
|
||||||
|
destination = CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, NULL);
|
||||||
|
properties = @{(NSString *)kCGImageDestinationLossyCompressionQuality: @(quality)};
|
||||||
|
}
|
||||||
|
CGImageDestinationAddImage(destination, image, (__bridge CFDictionaryRef)properties);
|
||||||
|
if (!CGImageDestinationFinalize(destination))
|
||||||
|
{
|
||||||
|
CFRelease(imageData);
|
||||||
|
imageData = NULL;
|
||||||
|
}
|
||||||
|
CFRelease(destination);
|
||||||
|
return (__bridge_transfer NSData *)imageData;
|
||||||
|
}
|
||||||
|
|
|
@ -61,8 +61,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||||
|
|
||||||
- (void)cancel
|
- (void)cancel
|
||||||
{
|
{
|
||||||
if ([_handler respondsToSelector:@selector(cancelRequest:)]) {
|
__strong id strongToken = _requestToken;
|
||||||
[_handler cancelRequest:_requestToken];
|
if (strongToken && [_handler respondsToSelector:@selector(cancelRequest:)]) {
|
||||||
|
[_handler cancelRequest:strongToken];
|
||||||
}
|
}
|
||||||
[self invalidate];
|
[self invalidate];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue