Method swizzling in Swift

Isuru

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.

Jasper Blues

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

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

TOP Ranking

  1. 1

    Failed to listen on localhost:8000 (reason: Cannot assign requested address)

  2. 2

    Loopback Error: connect ECONNREFUSED 127.0.0.1:3306 (MAMP)

  3. 3

    How to import an asset in swift using Bundle.main.path() in a react-native native module

  4. 4

    pump.io port in URL

  5. 5

    Compiler error CS0246 (type or namespace not found) on using Ninject in ASP.NET vNext

  6. 6

    BigQuery - concatenate ignoring NULL

  7. 7

    ngClass error (Can't bind ngClass since it isn't a known property of div) in Angular 11.0.3

  8. 8

    ggplotly no applicable method for 'plotly_build' applied to an object of class "NULL" if statements

  9. 9

    Spring Boot JPA PostgreSQL Web App - Internal Authentication Error

  10. 10

    How to remove the extra space from right in a webview?

  11. 11

    java.lang.NullPointerException: Cannot read the array length because "<local3>" is null

  12. 12

    Jquery different data trapped from direct mousedown event and simulation via $(this).trigger('mousedown');

  13. 13

    flutter: dropdown item programmatically unselect problem

  14. 14

    How to use merge windows unallocated space into Ubuntu using GParted?

  15. 15

    Change dd-mm-yyyy date format of dataframe date column to yyyy-mm-dd

  16. 16

    Nuget add packages gives access denied errors

  17. 17

    Svchost high CPU from Microsoft.BingWeather app errors

  18. 18

    Can't pre-populate phone number and message body in SMS link on iPhones when SMS app is not running in the background

  19. 19

    12.04.3--- Dconf Editor won't show com>canonical>unity option

  20. 20

    Any way to remove trailing whitespace *FOR EDITED* lines in Eclipse [for Java]?

  21. 21

    maven-jaxb2-plugin cannot generate classes due to two declarations cause a collision in ObjectFactory class

HotTag

Archive