Search code examples
iosobjective-ciphonegoogle-maps-sdk-iosobjective-c-runtime

how to override/swizzle a method of a private class in runtime objective-c?


To give a bit of context of why I'm asking this: basically I would like to change the location of the myLocationButton of the google map on iOS. So I first fetch the actual button like so:

@implementation GMSMapView (UIChanges)

- (UIButton *)myLocationButton
{
    UIButton *myLocationButton;
    for (myLocationButton in [settingView subviews])
    {
        if ([myLocationButton isMemberOfClass:[UIButton class]])
            break;
    }
    return myLocationButton;
}

Then I try to change it's position in the screen using NSLayoutConstraints (directly changing values of the frame property of the button does nothing with google maps SDK 1.8+):

UIButton *myLocationButton = [mapView_ myLocationButton];
[myLocationButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[myLocationButton constraintRightEqualTo:[myLocationButton superview] constant:0];
[myLocationButton constraintTopEqualTo:[myLocationButton superview] constant:50];

where constraintRightEqualTo is defined in a category as:

- (void)constraintRightEqualTo:(UIView *)toView constant:(CGFloat)constant
{
    NSLayoutConstraint *cn = [NSLayoutConstraint constraintWithItem:self
                                                          attribute:NSLayoutAttributeRight

                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:toView
                                                          attribute:NSLayoutAttributeRight
                                                         multiplier:1 constant:constant];

    [toView addConstraint:cn];
}

so far so good? ok.

Now this works just fine in iOS8.. however when I run this in iOS 7 it crashes with this famous error:

-[TPMURequestStatusNotificationManager makeActionButtonResponsive]:810 - makeActionButtonResponsive 2014-10-08 16:03:20.775 SmartTaxi[13009:60b] * Assertion failure in -[GMSUISettingsView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794 2014-10-08 16:03:20.779 SmartTaxi[13009:60b] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. GMSUISettingsView's implementation of -layoutSubviews needs to call super.'

the problem is that GMSUISettingsView is not calling [super layoutSubviews]..

I've seen this kind of error before.. the thing is that it happens with public classes such as UITableViewCell, as opposed to this private one GMSUISettingsView which is hidden in the guts of the google maps SDK for iOS. Had it been public.. i could have easily swizzled the method layoutsubviews inside of it using an approach similar to this answer. But it's not a public method. How can I in runtime change the definition of it's layoutsubviews to get around this problem? (suggestions to accomplish the same goal using simpler methods are also welcome)


UPDATE

so based on feedback + a bit more research I did the following:

@interface AttackerClass : UIView @end
@implementation AttackerClass

- (void)_autolayout_replacementLayoutSubviews
{
    struct objc_super superTarget;
    superTarget.receiver = self;
    superTarget.super_class = class_getSuperclass(object_getClass(self));

    objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
    NSLog(@":: calling send super")
    // PROBLEM: recursive call.. how do I call the *original* 
    // GMSUISettingsView implementation of layoutSubivews here?
    // replacing this with _autolayout_replacementLayoutSubviews will
    // throw an error b/c GMSUISettingsView doesn't have that method defined
    objc_msgSend(self, @selector(layoutSubviews)); 
    objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
}            
@end


Method attackerMethod = class_getInstanceMethod([AttackerClass class], @selector(_autolayout_replacementLayoutSubviews));
Method victimMethod = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews));

method_exchangeImplementations(victimMethod, attackerMethod);

The problem with this approach is that anytime GMSUISettingsView calls layoutSubviews.. it is in effect calling _autolayout_replacementLayoutSubviews.. which then recursively calls GMSUISettingsView layoutsubviews.. so my app goes into an infinite recursive loop. this answer solved that problem by using a category.. but I can't in this case b/c GMSUISettingsView is a private class..

another way of asking the same question: how can i retain a reference to the unaltered version of GMSUISettingsView's layoutSubviews and use it in _autolayout_replacementLayoutSubviews so that I don't fall into this recursive call problem.

ideas?


Solution

  • this did it.. i'm not sure if this counts as an actual answer since i just got around the problem by simply calling [self layoutIfNeeded] instead of [self layoutSubviews]

    void _autolayout_replacementLayoutSubviews(id self, SEL _cmd)
    {
        // calling super
        struct objc_super superTarget;
        superTarget.receiver = self;
        superTarget.super_class = class_getSuperclass(object_getClass(self));
        objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
    
        // to get around calling layoutSubviews and having
        // a recursive call
        [self layoutIfNeeded];
    
        objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
    }
    
    - (void)replaceGMSUISettingsViewImplementation
    {
        class_addMethod(NSClassFromString(@"GMSUISettingsView"), @selector(_autolayout_replacementLayoutSubviews), (IMP)_autolayout_replacementLayoutSubviews, "v@:");
    
        Method existing = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews));
        Method new = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(_autolayout_replacementLayoutSubviews));
    
        method_exchangeImplementations(existing, new);
    }