Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Unreleased
- [feature] Added proxy support for `scene:willConnectToSession:options:` and
`scene:continueUserActivity:` to `GULSceneDelegateSwizzler`. Existing implementations
will override this behavior, so current scene delegate implementations will
continue to work as expected. (#242)
- [fixed] Fixed a potentional race-condition where `GULSceneDelegateSwizzler`
could miss being configured on scene delegates if they were spawned before
the proxy was ready. (#242)

# 8.1.2
- [fixed] Resolve EXC_BAD_ACCESS in GULNetworkURLSession via O(1) passive memory
lifecycle cleanup. (#233)
Expand Down
88 changes: 82 additions & 6 deletions GoogleUtilities/AppDelegateSwizzler/GULSceneDelegateSwizzler.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (*GULOpenURLContextsIMP)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);

API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (*GULWillConnectToSessionIMP)(
id, SEL, UIScene *, UISceneSession *, UISceneConnectionOptions *);

API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (*GULContinueUserActivityIMP)(id, SEL, UIScene *, NSUserActivity *);

API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (^GULSceneDelegateInterceptorCallback)(id<UISceneDelegate>);

Expand Down Expand Up @@ -96,6 +103,18 @@ + (void)proxyOriginalSceneDelegate {
selector:@selector(handleSceneWillConnectToNotification:)
name:UISceneWillConnectNotification
object:nil];

// For catching scene delegates that were already connected when swizzling started,
// which may or may not be an actual problem. Not sure, but better safe than sorry.
id sharedApplication = [GULAppDelegateSwizzler sharedApplication];
if ([sharedApplication respondsToSelector:@selector(openSessions)]) {
NSSet<UISceneSession *> *sessions = [sharedApplication openSessions];
for (UISceneSession *session in sessions) {
if (session.scene) {
[GULSceneDelegateSwizzler proxySceneDelegateIfNeeded:session.scene];
}
}
}
}
});
#endif // UISCENE_SUPPORTED
Expand Down Expand Up @@ -327,12 +346,7 @@ - (void)scene:(UIScene *)scene
[GULSceneDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<UISceneDelegate> interceptor) {
if ([interceptor
conformsToProtocol:@protocol(UISceneDelegate)]) {
id<UISceneDelegate> sceneInterceptor =
(id<UISceneDelegate>)interceptor;
[sceneInterceptor scene:scene openURLContexts:URLContexts];
}
[interceptor scene:scene openURLContexts:URLContexts];
}];

if (openURLContextsIMP) {
Expand All @@ -341,6 +355,50 @@ - (void)scene:(UIScene *)scene
}
}

- (void)scene:(UIScene *)scene
willConnectToSession:(UISceneSession *)session
options:(UISceneConnectionOptions *)connectionOptions
API_AVAILABLE(ios(13.0), tvos(13.0)) {
SEL methodSelector = @selector(scene:willConnectToSession:options:);
NSValue *willConnectToSessionIMPPointer =
[GULSceneDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULWillConnectToSessionIMP willConnectToSessionIMP =
[willConnectToSessionIMPPointer pointerValue];

[GULSceneDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<UISceneDelegate> interceptor) {
[interceptor scene:scene
willConnectToSession:session
options:connectionOptions];
}];

// Call the real implementation if the real Scene Delegate has any.
if (willConnectToSessionIMP) {
willConnectToSessionIMP(self, methodSelector, scene, session, connectionOptions);
}
}

- (void)scene:(UIScene *)scene
continueUserActivity:(NSUserActivity *)userActivity API_AVAILABLE(ios(13.0), tvos(13.0)) {
SEL methodSelector = @selector(scene:continueUserActivity:);
NSValue *continueUserActivityIMPPointer =
[GULSceneDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULContinueUserActivityIMP continueUserActivityIMP =
[continueUserActivityIMPPointer pointerValue];

[GULSceneDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<UISceneDelegate> interceptor) {
[interceptor scene:scene continueUserActivity:userActivity];
}];

// Call the real implementation if the real Scene Delegate has any.
if (continueUserActivityIMP) {
continueUserActivityIMP(self, methodSelector, scene, userActivity);
}
}

+ (void)proxySceneDelegateIfNeeded:(UIScene *)scene {
Class realClass = [scene.delegate class];
NSString *className = NSStringFromClass(realClass);
Expand Down Expand Up @@ -396,6 +454,24 @@ + (void)proxySceneDelegateIfNeeded:(UIScene *)scene {
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];

// For scene:willConnectToSession:options:
SEL willConnectToSessionSEL = @selector(scene:willConnectToSession:options:);
[self proxyDestinationSelector:willConnectToSessionSEL
implementationsFromSourceSelector:willConnectToSessionSEL
fromClass:[GULSceneDelegateSwizzler class]
toClass:sceneDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];

// For scene:continueUserActivity:
SEL continueUserActivitySEL = @selector(scene:continueUserActivity:);
[self proxyDestinationSelector:continueUserActivitySEL
implementationsFromSourceSelector:continueUserActivitySEL
fromClass:[GULSceneDelegateSwizzler class]
toClass:sceneDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];

// Store original implementations to a fake property of the original delegate.
objc_setAssociatedObject(scene.delegate, &kGULRealIMPBySelectorKey,
[realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Expand Down
143 changes: 143 additions & 0 deletions GoogleUtilities/Tests/Unit/Swizzler/GULSceneDelegateSwizzlerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,39 @@ @interface GULTestSceneDelegate : NSObject <UISceneDelegate>
@implementation GULTestSceneDelegate
@end

API_AVAILABLE(ios(13.0), tvos(13.0))
@interface GULImplementingTestSceneDelegate : NSObject <UISceneDelegate>
@property(nonatomic, assign) BOOL openURLContextsInvoked;
@property(nonatomic, assign) BOOL willConnectToSessionOptionsInvoked;
@property(nonatomic, assign) BOOL continueUserActivityInvoked;
@end

@implementation GULImplementingTestSceneDelegate
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
_openURLContextsInvoked = YES;
}
- (void)scene:(UIScene *)scene
willConnectToSession:(UISceneSession *)session
options:(UISceneConnectionOptions *)connectionOptions {
_willConnectToSessionOptionsInvoked = YES;
}
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
_continueUserActivityInvoked = YES;
}
@end

@interface GULSceneDelegateSwizzlerTest : XCTestCase
@end

@implementation GULSceneDelegateSwizzlerTest

- (void)tearDown {
if (@available(iOS 13.0, tvOS 13.0, *)) {
[GULSceneDelegateSwizzler clearInterceptors];
}
[super tearDown];
}

- (void)testProxySceneDelegateWithNoSceneDelegate {
if (@available(iOS 13, tvOS 13, *)) {
id mockSharedScene = OCMClassMock([UIScene class]);
Expand Down Expand Up @@ -85,6 +113,9 @@ - (void)testProxySceneDelegate {

// After being proxied, it should be able to respond to the required method selector.
XCTAssertTrue([realSceneDelegate respondsToSelector:@selector(scene:openURLContexts:)]);
XCTAssertTrue(
[realSceneDelegate respondsToSelector:@selector(scene:willConnectToSession:options:)]);
XCTAssertTrue([realSceneDelegate respondsToSelector:@selector(scene:continueUserActivity:)]);

// Make sure that the class has changed.
XCTAssertNotEqualObjects([realSceneDelegate class], realSceneDelegateClassBefore);
Expand Down Expand Up @@ -144,6 +175,118 @@ - (void)testSceneOpenURLContextsIsInvokedOnInterceptors {
}
}

- (void)testSceneWillConnectToSessionOptionsIsInvokedOnInterceptors {
if (@available(iOS 13, tvOS 13, *)) {
id mockSession = OCMClassMock([UISceneSession class]);
id mockConnectionOptions = OCMClassMock([UISceneConnectionOptions class]);

GULTestSceneDelegate *realSceneDelegate = [[GULTestSceneDelegate alloc] init];
id mockSharedScene = OCMClassMock([UIScene class]);
OCMStub([mockSharedScene delegate]).andReturn(realSceneDelegate);

id interceptor = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor scene:mockSharedScene
willConnectToSession:mockSession
options:mockConnectionOptions]);

id interceptor2 = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor2 scene:mockSharedScene
willConnectToSession:mockSession
options:mockConnectionOptions]);

[GULSceneDelegateSwizzler proxySceneDelegateIfNeeded:mockSharedScene];

[GULSceneDelegateSwizzler registerSceneDelegateInterceptor:interceptor];
[GULSceneDelegateSwizzler registerSceneDelegateInterceptor:interceptor2];

[realSceneDelegate scene:mockSharedScene
willConnectToSession:mockSession
options:mockConnectionOptions];
OCMVerifyAll(interceptor);
OCMVerifyAll(interceptor2);

[mockSharedScene stopMocking];
[mockSession stopMocking];
[mockConnectionOptions stopMocking];
mockSharedScene = nil;
mockSession = nil;
mockConnectionOptions = nil;
}
}

- (void)testSceneContinueUserActivityIsInvokedOnInterceptors {
if (@available(iOS 13, tvOS 13, *)) {
NSUserActivity *userActivity =
[[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];

GULTestSceneDelegate *realSceneDelegate = [[GULTestSceneDelegate alloc] init];
id mockSharedScene = OCMClassMock([UIScene class]);
OCMStub([mockSharedScene delegate]).andReturn(realSceneDelegate);

id interceptor = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor scene:mockSharedScene continueUserActivity:userActivity]);

id interceptor2 = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor2 scene:mockSharedScene continueUserActivity:userActivity]);

[GULSceneDelegateSwizzler proxySceneDelegateIfNeeded:mockSharedScene];

[GULSceneDelegateSwizzler registerSceneDelegateInterceptor:interceptor];
[GULSceneDelegateSwizzler registerSceneDelegateInterceptor:interceptor2];

[realSceneDelegate scene:mockSharedScene continueUserActivity:userActivity];
OCMVerifyAll(interceptor);
OCMVerifyAll(interceptor2);

[mockSharedScene stopMocking];
mockSharedScene = nil;
}
}

- (void)testDonorMethodsInvokeOriginalSceneDelegateImplementations {
if (@available(iOS 13, tvOS 13, *)) {
GULImplementingTestSceneDelegate *realSceneDelegate =
[[GULImplementingTestSceneDelegate alloc] init];
id mockSharedScene = OCMClassMock([UIScene class]);
OCMStub([mockSharedScene delegate]).andReturn(realSceneDelegate);

id interceptor = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor scene:mockSharedScene openURLContexts:[NSSet set]]);

[GULSceneDelegateSwizzler proxySceneDelegateIfNeeded:mockSharedScene];
[GULSceneDelegateSwizzler registerSceneDelegateInterceptor:interceptor];

[realSceneDelegate scene:mockSharedScene openURLContexts:[NSSet set]];
OCMVerifyAll(interceptor);
XCTAssertTrue(realSceneDelegate.openURLContextsInvoked);

id mockSession = OCMClassMock([UISceneSession class]);
id mockConnectionOptions = OCMClassMock([UISceneConnectionOptions class]);
OCMExpect([interceptor scene:mockSharedScene
willConnectToSession:mockSession
options:mockConnectionOptions]);
[realSceneDelegate scene:mockSharedScene
willConnectToSession:mockSession
options:mockConnectionOptions];
OCMVerifyAll(interceptor);
XCTAssertTrue(realSceneDelegate.willConnectToSessionOptionsInvoked);

NSUserActivity *userActivity =
[[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
OCMExpect([interceptor scene:mockSharedScene continueUserActivity:userActivity]);
[realSceneDelegate scene:mockSharedScene continueUserActivity:userActivity];
OCMVerifyAll(interceptor);
XCTAssertTrue(realSceneDelegate.continueUserActivityInvoked);

[mockSharedScene stopMocking];
[mockSession stopMocking];
[mockConnectionOptions stopMocking];
mockSharedScene = nil;
mockSession = nil;
mockConnectionOptions = nil;
}
}

#if !TARGET_OS_MACCATALYST
// Test fails on Catalyst.

Expand Down
Loading