3

I have a protocol called NakedNavigationBar.

I also have an extension that extends all UIViewControllers that conform to NakedNavigationBar.

The problem is that in the extension I want to add default behaviour so that when the UIViewController initialises, we use method swizzling on the UIViewController.

Here is my protocol and extension:

import UIKit


protocol NakedNavigationBar
{

}

extension NakedNavigationBar where Self: UIViewController
{
    public override class func initialize()
    {
        struct Static
        {
            static var token: dispatch_once_t = 0
        }

        dispatch_once(&Static.token)
        {
            let originalSelector = #selector(viewWillAppear(_:))
            let swizzledSelector = #selector(nakedNavigationBar_viewWillAppear(_:))

            let originalMethod = class_getInstanceMethod(self, originalSelector)
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

            let didAddMethod = class_addMethod(self,
                                               originalSelector,
                                               method_getImplementation(swizzledMethod),
                                               method_getTypeEncoding(swizzledMethod))

            if didAddMethod
            {
                class_replaceMethod(self,
                                    swizzledSelector,
                                    method_getImplementation(originalMethod),
                                    method_getTypeEncoding(originalMethod))
            }
            else
            {
                method_exchangeImplementations(originalMethod, swizzledMethod)
            }
        }
    }


    //  MARK: - Swizzling Methods

    func nakedNavigationBar_viewWillAppear(animated: Bool)
    {
        self.nakedNavigationBar_viewWillAppear(animated)

        print("VWA Swizzling...")

        //  Hide the navigation bar
        _setNavigationBarVisible(isVisible: false)
    }


    //  MARK: -

    private func _setNavigationBarVisible(isVisible isVisible: Bool)
    {
        // (Changes background and shadow image)
    }
}

The errors I get when building are: errors

Essentially telling me that I cannot extend the protocol because it doesn’t have the UIViewController methods. But as I understand it, the where Self: UIViewController should make it so that this only works on a UIViewController, extending the view controller only when it conforms to NakedNavigationBar.

Originally the extension was extension UIViewController: NakedNavigationBar but this makes all of my UIViewControllers instantly conform to NakedNavigationBar rather than only the ones I choose.

Adam Carter
  • 4,741
  • 5
  • 42
  • 103
  • Even if that compiled your approach wouldn't work. Swizzling the methods would effect every `UIViewController`, not just the ones that conformed to your protocol. – dan May 13 '16 at 19:40
  • Good point :/ Can you think of a work around? – Adam Carter May 13 '16 at 20:03

1 Answers1

5

As of swift 3.0 as I understand that (semantically) what you ideally want to do is the following:

protocol NakedNavigationBar
{
    //your protocol definition
}

extension UIViewController where Self: NakedNavigationBar
{
    //method swizzling and all the goodies
}

Notice I've changed the order of the extension declaration, since what you want is not a default implementation of the protocol, but a default implementation of the UIViewController class WHEN the specific subclass conforms to NakedNavigationBar. The thing is, currently that syntax does not compile!!, we can't add Self requirements to class extensions =( (I feel your pain).

On the other hand, what you actually tried to do (protocol extension with Self requirement) compiles well, but the problem is that you can't override class methods inside a protocol extension, and by trying to swizzle you:

public override class func initialize()

What you CAN do when adding Self requirements to a protocol extension is to invoke any public API the class (in this case UIViewController) offers, so you can do things like:

protocol NakedNavigationBar
{
    func a() {
        //define cool stuff...
    }
}

extension NakedNavigationBar where Self: UIViewController
{

    func b() {
        //invoke stuff from NakedNavigationBar
        a()
        //or invoke stuff from UIViewController
        let viewLoaded = self.isViewLoaded
    }
    //but what we CAN'T do are class overrides
    override class func initialize() {} //---->compilation error 
}

I hope you find the explanation useful, and I'm sorry to be the bearer of bad news, but currently what you want to achieve does not really seem feasible to me in a Swifty type-safe manner.

But worry not!, life continues!. When I ran into the same use case and hit the same wall as you did, I managed to get things working, but it has a bit of a drawback: it's not the Swifty type-safe way of doing things (maybe we can improve on this in a later release of Swift if it allows for Self requirements on class extensions #fingersCrossed)

In a nutshell, the solution goes like this (using your original code as example):

import UIKit

protocol NakedNavigationBar
{

}

extension UIViewController //------->simple extension on UIViewController directly
{
    public override class func initialize()
    {
        //swizzling stuff switching viewWillAppear(_: Bool) with nakedNavigationBar_viewWillAppear(animated: Bool)
    }

    //  MARK: - Swizzling Methods

    func nakedNavigationBar_viewWillAppear(animated: Bool)
    {
        self.nakedNavigationBar_viewWillAppear(animated)

        print("VWA Swizzling...")

        //------->only run when self conforms to NakedNavigationBar
        if let protocolConformingSelf = self as? NakedNavigationBar { 
            print("UIViewControllers that conform to NakedNavigationBar, yay!!")
            //here it's safe to call UIViewController methods OR NakedNavigationBar on protocolConformingSelf instance
        }

    }


    //  anything else you want for UIViewController
}

Drawbacks:

  • Not "the Swift way" of doing things
  • The swizzling method will be invoked and take effect on ALL UIViewControllers, even though we validate for only those that conform to the NakedNavigationBar protocol to run the sensitive lines of code.

I very much hope it helps. If anyone found another (better) way of doing it, it would be great to hear it!

Robertibiris
  • 844
  • 8
  • 15