11

There is a little problem with UINavigationBar which I'm trying to overcome. When you hide the status bar using prefersStatusBarHidden() method in a view controller (I don't want to disable the status bar for the entire app), the navigation bar loses 20pt of its height that belongs to the status bar. Basically the navigation bar shrinks.

enter image description here enter image description here

I was trying out different workarounds but I found that each one of them had drawbacks. Then I came across this category which using method swizzling, adds a property called fixedHeightWhenStatusBarHidden to the UINavigationBar class which solves this issue. I tested it in Objective-C and it works. Here are the header and the implementation of the original Objective-C code.

Now since I'm doing my app in Swift, I tried translating it to Swift.

The first problem I faced was Swift extension can't have stored properties. So I had to settle for computed property to declare the fixedHeightWhenStatusBarHidden property that enables me to set the value. But this sparks another problem. Apparently you can't assign values to computed properties. Like so.

self.navigationController?.navigationBar.fixedHeightWhenStatusBarHidden = true

I get the error Cannot assign to the result of this expression.

Anyway below is my code. It compiles without any errors but it doesn't work.

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() method in the middle is commented out because leaving it gives me a method redeclaration error.

I haven't done method swizzling in Swift or Objective-C before so I'm not sure of the next step to resolve this issue or even it's possible at all.

Can someone please shed some light on this?

Thank you.


UPDATE 1: Thanks to newacct, my first issue about properties was resolved. But the code doesn't work still. I found that the execution doesn't reach the load() method in the extension. From comments in this answer, I learned that either your class needs to be descendant of NSObject, which in my case, UINavigationBar isn't directly descended from NSObject but it does implement NSObjectProtocol. So I'm not sure why this still isn't working. The other option is adding @objc but Swift doesn't allow you to add it to extensions. Below is the updated code.

The issue is still open.

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:"))
    }

}

UPDATE 2: Jasper helped me to achieve the desired functionality but apparently it comes with a couple of major drawbacks.

Since the load() method in the extension wasn't firing, as Jasper suggested, I moved the following code block to app delegates' didFinishLaunchingWithOptions method.

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

And I had to hardcode the return value to 'true' in the getter of fixedHeightWhenStatusBarHidden property because now that the swizzling code executes in the app delegate, you can't set a value from a view controller. This makes the extension redundant when it comes to reusability.

So the question is still open more or less. If anyone has an idea to improve it, please do answer.

4

5 回答 5

18

使用动态调度的 Objective-C 支持以下内容:

类方法混搭:

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

Isa 指针调动:

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

消息转发:

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

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

输入斯威夫特:

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

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

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

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

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

//Put this instead in AppDelegate
method_exchangeImplementations(
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits:"), 
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits_FixedHeightWhenStatusBarHidden:"))  
于 2014-09-04T06:43:53.967 回答
2

虽然这不是对您的 Swift 问题的直接答案(似乎它可能是一个load不适用于NSObjects 的 Swift 扩展的错误),但是,我确实有解决您最初问题的方法

我发现继承 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!)在构建导航控制器时使用新类。

于 2014-10-29T22:03:57.733 回答
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))
    }
}
于 2014-09-03T20:34:42.160 回答
1

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

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

因此,要初始化扩展的状态,您可以在应用程序完成启动时从应用程序委托调用该方法,或者在运行时调用 +load 方法时从 ObjC 存根调用该方法。由于每个扩展都可以有一个加载方法,因此它们应该被唯一命名。假设您有从 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
于 2015-02-16T19:50:29.130 回答
1

Swift 4 的更新。

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

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

swift 4中的方法调配

于 2017-09-22T08:35:37.830 回答