Using a new super evil hack solution on watchOS, solve the problem that aspectRatio does not works as expected.

This code by using the native UIView and logic by reverse engineering
This commit is contained in:
DreamPiggy 2019-11-10 05:01:29 +08:00
parent 996f65ea15
commit a2a54ec329
3 changed files with 167 additions and 40 deletions

View File

@ -14,12 +14,7 @@ import SDWebImageSwiftUIObjC
// Convenient
#if os(watchOS)
public typealias AnimatedImageViewWrapper = SDAnimatedImageInterface
extension SDAnimatedImageInterface {
var wrapped: SDAnimatedImageInterface {
return self
}
}
public typealias AnimatedImageViewWrapper = SDAnimatedImageInterfaceWrapper
#endif
// Coordinator Life Cycle Binding Object
@ -562,19 +557,7 @@ extension AnimatedImage {
var result = self
result.aspectRatio = aspectRatio
result.contentMode = contentMode
#if os(macOS) || os(iOS) || os(tvOS)
return result.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
#else
return Group {
if aspectRatio != nil {
result.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
} else {
// on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues
// To workaround, we do not call default implementation for this case, using original solution instead
result
}
}
#endif
}
/// Constrains this view's dimensions to the aspect ratio of the given size.

View File

@ -29,5 +29,13 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface SDAnimatedImageInterfaceWrapper : WKInterfaceGroup
@property (nonatomic, strong, nonnull) SDAnimatedImageInterface *wrapped;
- (instancetype)init WK_AVAILABLE_WATCHOS_ONLY(6.0);
@end
NS_ASSUME_NONNULL_END
#endif

View File

@ -9,25 +9,10 @@
#import "SDAnimatedImageInterface.h"
#if SD_WATCH
#pragma mark - SPI
#import <objc/runtime.h>
#import <objc/message.h>
static UIImage * SharedEmptyImage(void) {
// This is used for placeholder on `WKInterfaceImage`
// Do not using `[UIImage new]` because WatchKit will ignore it
static dispatch_once_t onceToken;
static UIImage *image;
dispatch_once(&onceToken, ^{
UIColor *color = UIColor.clearColor;
CGRect rect = WKInterfaceDevice.currentDevice.screenBounds;
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
return image;
}
#pragma mark - SPI
@protocol CALayerProtocol <NSObject>
@property (nullable, strong) id contents;
@ -43,6 +28,22 @@ static UIImage * SharedEmptyImage(void) {
@property (nonatomic) CGFloat alpha;
@property (nonatomic, getter=isHidden) BOOL hidden;
@property (nonatomic, getter=isOpaque) BOOL opaque;
@property (nonatomic) CGRect frame;
@property (nonatomic) CGRect bounds;
@property (nonatomic) CGPoint center;
@property (nonatomic) BOOL clipsToBounds;
@property (nonatomic, readonly) CGSize intrinsicContentSize;
@property(nonatomic) NSInteger tag;
- (void)invalidateIntrinsicContentSize;
- (void)drawRect:(CGRect)rect;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
- (void)addSubview:(id<UIViewProtocol>)view;
- (void)removeFromSuperview;
- (void)layoutSubviews;
- (CGSize)sizeThatFits:(CGSize)size;
- (void)sizeToFit;
@end
@ -60,7 +61,7 @@ static UIImage * SharedEmptyImage(void) {
// This is needed for dynamic created WKInterfaceObject, like `WKInterfaceMap`
- (instancetype)_initForDynamicCreationWithInterfaceProperty:(NSString *)property;
// This is remote UIView
@property (nonatomic, strong, readonly) id<UIImageViewProtocol> _interfaceView;
@property (nonatomic, strong, readwrite) id<UIViewProtocol> _interfaceView;
@end
@ -97,7 +98,6 @@ static UIImage * SharedEmptyImage(void) {
return @{
@"type" : @"image",
@"property" : self.interfaceProperty,
@"image" : SharedEmptyImage()
};
}
@ -113,8 +113,7 @@ static UIImage * SharedEmptyImage(void) {
self.currentFrameIndex = 0;
self.currentLoopCount = 0;
[super setImage:image];
[self _interfaceView].image = image;
((id<UIImageViewProtocol>)[self _interfaceView]).image = image;
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
// Create animted player
self.player = [SDAnimatedImagePlayer playerWithProvider:(id<SDAnimatedImage>)image];
@ -257,5 +256,142 @@ static UIImage * SharedEmptyImage(void) {
}];
}
@end
#define SDAnimatedImageInterfaceWrapperTag 123456789
#define SDAnimatedImageInterfaceWrapperSEL_layoutSubviews @"SDAnimatedImageInterfaceWrapper_layoutSubviews"
#define SDAnimatedImageInterfaceWrapperSEL_sizeThatFits @" SDAnimatedImageInterfaceWrapper_sizeThatFits:"
// This using hook to implements the same logic like AnimatedImageViewWrapper.swift
static CGSize intrinsicContentSizeIMP(id<UIViewProtocol> self, SEL _cmd) {
struct objc_super superClass = {
self,
[self superclass]
};
NSUInteger tag = self.tag;
id<UIViewProtocol> interfaceView = self.subviews.firstObject;
if (tag != SDAnimatedImageInterfaceWrapperTag || !interfaceView) {
return ((CGSize(*)(id, SEL))objc_msgSendSuper)((__bridge id)(&superClass), _cmd);
}
CGSize size = interfaceView.intrinsicContentSize;
if (size.width > 0 && size.height > 0) {
CGFloat aspectRatio = size.height / size.width;
return CGSizeMake(1, 1 * aspectRatio);
} else {
return CGSizeMake(-1, -1);
}
}
static void layoutSubviewsIMP(id<UIViewProtocol> self, SEL _cmd) {
struct objc_super superClass = {
self,
[self superclass]
};
NSUInteger tag = self.tag;
id<UIViewProtocol> interfaceView = self.subviews.firstObject;
if (tag != SDAnimatedImageInterfaceWrapperTag || !interfaceView) {
((void(*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_layoutSubviews));
return;
}
((void(*)(id, SEL))objc_msgSendSuper)((__bridge id)(&superClass), _cmd);
interfaceView.frame = self.bounds;
}
// This is suck that SwiftUI on watchOS will call extra sizeThatFits, we should always input size (already calculated with aspectRatio)
// iOS's wrapper don't need this
static CGSize sizeThatFitsIMP(id<UIViewProtocol> self, SEL _cmd, CGSize size) {
NSUInteger tag = self.tag;
id<UIViewProtocol> interfaceView = self.subviews.firstObject;
if (tag != SDAnimatedImageInterfaceWrapperTag || !interfaceView) {
return ((CGSize(*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_sizeThatFits));
}
return size;
}
@implementation SDAnimatedImageInterfaceWrapper
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = NSClassFromString(@"SPInterfaceGroupView");
// Implements `intrinsicContentSize`
SEL selector = @selector(intrinsicContentSize);
Method method = class_getInstanceMethod(class, selector);
BOOL didAddMethod =
class_addMethod(class,
selector,
(IMP)intrinsicContentSizeIMP,
method_getTypeEncoding(method));
if (!didAddMethod) {
NSAssert(NO, @"SDAnimatedImageInterfaceWrapper will not work as expected.");
}
// Override `layoutSubviews`
SEL originalSelector = @selector(layoutSubviews);
SEL swizzledSelector = NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_layoutSubviews);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
didAddMethod =
class_addMethod(class,
swizzledSelector,
(IMP)layoutSubviewsIMP,
method_getTypeEncoding(originalMethod));
if (!didAddMethod) {
NSAssert(NO, @"SDAnimatedImageInterfaceWrapper will not work as expected.");
} else {
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
// Override `sizeThatFits:`
originalSelector = @selector(sizeThatFits:);
swizzledSelector = NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_sizeThatFits);
originalMethod = class_getInstanceMethod(class, originalSelector);
didAddMethod =
class_addMethod(class,
swizzledSelector,
(IMP)sizeThatFitsIMP,
method_getTypeEncoding(originalMethod));
if (!didAddMethod) {
NSAssert(NO, @"SDAnimatedImageInterfaceWrapper will not work as expected.");
} else {
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (instancetype)init {
Class cls = [self class];
NSString *UUID = [NSUUID UUID].UUIDString;
NSString *property = [NSString stringWithFormat:@"%@_%@", cls, UUID];
self = [self _initForDynamicCreationWithInterfaceProperty:property];
if (self) {
self.wrapped = [[SDAnimatedImageInterface alloc] init];
}
return self;
}
- (NSDictionary *)interfaceDescriptionForDynamicCreation {
// This is called by WatchKit to provide default value
return @{
@"type" : @"group",
@"property" : self.interfaceProperty,
@"radius" : @(0),
@"items": @[self.wrapped.interfaceDescriptionForDynamicCreation], // This will create the native view and added to subview
};
}
- (void)set_interfaceView:(id<UIViewProtocol>)interfaceView {
// This is called by WatchKit when native view created
[super set_interfaceView:interfaceView];
// Bind the interface object and native view
interfaceView.tag = SDAnimatedImageInterfaceWrapperTag;
self.wrapped._interfaceView = interfaceView.subviews.firstObject;
}
@end
#endif