ProtocolProxy 0.1.2

ProtocolProxy 0.1.2

Joseph Newton 维护。



  • 作者:
  • Joe Newton

ProtocolProxy

Codacy Badge License MIT CocoaPods Compatible Carthage Compatible Platform Code Coverage

Carthage Cocoapods Swift Package SwiftLint XCFramework Xcode Project

灵活的代理,用于覆盖和观察协议方法/属性消息。

目的

该库的目的是提供一个轻量级类,作为需要实现一个或多个协议的对象的替身(例如,代理人、数据源等)。此外,此代理允许选择性地覆盖所采用的协议中的特定方法/属性,以及观察在调用之前和之后任何协议方法/属性。

安装

ProtocolProxy 可以通过 CocoaPodsCarthageSwift 包管理器 获得。

通过 CocoaPods 安装,只需将以下行添加到 Podfile 中

pod 'ProtocolProxy'

通过 Carthage 安装,只需将以下行添加到 Cartfile 中

github "SomeRandomiOSDev/ProtocolProxy"

通过 Swift 包管理器安装,请将以下行添加到 Package.swift 文件中的 dependencies

.package(url: "https://github.com/SomeRandomiOSDev/ProtocolProxy.git", from: "0.1.0")

用法

将此库导入源文件后(Objective-C:@import ProtocolProxy;,Swift:import ProtocolProxy),可以通过传递一个或多个Objective-C协议和一个可选对象(该对象实现这些协议)来实例化ProtocolProxy。在这种情况下,发送给代理的任何从采用的协议(s)中的方法都将相应地转发给实现者。此时,这个代理已准备好重写或观察采用的协议(s)的特定方法。

Objective-C

UIViewController *viewControler = ...;
id<UIAdaptivePresentationControllerDelegate> delegate = viewController.presentationController.delegate;

...

ProtocolProxy *proxy = [[ProtocolProxy alloc] initWithProtocol:@protocol(UIAdaptivePresentationControllerDelegate) implementer:delegate];

if (![delegate respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
    // delegate doesn't respond to the `-presentationControllerDidDismiss:` selector so
    // we set `respondsToSelectorsWithObservers` to YES to ensure that our observer
    // block gets called.
    proxy.respondsToSelectorsWithObservers = YES;
}

[proxy overrideSelector:@selector(presentationControllerShouldDismiss:) usingBlock:^BOOL (id self, UIPresentationController *presentationController) {
    BOOL shouldDismiss;
    
    ...
    
    return shouldDismiss;
}];

[proxy addObserverForSelector:@selector(presentationControllerDidDismiss:) beforeObservedSelector:NO usingBlock:^(id self, UIPresentationController *presentationController) {
    // `viewController` was interactively dismissed by the user; here we can update our state or UI if necessary.
}];

Swift

let viewControler: UIViewController = ...;
let delegate = viewController.presentationController?.delegate

...

let proxy = ProtocolProxy(protocol: UIAdaptivePresentationControllerDelegate.self, implementer: delegate)

if delegate?.responds(to: #selector(presentationControllerDidDismiss(_:))) != true {
    // delegate doesn't respond to the `presentationControllerDidDismiss(_:)` selector
    // so we set `respondsToSelectorsWithObservers` to true to ensure that our observer
    // closure gets called.
    proxy.respondsToSelectorsWithObservers = true
}

let overrideBlock: @convention(block) (AnyObject, UIPresentationController) -> Bool = { self, presentationController in 
    var shouldDismiss = false
    
    ...
    
    return shouldDismiss
}
let observerBlock: @convention(block) (AnyObject, UIPresentationController) -> Void = { self, presentationController in 
    // `viewController` was interactively dismissed by the user; here we can update our state or UI if necessary.
}

proxy.override(#selector(presentationControllerShouldDismiss(_:)), using: overrideBlock)
proxy.addObserver(for: #selector(presentationControllerDidDismiss(_:)), beforeObservedSelector: false, using: observerBlock)

在初始化过程中,ProtocolProxy构建一个它所属的协议列表,从传递给初始化器的协议(s)开始,以及它们采用的任何协议。此搜索是递归执行的,因此像以下这样的层次化协议结构

@protocol Protocol1 <NSObject>
...
@end

@protocol Protocol2 <Protocol1>
...
@end

@protocol Protocol3 <Protocol2>
...
@end

...

ProtocolProxy *proxy = [[ProtocolProxy alloc] initWithProtocol:@protocol(Protocol3) implementer:...];

将相应地遍历并转换为以下协议列表

@[@protocol(Protocol3), @protocol(Protocol2), @protocol(Protocol1), @protocol(NSObject)]

初始化后,ProtocolProxy对象可以安全地转换为采用这些协议之一的对象

Objective-C

... = (id<Protocol1>)proxy; // safe
... = (id<Protocol2>)proxy; // safe
... = (id<Protocol3>)proxy; // safe
... = (id<NSObject>)proxy; // safe
... = (id<NSCopying>)proxy; // UNSAFE: -copyWithZone: does not belong to any of the adopted protocols so attempting to call it will throw an exception

Swift

... = proxy as! Protocol1 // safe
... = proxy as? Protocol1 // produces a nonnil value

... = proxy as! Protocol2 // safe
... = proxy as? Protocol2 // produces a nonnil value

... = proxy as! Protocol3 // safe
... = proxy as? Protocol3 // produces a nonnil value

... = proxy as! NSObjectProtocol // safe
... = proxy as? NSObjectProtocol // produces a nonnil value

... = proxy as! NSCopying // UNSAFE: This cast will fail and cause a crash
... = proxy as? NSCopying // produces a nil value

当重写选择器时,有三个方法可用

- (BOOL)overrideSelector:(SEL)selector withTarget:(id)target;
- (BOOL)overrideSelector:(SEL)selector withTarget:(id)target targetSelector:(SEL __nullable)targetSelector;

- (BOOL)overrideSelector:(SEL)selector usingBlock:(id)block;

前两种方法通过将一个对象(弱保留)注册为目标来重写给定的选择器。该对象应实现协议中重写的方法。如果该对象已针对不同目的实现协议方法,则第二个重写方法可用于为目标对象提供一个不同命名的选择器。这个不同命名的期望方法将有与被重写的相同签名。对于这两种情况,beforeObservedSelector参数决定了观察者在观察选择器之前是否被调用(YES)或在其之后被调用(NO)。

第三种方法用于通过注册一个将被调用的块来替换给定的选择器。块应具有与被重写的方法相同的签名,除了隐藏的_cmd参数:method_return_type (^)(id self, method_args...)。如果被重写的方法除了隐藏的self_cmd参数外没有其他参数,或者块中不需要任何方法参数,则可以安全地将一个签名为method_return_type (^)(void)的块传递。有关与Swift闭包一起使用此方法的限制,请参阅限制


当观察选择器时,有三个方法可用

- (BOOL)addObserver:(id)observer forSelector:(SEL)selector beforeObservedSelector:(BOOL)before;
- (BOOL)addObserver:(id)observer forSelector:(SEL)selector beforeObservedSelector:(BOOL)before observerSelector:(SEL __nullable)observerSelector;

- (BOOL)addObserverForSelector:(SEL)selector beforeObservedSelector:(BOOL)before usingBlock:(id)block;

前两种方法通过将一个对象(弱保留)注册为接收观察选择器消息的对象来添加观察者。该对象应实现所观察协议中的精确方法。如果该对象已针对不同目的实现协议方法,则第二个观察者方法可用于为目标对象提供一个不同命名的选择器来调用。这个不同命名的期望方法将有与被观察的方法相同的签名。对于这两种情况,beforeObservedSelector参数决定了观察者在观察选择器之前是否被调用(YES)或在其之后被调用(NO)。

第三种方法用于通过注册一个块来观察给定的选择器。块应具有与被观察的方法相同的签名,除了隐藏的_cmd参数和返回类型为voidvoid (^)(id self, method_args...)。如果被观察的方法除了隐藏的self_cmd参数外没有其他参数,或者块中不需要任何方法参数,则可以安全地将一个签名为void (^)(void)的块传递。有关与Swift闭包一起使用此方法的限制,请参阅限制

观察者返回的任何值都将被忽略。此外,由于观察者不应中断它们所观察的代码的正常流程,来自观察者的任何异常都将被捕获并忽略。无论选择器是否引发了异常,都将调用注册为在观察选择器之后调用的任何观察者。


ProtocolProxy的-conformsToProtocol:方法会返回YES,如果adoptedProtocols属性包含任何协议,并且其-respondsToSelector:方法会返回YES,对于任何由任何采纳的协议声明的选择器或属性访问器,这些选择器是协议的必需选择器或者implementer响应的可选选择器。

注意事项

ProtocolProxy声明了一个名为respondsToSelectorsWithObservers的公共属性,该属性控制ProtocolProxy是否会从-respondsToSelector:返回YES对于implementer不响应且代理有观察者的可选方法。该属性的目的是对于implementer实际上不响应某个特定的方法,但代码设置的方式是观察者期望该方法被调用。在这种情况下,将respondsToSelectorsWithObservers属性设置为YES。当与该代理一起工作的代码到达预期调用被观察方法的点时,由于该方法可选,它首先调用-respondsToSelector:以确认代理对该方法做出响应。由于代理现在声明它响应该方法,代码应该调用该方法,这会触发观察。

尽管这个属性很有用,但应该谨慎使用,因为某些代码可能会根据代理是否响应特定的选择器进行逻辑决策,这可能会导致意外的副作用。

考虑这样一个场景,一个ProtocolProxy对象被实例化以遵循UIAdaptivePresentationControllerDelegate并将其设置为UIPresentationController的委托。此外,我们为协议的-[UIAdaptivePresentationControllerDelegate adaptivePresentationStyleForPresentationController:traitCollection:]方法设置了观察者,并且代理的实现者仅实现了-[UIAdaptivePresentationControllerDelegate adaptivePresentationStyleForPresentationController:]方法。当与呈现控制器相关联的视图控制器呈现时,控制器会首先检查其委托(ProtocolProxy)是否响应-[UIAdaptivePresentationControllerDelegate adaptivePresentationStyleForPresentationController:traitCollection:]方法。

对于相同场景,但将respondsToSelectorsWithObservers属性设置为YES,代理会返回YES,这会导致呈现控制器调用-[UIAdaptivePresentationControllerDelegate adaptivePresentationStyleForPresentationController:traitCollection:]方法。如果代理为这个选择器提供了覆盖,则不会真正有问题,但是,如果没有覆盖,则该方法不会转发到任何地方,导致返回给呈现控制器的值全部为零(UIModalPresentationFullScreen),这可能与没有委托或委托不响应这些方法的默认值大不相同。

如果使用该属性,建议仅将其用于那些既没有返回值也没有通过指针参数返回值的那些方法。在某些场景中,可以通过有条件地覆盖方法、运行观察者代码,然后将方法转发给implementer(如果它响应该方法)来避免使用该属性。

UIViewController *viewControler = ...;
id<UIAdaptivePresentationControllerDelegate> delegate = viewController.presentationController.delegate;

ProtocolProxy *proxy = [[ProtocolProxy alloc] initWithProtocol:@protocol(UIAdaptivePresentationControllerDelegate) implementer:delegate];

... 

[proxy overrideSelector:@selector(adaptivePresentationStyleForPresentationController:traitCollection:) usingBlock:^(id self, UIPresentationController *presentationController, UITraitCollection *traitCollection) {
    // observer code
    
    if ([delegate respondsToSelector:@selector(adaptivePresentationStyleForPresentationController:traitCollection:)]) {
        return [delegate adaptivePresentationStyleForPresentationController:presentationController traitCollection:traitCollection];
    } else {
        return <Default UIModalPresentationStyle>; 
    }
}];

OR

void (^observerBlock)(id, UIPresentationController *, UITraitCollection *) = ^(id self, UIPresentationController *presentationController, UITraitCollection *traitCollection) {
    // observer code
};

if ([delegate respondsToSelector:@selector(adaptivePresentationStyleForPresentationController:traitCollection:)]) {
    [proxy addObserverForSelector:@selector(adaptivePresentationStyleForPresentationController:traitCollection:) usingBlock:observerBlock];
} else {
    [proxy overrideSelector:@selector(adaptivePresentationStyleForPresentationController:traitCollection:) usingBlock:^(id self, UIPresentationController *presentationController, UITraitCollection *traitCollection) {
        observerBlock(self, presentationController, traitCollection);
        return <Default UIModalPresentationStyle>;
    }];
}

ProtocolProxy提供了三个属性,大多数情况下只是为了方便。

  • implementer获取传递给其初始化方法之一的对象。
  • adoptedProtocols 用于获取该代理遵从的协议列表。
  • respondsToSelectorsWithObservers 获取或设置一个标志,用来确定如何响应实现者未实现的可选方法。

由于 ProtocolProxy 应该作为协议的替身,因此有可能(尽管可能性很小)该对象可能初始化于一个声明具有与这些属性名称完全重叠的方法或属性的协议。在这种情况下,调用重叠的属性(们)将不再获取或设置上述值。相反,调用将遵循正常的转发常规,不管 implementer 是否为 null,或者重叠的方法/属性是否可选且未由 implementer 实现。

在这种情况下,仍然可以通过使用以下属性名称的 object_getIvar/object_setIvarivar_getOffset Objective-C 运行时函数来访问这些属性:_protocolProxyImplementer_protocolProxyAdoptedProtocols_protocolProxyRespondsToSelectorsWithObservers

// Sets a value to `ProtocolProxy`'s `respondsToSelectorsWithObservers` property where the proxy adopts a protocol that has a `@property BOOL respondsToSelectorsWithObservers` requirement

ProtocolProxy *proxy = ...
Ivar ivar = class_getInstanceVariable(proxy.class, "_protocolProxyRespondsToSelectorsWithObservers");
 
object_setIvar(proxy, ivar, @YES);

// OR

ptrdiff_t offset = ivar_getOffset(ivar);
*(BOOL *)((uint8_t *)(__bridge void *)proxy + offset) = YES;

-overrideSelector:usingBlock:-addObserverForSelector:beforeObservedSelector:usingBlock: 方法的第一个参数(如果有)是块(block)的 self 参数,对于常规方法,它将对应于接收消息的对象。按照惯例,这应该是 implementer;然而,由于线程考虑,这实际上是一个继承自 NSProxy 的临时替身对象。此对象除了填充一个必需的参数槽位外没有其他价值。

限制

该库的主要限制是与 Swift 关闭(closures)的互操作性。遗憾的是,由于编译器差异,Swift 关闭不能直接与 -overrideSelector:usingBlock:-addObserverForSelector:beforeObservedSelector:usingBlock: 方法兼容,但是,使用 @convention(block) 属性声明的 Swift 关闭可以这样做

let proxy: ProtocolProxy = ...
let observerBlock: @convention(block) () -> Void = {
    // Do some stuff here...
}

proxy.addObserver(for: #selector(foobar), beforeObservedSelector: true, using: observerBlock)

目前无法内联 @convention(block) 属性,因此必须在调用方法内声明闭包以进行兼容。因此,必须创建具有显式类型的局部变量,这样才能与这些方法兼容。如果忘记了这个属性,那么尝试注册覆盖(override)或观察者(observer)时这两个方法都将返回 false

贡献

无论是提交功能请求、报告错误还是自己编写代码,对这该库的所有贡献都受欢迎!请参阅 CONTRIBUTING 获取有关如何贡献的更多信息。

作者

Joe Newton, [email protected]

许可证

ProtocolProxy 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。