Swift中的方法交换

12

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

enter image description here enter image description here

我试过不同的解决方法,但发现每种方法都有缺点。然后我找到了这个类别,它使用方法交换,在UINavigationBar类中添加了一个名为fixedHeightWhenStatusBarHidden的属性,可以解决此问题。我在Objective-C中测试过,它可以工作。以下是原始Objective-C代码的头文件实现文件

现在,由于我正在使用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()方法并未执行。从 这个 答案的评论中,我了解到您的类需要是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”,因为现在交换代码在应用程序委托中执行,无法从视图控制器中设置值。这使得扩展在可重用性方面变得多余。

因此,问题或多或少仍然存在。如果有人有改进的想法,请回答。


你为什么要交换 UINavigationBar?使用自定义的 UINavigationBar 子类可以轻松实现此功能。UINavigationController 的文档中提供了有关使用自定义 UINavigationBar 子类的信息,@AlexPretzlav's answer 也提供了相关信息。 - Slipp D. Thompson
5个回答

19

Objective-C支持动态调度,包括以下内容:

类方法替换:

即你上面使用的方法。类的所有实例将用新实现替换它们的方法。新的实现可以选择性地包装旧实现。

Isa指针替换:

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

消息转发:

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

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

引入Swift:

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

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

在您提供的示例中,这些要求都被满足了。您的类是通过UIView和UIResponder间接派生自NSObject的,但该类别有一些奇怪之处:

  • 该类别重写了load方法,该方法通常仅对一个类调用一次。从类别中执行此操作可能不明智,在Swift的情况下可能不起作用。

相反,尝试将Swizzling代码移动到AppDelegate的didFinishLaunching:

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

谢谢你详细的回答,Jasper。我把交换代码移到了AppDelegate中。但是我遇到了一个问题,由于我在AppDelegate中,无法使用self。在这个答案的帮助下,我将self更改为object_getClass(UINavigationBar),然后运行了应用程序,但不幸的是没有效果。 - Isuru
1
崩溃是一个好的迹象 ;) 看起来现在正在做某些事情。自从Xcode6beta6以来,即使类扩展自Objective-C,函数仍然可能被内联。因此,我们必须在func后面键入“dynamic”,告诉Swift不要这样做。例如 public dynamic func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize - Jasper Blues
1
好的,我找到了问题所在。由于交换代码现在在AppDelegate中,它无法到达我的视图控制器中的viewDidLoad,在那里我将fixedHeightWhenStatusBarHidden设置为true,因此在扩展中,getter接收到nil。我尝试在getter中硬编码true,现在它可以工作了。即使没有使用dynamic关键字。 :) - Isuru
1
有点糟糕的是,Swift使得交换方法变得棘手了,因为采用扩展的方式的整个目的就是为了使其可重用。 - Isuru
2
请注意,dynamic修饰符意味着@objc,因此从NSObject继承或明确提供@objc属性是不必要的(但是重要的是要知道编译器正在执行此操作,因为dynamic修饰符仅支持Objective-C声明)。 - Jack Lawrence
显示剩余6条评论

2

我面临的第一个问题是Swift扩展不能有存储属性。

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

无论如何,回到您的问题,您无法将其分配给属性,因为您将其定义为只读属性。要定义可读写的计算属性,您需要提供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,来自这个。虽然UINavigationBar没有直接派生自NSObject,但它实现了NSObjectProtocol。所以我很困惑为什么这不起作用。 - Isuru
我也看到了你在之前链接的答案中关于它适用于任何@objc类的评论。我尝试在扩展中定义@objc,但是不被允许。 - Isuru
我设法让它工作了,_但是_有一些缺点。我已经更新了问题的进展。 - Isuru
自从Swift 1.2起,load()方法不再被调用。实际上,代码会因为这个问题而无法被编译通过。 - Remover

2

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

我发现在iOS 7和8中,子类化UINavigationBar并提供类似于交换解决方案的实现可以解决问题:

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!)来使用新类。


1

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

与ObjC类似,您不应该在扩展中重写initialize()类方法来执行swizzling。如果您在多个扩展中实现了initialize()类方法,则仅调用一个实现。这与+load方法不同,后者在应用程序启动时运行时调用所有实现。

因此,要初始化扩展的状态,您可以在应用程序完成启动时从应用程序委托中调用该方法,或者从ObjC存根中在运行时调用+load方法。由于每个扩展都可能具有load方法,因此它们应具有唯一的名称。假设您有一个从NSObject继承的SomeClass,则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

1

Swift 4更新。

initialize()不再公开: 方法'initialize()'定义了Objective-C类方法'initialize',Swift不允许

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

Swift 4中的方法swizzling


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接