Swift 中的方法调配

Posted

技术标签:

【中文标题】Swift 中的方法调配【英文标题】:Method swizzling in Swift 【发布时间】:2014-09-03 18:19:15 【问题描述】:

UINavigationBar 有一个小问题,我正在努力克服。当您在视图控制器中使用 prefersStatusBarHidden() 方法隐藏状态栏时(我不想禁用整个应用程序的状态栏),导航栏会失去属于状态栏的 20pt 高度。基本上导航栏会缩小。

我尝试了不同的解决方法,但我发现它们中的每一个都有缺点。然后我遇到了这个category,它使用方法调配,在UINavigationBar 类中添加了一个名为fixedHeightWhenStatusBarHidden 的属性,从而解决了这个问题。我在 Objective-C 中对其进行了测试,并且可以正常工作。这是原始 Objective-C 代码的header 和implementation。

现在,由于我正在使用 Swift 开发我的应用程序,因此我尝试将其翻译为 Swift。

我遇到的第一个问题是 Swift 扩展不能存储属性。所以我不得不满足于计算属性来声明fixedHeightWhenStatusBarHidden 属性,使我能够设置值。但这引发了另一个问题。显然你不能为计算属性赋值。像这样。

self.navigationController?.navigationBar.fixedHeightWhenStatusBarHidden = true

我收到错误无法分配给此表达式的结果

下面是我的代码。它编译没有任何错误,但它不起作用。

import Foundation
import UIKit

extension UINavigationBar 

    var fixedHeightWhenStatusBarHidden: Bool 
        return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue
    

    func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize 

        if UIApplication.sharedApplication().statusBarHidden && fixedHeightWhenStatusBarHidden 
            let newSize = CGSizeMake(self.frame.size.width, 64)
            return newSize
         else 
            return sizeThatFits_FixedHeightWhenStatusBarHidden(size)
        

    
    /*
    func fixedHeightWhenStatusBarHidden() -> Bool 
        return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue
    
    */

    func setFixedHeightWhenStatusBarHidden(fixedHeightWhenStatusBarHidden: Bool) 
        objc_setAssociatedObject(self, "FixedNavigationBarSize", NSNumber(bool: fixedHeightWhenStatusBarHidden), UInt(OBJC_ASSOCIATION_RETAIN))
    

    override public class func load() 
        method_exchangeImplementations(class_getInstanceMethod(self, "sizeThatFits:"), class_getInstanceMethod(self, "sizeThatFits_FixedHeightWhenStatusBarHidden:"))
    


中间的fixedHeightWhenStatusBarHidden()方法被注释掉了,因为离开它会给我一个方法重新声明错误。

我之前没有在 Swift 或 Objective-C 中进行方法调配,所以我不确定下一步如何解决这个问题,甚至根本不可能。

有人可以解释一下吗?

谢谢。


更新 1:感谢 newacct,我的第一个关于属性的问题得到了解决。但是代码仍然不起作用。我发现执行没有到达扩展中的load()方法。从this 答案中的cmets 中,我了解到您的班级必须是NSObject 的后代,在我的情况下,UINavigationBar 不是直接来自NSObject,但它确实实现了NSObjectProtocol。所以我不确定为什么这仍然不起作用。另一种选择是添加 @objc 但 Swift 不允许您将其添加到扩展中。以下是更新后的代码。

问题仍未解决。

import Foundation
import UIKit

let FixedNavigationBarSize = "FixedNavigationBarSize";

extension UINavigationBar 

    var fixedHeightWhenStatusBarHidden: Bool 
        get 
            return objc_getAssociatedObject(self, FixedNavigationBarSize).boolValue
        
        set(newValue) 
            objc_setAssociatedObject(self, FixedNavigationBarSize, NSNumber(bool: newValue), UInt(OBJC_ASSOCIATION_RETAIN))
        
    

    func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize 

        if UIApplication.sharedApplication().statusBarHidden && fixedHeightWhenStatusBarHidden 
            let newSize = CGSizeMake(self.frame.size.width, 64)
            return newSize
         else 
            return sizeThatFits_FixedHeightWhenStatusBarHidden(size)
        

    

    override public class func load() 
        method_exchangeImplementations(class_getInstanceMethod(self, "sizeThatFits:"), class_getInstanceMethod(self, "sizeThatFits_FixedHeightWhenStatusBarHidden:"))
    



更新 2:Jasper 帮助我实现了所需的功能,但显然它有几个主要缺点。

由于扩展中的 load() 方法没有触发,正如 Jasper 建议的那样,我将以下代码块移动到应用代理的 didFinishLaunchingWithOptions 方法。

method_exchangeImplementations(class_getInstanceMethod(UINavigationBar.classForCoder(), "sizeThatFits:"), class_getInstanceMethod(UINavigationBar.classForCoder(), "sizeThatFits_FixedHeightWhenStatusBarHidden:"))

我不得不在 fixedHeightWhenStatusBarHidden 属性的 getter 中将返回值硬编码为“true”,因为现在 swizzling 代码在应用程序委托中执行,您无法从视图控制器设置值。这使得扩展在可重用性方面变得多余。

所以这个问题或多或少仍然是开放的。如果有人有改进的想法,请回答。

【问题讨论】:

你为什么要挥霍UINavigationBar?您可以使用UINavigationBar 的自定义子类轻松完成此操作。 UINavigationController 的文档包含有关使用自定义 UINavigationBar 子类的信息,@AlexPretzlav's answer 也是如此。 【参考方案1】:

Objective-C,它使用动态调度支持以下:

类方法混搭:

您在上面使用的那种。一个类的所有实例都将用新的实现替换它们的方法。新的实现可以选择包装旧的。

Isa-pointer swizzling:

一个类的实例被设置为一个新的运行时生成的子类。此实例的方法将被替换为新方法,该方法可以选择包装现有方法。

消息转发:

在将消息转发给另一个处理程序之前,一个类通过执行一些工作来充当另一个类的代理。

这些都是强大的拦截模式的变体,Cocoa 的许多最佳功能都依赖于这种模式。

输入斯威夫特:

Swift 延续了 ARC 的传统,编译器会为您进行强大的优化。它将尝试内联您的方法或使用静态调度或 vtable 调度。虽然速度更快,但这些都可以防止方法拦截(在没有虚拟机的情况下)。但是,您可以通过遵守以下内容向 Swift 表明您想要动态绑定(以及因此方法拦截):

通过扩展 NSObject 或使用 @objc 指令。 通过向函数添加动态属性,例如public dynamic func foobar() -> AnyObject

在您上面提供的示例中,这些要求都得到了满足。您的类NSObject 通过UIViewUIResponder 传递派生的,但是该类别有些奇怪:

该类别覆盖了加载方法,该方法通常会为一个类调用一次。从一个类别中这样做可能是不明智的,我猜虽然它以前可能有效,但在 Swift 的情况下却没有。

尝试将 Swizzling 代码移动到您的 AppDelegate 中并完成启动:

//Put this instead in AppDelegate
method_exchangeImplementations(
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits:"), 
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits_FixedHeightWhenStatusBarHidden:"))  

【讨论】:

您好,感谢 Jasper 的详细回答。我将 swizzling 代码移至 AppDelegate。我遇到了无法使用self 的问题,因为我在 AppDelegate 中。在this 答案的帮助下,我将self 更改为object_getClass(UINavigationBar) 并运行了该应用程序,但不幸的是无济于事。 崩溃是个好兆头 ;) 看起来它现在正在做某事。由于 Xcode6beta6,即使类从 Objective-C 扩展,函数仍可能被内联。 .所以我们必须在 func 之后输入 'dynamic' 来告诉 Swift 不要这样做。例如public dynamic func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize 好的,我找到了问题所在。由于 swizzling 代码现在在 AppDelegate 中,它没有到达我的视图控制器中的 viewDidLoad,我将 fixedHeightWhenStatusBarHidden 设置为 true,因此在扩展中,getter 收到 nil。我尝试在 getter 中对 true 进行硬编码,现在它可以工作了。即使没有 dynamic 关键字。 :) 有点糟糕 Swift 让 swizzling 成为问题,因为采用扩展方式的全部意义在于使其可重用。 请注意 dynamic 修饰符隐含 @objc,因此从 NSObject 降序或显式提供 @objc 属性是不必要的(但它对于知道编译器正在这样做,因为 dynamic 修饰符仅支持 Objective-C 声明)【参考方案2】:

我遇到的第一个问题是 Swift 扩展无法存储 属性。

首先,Objective-C 中的类别也不能具有存储属性(称为合成属性)。您链接到的 Objective-C 代码使用计算属性(它提供显式的 getter 和 setter)。

无论如何,回到您的问题,您不能分配给您的属性,因为您将其定义为只读属性。要定义 read-write 计算属性,您需要像这样提供 getter 和 setter:

var fixedHeightWhenStatusBarHidden: Bool 
    get 
        return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue
    
    set(newValue) 
        objc_setAssociatedObject(self, "FixedNavigationBarSize", NSNumber(bool: newValue), UInt(OBJC_ASSOCIATION_RETAIN))
    

【讨论】:

谢谢。但是我的代码不起作用。似乎 load() 方法从未被解雇过。我发现你要继承的类(在我的例子中是扩展)必须从NSObject 派生自this。虽然UINavigationBar 不是直接派生自NSObject,但它实现了NSObjectProtocol。所以我很困惑为什么这不起作用。 我还看到了您对我链接的上一个答案的评论,该答案适用于任何@objc 类。我尝试在扩展中定义@objc,但这是不允许的。 我设法让它工作但是有一些缺点。我用进度更新了问题。 load() 从 swift 1.2 开始不再被调用。它实际上不会编译【参考方案3】:

虽然这不是对您的 Swift 问题的直接回答(似乎这可能是一个错误,load 不适用于 NSObjects 的 Swift 扩展),但是,我做 em> 有一个解决你原来的问题的方法

我发现继承 UINavigationBar 并为 swizzled 解决方案提供类似的实现在 ios 7 和 8 中有效:

class MyNavigationBar: UINavigationBar 
    override func sizeThatFits(size: CGSize) -> CGSize 
        if UIApplication.sharedApplication().statusBarHidden 
            return CGSize(width: frame.size.width, height: 64)
         else 
            return super.sizeThatFits(size)
        
    

然后更新故事板中导航栏的类,或者在构造导航控制器时使用init(navigationBarClass: AnyClass!, toolbarClass: AnyClass!) 以使用新类。

【讨论】:

【参考方案4】:

Swift 没有在扩展上实现 load() 类方法。与 ObjC 不同,运行时为每个类和类别调用 +load,在 Swift 中,运行时只调用定义在类上的 load() 类方法; Swift 1.1 忽略了扩展中定义的 load() 类方法。在 ObjC 中,每个类别都可以覆盖 +load,以便每个类/类别可以在该类别/类加载时注册通知或执行其他一些初始化(如 swizzling)。

与 ObjC 一样,您不应在扩展中重写 initialize() 类方法来执行 swizzling。如果要在多个扩展中实现 initialize() 类方法,则只会调用一个实现。这与应用启动时运行时调用所有实现的 +load 方法不同。

因此,要初始化扩展程序的状态,您可以在应用程序完成启动时从应用程序委托调用该方法,或者在运行时调用 +load 方法时从 ObjC 存根调用该方法。由于每个扩展都可以有一个加载方法,因此它们应该被唯一命名。假设你有 SomeClass,它是 NSObject 的后代,ObjC 存根的代码看起来像这样:

在您的 SomeClass+Foo.swift 文件中:

extension SomeClass 
    class func loadFoo 
        ...do your class initialization here...
    

在您的 SomeClass+Foo.m 文件中:

@implementation SomeClass(Foo)
    + (void)load 
        [self performSelector:@selector(loadFoo);
    
@end

【讨论】:

【参考方案5】:

Swift 4 更新。

initialize() 不再暴露:Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift

所以现在的方法是通过公共静态方法或单例运行您的 swizzle 代码。

Method swizzling in swift 4

【讨论】:

以上是关于Swift 中的方法调配的主要内容,如果未能解决你的问题,请参考以下文章

Project: 如何调配项目中的资源?

UIView 方法 swizzling swift 3

无法调配类方法

使用方法调配一次在所有 UIViewController 实例上更改 iOS13 上的 modalPresentationStyle

调配一个没有实例的类

Swift编程语言中的方法引用(基于2.2版本)