iOS 14 Beta: Change of behavior for class_addMethod expected?

Hi all,
I incurred into a change of behavior that is not documented, so I am not sure if it is an issue or it is expected.

In particular, calling class_addMethod started to return false for some UIKit related classes on iOS 14 Beta while on iOS <13.5 was returning true.

A tests that shows the change in behavior, that succeed on iOS 13.5 and fails on iOS 14 Beta is the following:

Code Block language
@import ObjectiveC;
@import UIKit;
@import XCTest;
static BOOL class_addMethodSuccedeed;
static BOOL UINavigationBarDidMoveToWindowCalled;
@interface TestCrashTests : XCTestCase
@end
@implementation TestCrashTests
- (void)testClassAddMethod {
XCTAssertTrue(class_addMethodSuccedeed);
[[[UINavigationBar alloc] initWithFrame:CGRectZero] didMoveToWindow];
XCTAssertTrue(UINavigationBarDidMoveToWindowCalled);
}
+ (void)swizzle:(Class)class methodName:(NSString*)methodName
{
SEL originalMethod = NSSelectorFromString(methodName);
SEL newMethod = NSSelectorFromString([NSString stringWithFormat:@"%@%@", @"override_", methodName]);
[self swizzle:class from:originalMethod to:newMethod];
}
+ (void)swizzle:(Class)class from:(SEL)original to:(SEL)new
{
Method originalMethod = class_getInstanceMethod(class, original);
Method newMethod = class_getInstanceMethod(class, new);
if (class_addMethod(class, original, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
class_replaceMethod(class, new, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
class_addMethodSuccedeed = YES;
} else {
method_exchangeImplementations(originalMethod, newMethod);
}
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzle:[UINavigationBar class] methodName:@"didMoveToWindow"];
[self swizzle:[UIView class] methodName:@"didMoveToWindow"];
});
}
@end
@implementation UIView (Swizzle)
- (void)override_didMoveToWindow {
[self override_didMoveToWindow];
}
@end
@implementation UINavigationBar (Swizzle)
- (void)override_didMoveToWindow {
UINavigationBarDidMoveToWindowCalled = YES;
[self override_didMoveToWindow];
}
@end


This seems, now, to swizzle wrongly methods of a subclass and in fact create an infinite recursive loop.

Any ideas?

Accepted Reply

This is just because those classes have gained overrides of those methods. class_addMethod returns false if the class already contains a method with that selector, but it returns true if the method is merely inherited from a superclass. It's expected that framework classes may gain (or lose) methods in OS updates, so your code needs to be able to handle both ways.

That said, the code you posted here looks correct for both cases, and indeed when I try it out here it seems to work fine. If you're still having trouble and the above doesn't address it, let me know what I might try differently and I'll see if I can take another look.

Replies

This is just because those classes have gained overrides of those methods. class_addMethod returns false if the class already contains a method with that selector, but it returns true if the method is merely inherited from a superclass. It's expected that framework classes may gain (or lose) methods in OS updates, so your code needs to be able to handle both ways.

That said, the code you posted here looks correct for both cases, and indeed when I try it out here it seems to work fine. If you're still having trouble and the above doesn't address it, let me know what I might try differently and I'll see if I can take another look.
Your feedback request got routed to me and I was able to replicate the problem using your attached project. Thanks very much for including that! I don't know why I couldn't replicate it before, but I must have been doing something wrong.

Just to make sure you see it, or in case anyone else comes across this thread and wants to know what's going on, here's the explanation of the problem.

The infinite loop is caused by the fact that both replacement methods have the same name. In this code:
Code Block
- (void)override_didMoveToWindow {
[self override_didMoveToWindow];
}

That call to self will call the most specific override_didMoveToWindow available. In this case, that's UINavigationBar's. That one calls back into UIView's, and we have infinite recursion.

The simpliest fix is to make sure each override has a unique name within the class hierarchy. In this particular example, naming the methods override_UIView_didMoveToWindow and override_UINavigationBar_didMoveToWindow, and modifying +swizzle:methodName: to look for those selectors, makes everything work as intended.

I also encountered a similar infinite loop on a swizzling method which replaces viewDidAppear to augment additional logging stuff, and it works well until I built the project with Xcode 12 beta 6 on an iOS14 simulator(iPhone 11 pro max).
My project is built with swift UI for demo.
Looks like it only causes problems on a project with SwiftUI on iOS14

Say that we have 1. an ContentView for our swiftUI project 2. an abc_viewDidAppear swizzled function for viewDidAppear which calls abc_viewDidAppear(should be the original viewDidAppear impl after swizzling) inside itself
Code Block objc
- (void)abc_viewDidAppear:(BOOL)animated {
if (meet some conditions) {
/* do some logging stuff */
    [self abc_viewDidAppear:animated];
/* do some logging stuff */
  } else {
/* do nothing but call the original viewDidAppear which was swizzled */
    [self abc_viewDidAppear:animated];
  }
}




When I tracked when my abc_viewDidAppear gets called:
Before iOS14
called by ContentView(actually something like 1234SwiftUI00UIHostingControllerV11Demo22ContentView) -> call the original viewDidAppear

iOS14
called by ContentView -> call the original viewDidAppear
called by an internal swift ui component - ViewList.View?(also a UIViewController) 1234SwiftUI00UIHostingControllerVS14ViewListView_ => this causes an infinite loop by calling abc_viewDidAppear -> abc_viewDidAppear is mine instead of the original viewDidAppear after swizzling