12

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.

Community
  • 1
  • 1
Isuru
  • 30,617
  • 60
  • 187
  • 303
  • Why are you swizzling `UINavigationBar`?  You could do this easily with a custom subclass of `UINavigationBar`.  The docs for [`UINavigationController`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/) have information on using a custom `UINavigationBar` subclass, as does [@AlexPretzlav's answer](http://stackoverflow.com/questions/25651081/method-swizzling-in-swift#26641469). – Slipp D. Thompson Jul 02 '16 at 00:21

5 Answers5

19

Objective-C, which uses dynamic dispatch supports the following:

Class method swizzling:

The kind you're using above. All instances of a class will have their method replaced with the new implementation. The new implementation can optionally wrap the old.

Isa-pointer swizzling:

An instance of a class is set to a new run-time generated sub-class. This instance's methods will be replaced with a new method, which can optionally wrap the existing method.

Message forwarding:

A class acts as a proxy to another class, by performing some work, before forwarding the message to another handler.

These are all variations on the powerful intercept pattern, which many of Cocoa's best features rely on.

Enter Swift:

Swift continues the tradition of ARC, in that the compiler will do powerful optimizations on your behalf. It will attempt to inline your methods or use static dispatch or vtable dispatch. While faster, these all prevent method interception (in the absence of a virtual machine). However you can indicate to Swift that you'd like dynamic binding (and therefore method interception) by complying with the following:

  • By extending NSObject or using the @objc directive.
  • By adding the dynamic attribute to a function, eg public dynamic func foobar() -> AnyObject

In the example you provide above, these requirements are being met. Your class is derived transitively from NSObject via UIView and UIResponder, however there is something odd about that category:

  • The category is overriding the load method, which will normally be called once for a class. Doing this from a category probably isn't wise, and I'm guessing that while it might have worked before, in the case of Swift it doesn't.

Try instead to move the Swizzling code into your AppDelegate's did finish launching:

//Put this instead in AppDelegate
method_exchangeImplementations(
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits:"), 
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits_FixedHeightWhenStatusBarHidden:"))  
Cœur
  • 37,241
  • 25
  • 195
  • 267
Jasper Blues
  • 28,258
  • 22
  • 102
  • 185
  • Hi thanks for the detailed answer, Jasper. I moved the swizzling code to AppDelegate. I came across the issue of not being able to use `self` since I'm in AppDelegate. With the help of [this](http://stackoverflow.com/a/24048694/1077789) answer, I changed `self` to `object_getClass(UINavigationBar)` and ran the app but unfortunately no avail. – Isuru Sep 04 '14 at 07:06
  • Can you try UINavigationBar.classForCoder() ? If this doesn't work then it seems I'm incorrect and I will delete the answer. . . – Jasper Blues Sep 04 '14 at 07:08
  • `UINavigationBar.classForCoder()` causes a crash with the error **unexpectedly found nil while unwrapping an Optional value** at getter of `fixedHeightWhenStatusBarHidden`. – Isuru Sep 04 '14 at 07:14
  • Can you please explain what you mean by dynamic attribute? – Isuru Sep 04 '14 at 07:15
  • 1
    The crash is a good sign ;) Looks like its doing something now. Since Xcode6beta6 a function may still be inlined even if the class extends from Objective-C. . So we have to type 'dynamic' after func to tell Swift not to do this. Eg `public dynamic func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize` – Jasper Blues Sep 04 '14 at 07:18
  • 1
    Okay, I figured out the problem. Since the swizzling code is in the AppDelegate now, it doesn't reach the `viewDidLoad` in my view controller where I set `fixedHeightWhenStatusBarHidden` to `true`, therefore in the extension, the getter receives nil. I tried hardcoding true in the getter and now it works. Even without the `dynamic` keyword. :) – Isuru Sep 04 '14 at 07:35
  • 1
    Kinda sucks Swift has made swizzling problematic because the whole point of going the extension way was to make it reusable. – Isuru Sep 04 '14 at 07:37
  • @Isuru Glad its working now. Agree about Swizzling, but hopefully its a matter of learning the rules and it will still work fine. . In this case, the choice to override load in a category probably wasn't wise, even if it had worked before. Once that was fixed, the other problems were trivial. – Jasper Blues Sep 04 '14 at 07:41
  • I will keep a lookout on it. Thanks so much for the help. – Isuru Sep 04 '14 at 07:55
  • 2
    Note that the `dynamic` modifier implies `@objc`, so descending from `NSObject` or explicitly providing the `@objc` attribute is not necessary (however it _is_ important to know that the compiler is doing this, as the `dynamic` modifier is only supported for Objective-C declarations) – Jack Lawrence Sep 05 '14 at 01:37
  • @JackLawrence Good info. Feel free to edit the answer - I'm still learning this stuff myself. – Jasper Blues Sep 05 '14 at 02:01
2

The first problem I faced was Swift extension can't have stored properties.

First of all, categories in Objective-C also cannot have stored properties (called synthesized properties). The Objective-C code you linked to uses a computed property (it provides explicit getters and setters).

Anyway, getting back to your problem, you cannot assign to your property because you defined it as a read-only property. To define a read-write computed property, you would need to provide a getter and setter like this:

var fixedHeightWhenStatusBarHidden: Bool {
    get {
        return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue
    }
    set(newValue) {
        objc_setAssociatedObject(self, "FixedNavigationBarSize", NSNumber(bool: newValue), UInt(OBJC_ASSOCIATION_RETAIN))
    }
}
newacct
  • 119,665
  • 29
  • 163
  • 224
  • Thank you. However my code is not working. It seems the `load()` method is never fired. I found that the class you're subclassing from (in my case the extension) has to be derived from `NSObject` from [this](http://stackoverflow.com/a/24898473/1077789). Although `UINavigationBar` isn't directly derived from `NSObject`, it implements the `NSObjectProtocol`. So I'm confused why this isn't working. – Isuru Sep 04 '14 at 05:53
  • Also I saw your comment on the previous answer I linked about it working for any `@objc` class. I tried defining `@objc` in the extension but it isn't allowed. – Isuru Sep 04 '14 at 05:55
  • I managed to get it working _but_ with a few drawbacks. I updated the question with the progress. – Isuru Sep 04 '14 at 07:54
  • `load()` doesn't get called anymore as of swift 1.2. It actually won't compile – Remover Oct 07 '15 at 10:19
2

While this isn't a direct answer to your Swift question (seems like it may be a bug that load doesn't work for Swift extensions of NSObjects), however, I do have a solution to your original problem

I figured out that subclassing UINavigationBar and providing a similar implementation to the swizzled solution works in iOS 7 and 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)
        }
    }
}

Then update the class of the Navigation Bar in your storyboard, or use init(navigationBarClass: AnyClass!, toolbarClass: AnyClass!) when constructing your navigation controller to use the new class.

Alex Pretzlav
  • 15,505
  • 9
  • 57
  • 55
1

Swift doesn't implement the load() class method on extensions. Unlike ObjC, where the runtime calls +load for each class and category, in Swift the runtime only calls the load() class method defined on a class; Swift 1.1 ignores load() class methods defined in extensions. In ObjC, it is acceptable for each category to override +load so that each class/category can register for notifications or perform some other initialization (like swizzling) when that category/class loads.

Like ObjC, you should not override the initialize() class method in your extensions to perform swizzling. If you were to implement the initialize() class method in multiple extensions, only one implementation would be called. This is different than the +load method where the runtime calls all implementations when the app launches.

So, to initialize the state of an extension, you can either call the method from the app delegate when the app finishes launching, or from an ObjC stub when the runtime calls the +load methods. Since each extension could have a load method, they should be uniquely named. Assuming that you have SomeClass which is descended from NSObject, the code for the ObjC stub would look something like this:

In your SomeClass+Foo.swift file:

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

In your SomeClass+Foo.m file:

@implementation SomeClass(Foo)
    + (void)load {
        [self performSelector:@selector(loadFoo);
    }
@end
John Wallace
  • 161
  • 1
  • 2
1

Update for Swift 4.

initialize() is no longer exposed: Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift

So the way to do it now is to run your swizzle code via a public static method or via a singleton.

Method swizzling in swift 4

Christopher Rex
  • 392
  • 3
  • 10