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
通过UIView
和UIResponder
传递派生的,但是该类别有些奇怪:
尝试将 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
不适用于 NSObject
s 的 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 中的方法调配的主要内容,如果未能解决你的问题,请参考以下文章
使用方法调配一次在所有 UIViewController 实例上更改 iOS13 上的 modalPresentationStyle