Как переопределить /swizzle метод частного класса во время выполнения objective-c?

Чтобы дать немного контекста, почему я спрашиваю об этом: в основном я хотел бы изменить расположение myLocationButton карты google на iOS. Поэтому я сначала выбираю фактическую кнопку так:

@implementation GMSMapView (UIChanges)

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

Затем я пытаюсь изменить его положение на экране с помощью NSLayoutConstraints (прямое изменение значений свойства frame кнопки ничего не делает с картами Google SDK 1.8+):

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

где constraintRightEqualTo определяется в категории как:

- (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];
}

пока что так хорошо? ок.

Теперь это работает отлично в iOS8.. однако, когда я запускаю это в iOS 7, он падает с этой известной ошибкой:

- [TPMURequestStatusNotificationManager makeActionButtonResponsive]: 810 - makeActionButtonResponsive 2014-10-08 16: 03: 20.775 SmartTaxi [13009: 60b] * Ошибка утверждения в - [GMSUISettingsView layoutSublayersOfLayer:],/SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794 2014-10-08 16: 03: 20.779 SmartTaxi [13009: 60b] * Завершение приложения из-за неотображения исключение "NSInternalInconsistencyException", причина: "Автоматическая компоновка все еще требуется после выполнения -layoutSubviews. GMSUISettingsView-х реализация -layoutSubviews требует вызова super. '

проблема заключается в том, что GMSUISettingsView не вызывает [super layoutSubviews]..

Я видел такую ​​ошибку раньше... Дело в том, что это происходит с общедоступными классами, такими как UITableViewCell, в отличие от этого частного GMSUISettingsView, который скрыт в китах SDK Google Maps для IOS. Если бы это было публично.. я мог бы легко swizzled метод layoutsubviews внутри него, используя подход, похожий на этот ответ. Но это не публичный метод. Как я могу во время выполнения изменить его определение layoutsubviews, чтобы обойти эту проблему? (предложения по достижению одной и той же цели с использованием более простых методов также приветствуются)


UPDATE

поэтому на основе обратной связи + немного больше исследований я сделал следующее:

@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);

Проблема с этим подходом заключается в том, что в любое время GMSUISettingsView вызывает layoutsubviews.. он фактически вызывает _autolayout_replacementLayoutSubviews.., который затем рекурсивно вызывает GMSUISettingsView layoutsubviews.. поэтому мое приложение переходит в бесконечный рекурсивный цикл. этот ответ решил эту проблему с помощью категории.. но я не могу в этом случае b/c GMSUISettingsView является частным классом..

другой способ задать тот же вопрос: как я могу сохранить ссылку на неизмененную версию GMSUISettingsView layoutSubviews и использовать ее в _autolayout_replacementLayoutSubviews, чтобы я не попадал в эту проблему с рекурсивным вызовом.

идеи?

Ответы

Ответ 1

это сделало это. Я не уверен, что это считается фактическим ответом, так как я просто обошел проблему, просто набрав [self layoutIfNeeded] вместо [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, "[email protected]:");

    Method existing = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews));
    Method new = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(_autolayout_replacementLayoutSubviews));

    method_exchangeImplementations(existing, new);
}

Ответ 2

ОБНОВЛЕНИЕ

Если вы хотите сохранить оригинальную реализацию, вы можете получить ее и вызвать ее внутри blockImp. Затем после этого вызовите [[aClass суперкласс] layoutSubviews] или вызовите его указателем на функцию. Обратите внимание, что весь этот код нуждается в некоторой проверке ошибок и исключении исключений.

//get method encoding
const char * encoding = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews)));

void(^oldImplementation)(id aClass, SEL aSelector) = imp_getBlock(method_getImplementation(class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews))));

id blockImp = ^void(id aClass, SEL aSelector)
{
    //old imlpementation
    oldImplementation(aClass, aSelector);

    //calling [super layoutSubviews]
    IMP aImp = [[aClass superclass] methodForSelector:@selector(layoutSubviews)];
    id (*func)(id, SEL) = (void *)aImp;
    func([aClass superclass], @selector(layoutSubviews));
};
IMP newImp = imp_implementationWithBlock(blockImp);
class_replaceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews), newImp, encoding);

ОРИГИНАЛЬНЫЙ ОТВЕТ

Извините, но я не совсем понял, вы хотите полностью переопределить "layoutsubviews" или иметь оригинальную реализацию и изменить только ее часть. Если это первое, вы можете использовать class_replaceMethod вместо swizzling. Это будет иметь хиты производительности, хотя! Что-то вроде этого должно сделать трюк:

//get method encoding
const char * encoding = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews)));

id blockImp = ^void(id aClass, SEL aSelector)
{
    //what you want to happen in layout subviews
};
IMP newImp = imp_implementationWithBlock(blockImp);
class_replaceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews), newImp, encoding);

Я не тестировал это на устройстве/симуляторе, но что-то вроде этого должно сработать для вас. Я полагаю, вы знаете, что это не очень яркая идея манипулировать частными классами;)