Skip to content

Commit a2a54ec

Browse files
committed
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
1 parent 996f65e commit a2a54ec

File tree

3 files changed

+167
-40
lines changed

3 files changed

+167
-40
lines changed

SDWebImageSwiftUI/Classes/AnimatedImage.swift

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@ import SDWebImageSwiftUIObjC
1414

1515
// Convenient
1616
#if os(watchOS)
17-
public typealias AnimatedImageViewWrapper = SDAnimatedImageInterface
18-
extension SDAnimatedImageInterface {
19-
var wrapped: SDAnimatedImageInterface {
20-
return self
21-
}
22-
}
17+
public typealias AnimatedImageViewWrapper = SDAnimatedImageInterfaceWrapper
2318
#endif
2419

2520
// Coordinator Life Cycle Binding Object
@@ -562,19 +557,7 @@ extension AnimatedImage {
562557
var result = self
563558
result.aspectRatio = aspectRatio
564559
result.contentMode = contentMode
565-
#if os(macOS) || os(iOS) || os(tvOS)
566560
return result.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
567-
#else
568-
return Group {
569-
if aspectRatio != nil {
570-
result.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
571-
} else {
572-
// on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues
573-
// To workaround, we do not call default implementation for this case, using original solution instead
574-
result
575-
}
576-
}
577-
#endif
578561
}
579562

580563
/// Constrains this view's dimensions to the aspect ratio of the given size.

SDWebImageSwiftUI/Classes/ObjC/SDAnimatedImageInterface.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,13 @@ NS_ASSUME_NONNULL_BEGIN
2929

3030
@end
3131

32+
@interface SDAnimatedImageInterfaceWrapper : WKInterfaceGroup
33+
34+
@property (nonatomic, strong, nonnull) SDAnimatedImageInterface *wrapped;
35+
36+
- (instancetype)init WK_AVAILABLE_WATCHOS_ONLY(6.0);
37+
38+
@end
39+
3240
NS_ASSUME_NONNULL_END
3341
#endif

SDWebImageSwiftUI/Classes/ObjC/SDAnimatedImageInterface.m

Lines changed: 158 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,10 @@
99
#import "SDAnimatedImageInterface.h"
1010
#if SD_WATCH
1111

12-
#pragma mark - SPI
12+
#import <objc/runtime.h>
13+
#import <objc/message.h>
1314

14-
static UIImage * SharedEmptyImage(void) {
15-
// This is used for placeholder on `WKInterfaceImage`
16-
// Do not using `[UIImage new]` because WatchKit will ignore it
17-
static dispatch_once_t onceToken;
18-
static UIImage *image;
19-
dispatch_once(&onceToken, ^{
20-
UIColor *color = UIColor.clearColor;
21-
CGRect rect = WKInterfaceDevice.currentDevice.screenBounds;
22-
UIGraphicsBeginImageContext(rect.size);
23-
CGContextRef context = UIGraphicsGetCurrentContext();
24-
CGContextSetFillColorWithColor(context, [color CGColor]);
25-
CGContextFillRect(context, rect);
26-
image = UIGraphicsGetImageFromCurrentImageContext();
27-
UIGraphicsEndImageContext();
28-
});
29-
return image;
30-
}
15+
#pragma mark - SPI
3116

3217
@protocol CALayerProtocol <NSObject>
3318
@property (nullable, strong) id contents;
@@ -43,6 +28,22 @@ @protocol UIViewProtocol <NSObject>
4328
@property (nonatomic) CGFloat alpha;
4429
@property (nonatomic, getter=isHidden) BOOL hidden;
4530
@property (nonatomic, getter=isOpaque) BOOL opaque;
31+
@property (nonatomic) CGRect frame;
32+
@property (nonatomic) CGRect bounds;
33+
@property (nonatomic) CGPoint center;
34+
@property (nonatomic) BOOL clipsToBounds;
35+
@property (nonatomic, readonly) CGSize intrinsicContentSize;
36+
@property(nonatomic) NSInteger tag;
37+
38+
- (void)invalidateIntrinsicContentSize;
39+
- (void)drawRect:(CGRect)rect;
40+
- (void)setNeedsDisplay;
41+
- (void)setNeedsDisplayInRect:(CGRect)rect;
42+
- (void)addSubview:(id<UIViewProtocol>)view;
43+
- (void)removeFromSuperview;
44+
- (void)layoutSubviews;
45+
- (CGSize)sizeThatFits:(CGSize)size;
46+
- (void)sizeToFit;
4647

4748
@end
4849

@@ -60,7 +61,7 @@ @interface WKInterfaceObject ()
6061
// This is needed for dynamic created WKInterfaceObject, like `WKInterfaceMap`
6162
- (instancetype)_initForDynamicCreationWithInterfaceProperty:(NSString *)property;
6263
// This is remote UIView
63-
@property (nonatomic, strong, readonly) id<UIImageViewProtocol> _interfaceView;
64+
@property (nonatomic, strong, readwrite) id<UIViewProtocol> _interfaceView;
6465

6566
@end
6667

@@ -97,7 +98,6 @@ - (NSDictionary *)interfaceDescriptionForDynamicCreation {
9798
return @{
9899
@"type" : @"image",
99100
@"property" : self.interfaceProperty,
100-
@"image" : SharedEmptyImage()
101101
};
102102
}
103103

@@ -113,8 +113,7 @@ - (void)setImage:(UIImage *)image {
113113
self.currentFrameIndex = 0;
114114
self.currentLoopCount = 0;
115115

116-
[super setImage:image];
117-
[self _interfaceView].image = image;
116+
((id<UIImageViewProtocol>)[self _interfaceView]).image = image;
118117
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
119118
// Create animted player
120119
self.player = [SDAnimatedImagePlayer playerWithProvider:(id<SDAnimatedImage>)image];
@@ -257,5 +256,142 @@ - (void)sd_setImageWithURL:(nullable NSURL *)url
257256
}];
258257
}
259258

259+
@end
260+
261+
262+
#define SDAnimatedImageInterfaceWrapperTag 123456789
263+
#define SDAnimatedImageInterfaceWrapperSEL_layoutSubviews @"SDAnimatedImageInterfaceWrapper_layoutSubviews"
264+
#define SDAnimatedImageInterfaceWrapperSEL_sizeThatFits @" SDAnimatedImageInterfaceWrapper_sizeThatFits:"
265+
266+
// This using hook to implements the same logic like AnimatedImageViewWrapper.swift
267+
static CGSize intrinsicContentSizeIMP(id<UIViewProtocol> self, SEL _cmd) {
268+
struct objc_super superClass = {
269+
self,
270+
[self superclass]
271+
};
272+
NSUInteger tag = self.tag;
273+
id<UIViewProtocol> interfaceView = self.subviews.firstObject;
274+
if (tag != SDAnimatedImageInterfaceWrapperTag || !interfaceView) {
275+
return ((CGSize(*)(id, SEL))objc_msgSendSuper)((__bridge id)(&superClass), _cmd);
276+
}
277+
CGSize size = interfaceView.intrinsicContentSize;
278+
if (size.width > 0 && size.height > 0) {
279+
CGFloat aspectRatio = size.height / size.width;
280+
return CGSizeMake(1, 1 * aspectRatio);
281+
} else {
282+
return CGSizeMake(-1, -1);
283+
}
284+
}
285+
286+
static void layoutSubviewsIMP(id<UIViewProtocol> self, SEL _cmd) {
287+
struct objc_super superClass = {
288+
self,
289+
[self superclass]
290+
};
291+
NSUInteger tag = self.tag;
292+
id<UIViewProtocol> interfaceView = self.subviews.firstObject;
293+
if (tag != SDAnimatedImageInterfaceWrapperTag || !interfaceView) {
294+
((void(*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_layoutSubviews));
295+
return;
296+
}
297+
((void(*)(id, SEL))objc_msgSendSuper)((__bridge id)(&superClass), _cmd);
298+
interfaceView.frame = self.bounds;
299+
}
300+
301+
// This is suck that SwiftUI on watchOS will call extra sizeThatFits, we should always input size (already calculated with aspectRatio)
302+
// iOS's wrapper don't need this
303+
static CGSize sizeThatFitsIMP(id<UIViewProtocol> self, SEL _cmd, CGSize size) {
304+
NSUInteger tag = self.tag;
305+
id<UIViewProtocol> interfaceView = self.subviews.firstObject;
306+
if (tag != SDAnimatedImageInterfaceWrapperTag || !interfaceView) {
307+
return ((CGSize(*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_sizeThatFits));
308+
}
309+
return size;
310+
}
311+
312+
@implementation SDAnimatedImageInterfaceWrapper
313+
314+
+ (void)load {
315+
static dispatch_once_t onceToken;
316+
dispatch_once(&onceToken, ^{
317+
Class class = NSClassFromString(@"SPInterfaceGroupView");
318+
// Implements `intrinsicContentSize`
319+
SEL selector = @selector(intrinsicContentSize);
320+
Method method = class_getInstanceMethod(class, selector);
321+
322+
BOOL didAddMethod =
323+
class_addMethod(class,
324+
selector,
325+
(IMP)intrinsicContentSizeIMP,
326+
method_getTypeEncoding(method));
327+
if (!didAddMethod) {
328+
NSAssert(NO, @"SDAnimatedImageInterfaceWrapper will not work as expected.");
329+
}
330+
331+
// Override `layoutSubviews`
332+
SEL originalSelector = @selector(layoutSubviews);
333+
SEL swizzledSelector = NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_layoutSubviews);
334+
Method originalMethod = class_getInstanceMethod(class, originalSelector);
335+
336+
didAddMethod =
337+
class_addMethod(class,
338+
swizzledSelector,
339+
(IMP)layoutSubviewsIMP,
340+
method_getTypeEncoding(originalMethod));
341+
if (!didAddMethod) {
342+
NSAssert(NO, @"SDAnimatedImageInterfaceWrapper will not work as expected.");
343+
} else {
344+
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
345+
method_exchangeImplementations(originalMethod, swizzledMethod);
346+
}
347+
348+
// Override `sizeThatFits:`
349+
originalSelector = @selector(sizeThatFits:);
350+
swizzledSelector = NSSelectorFromString(SDAnimatedImageInterfaceWrapperSEL_sizeThatFits);
351+
originalMethod = class_getInstanceMethod(class, originalSelector);
352+
353+
didAddMethod =
354+
class_addMethod(class,
355+
swizzledSelector,
356+
(IMP)sizeThatFitsIMP,
357+
method_getTypeEncoding(originalMethod));
358+
if (!didAddMethod) {
359+
NSAssert(NO, @"SDAnimatedImageInterfaceWrapper will not work as expected.");
360+
} else {
361+
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
362+
method_exchangeImplementations(originalMethod, swizzledMethod);
363+
}
364+
});
365+
}
366+
367+
- (instancetype)init {
368+
Class cls = [self class];
369+
NSString *UUID = [NSUUID UUID].UUIDString;
370+
NSString *property = [NSString stringWithFormat:@"%@_%@", cls, UUID];
371+
self = [self _initForDynamicCreationWithInterfaceProperty:property];
372+
if (self) {
373+
self.wrapped = [[SDAnimatedImageInterface alloc] init];
374+
}
375+
return self;
376+
}
377+
378+
- (NSDictionary *)interfaceDescriptionForDynamicCreation {
379+
// This is called by WatchKit to provide default value
380+
return @{
381+
@"type" : @"group",
382+
@"property" : self.interfaceProperty,
383+
@"radius" : @(0),
384+
@"items": @[self.wrapped.interfaceDescriptionForDynamicCreation], // This will create the native view and added to subview
385+
};
386+
}
387+
388+
- (void)set_interfaceView:(id<UIViewProtocol>)interfaceView {
389+
// This is called by WatchKit when native view created
390+
[super set_interfaceView:interfaceView];
391+
// Bind the interface object and native view
392+
interfaceView.tag = SDAnimatedImageInterfaceWrapperTag;
393+
self.wrapped._interfaceView = interfaceView.subviews.firstObject;
394+
}
395+
260396
@end
261397
#endif

0 commit comments

Comments
 (0)