4

I assume that the answer to this question will address issues with Objective-C protocols in general, but this is the first problem of this type that I've come across.

I expect for these methods to be used, when implementing UIPageViewControllerDataSourceWithConnections.

import UIKit

protocol UIPageViewControllerDataSourceWithConnections: UIPageViewControllerDataSource {
    var connectedViewControllers: [UIViewController] {get}
}

extension UIPageViewControllerDataSourceWithConnections {
    func pageViewController(pageViewController: UIPageViewController,
        viewControllerBeforeViewController viewController: UIViewController
    ) -> UIViewController? {return connectedViewController(
        current: viewController,
        adjustIndex: -
    )}

    func pageViewController(pageViewController: UIPageViewController,
        viewControllerAfterViewController viewController: UIViewController
    ) -> UIViewController? {return connectedViewController(
        current: viewController,
        adjustIndex: +
    )}

    private func connectedViewController(
        current viewController: UIViewController,
        adjustIndex: (Int, Int) -> Int
    ) -> UIViewController? {
        let requestedIndex = adjustIndex(connectedViewControllers.indexOf(viewController)!, 1)
        return connectedViewControllers.indices.contains(requestedIndex) ?
            connectedViewControllers[requestedIndex] : nil
    }

    func presentationCountForPageViewController(pageViewController: UIPageViewController)
    -> Int {return connectedViewControllers.count}

    func presentationIndexForPageViewController(pageViewController: UIPageViewController)
    -> Int {
        return connectedViewControllers.indexOf(pageViewController.viewControllers!.first!)!
    }
}

However, that won't compile. I have to implement this nonsense to make things work. Can you tell me why? Is a code-lighter solution available?

// connectedViewControllers is defined elsewhere in InstructionsPageViewController.
extension InstructionsPageViewController: UIPageViewControllerDataSourceWithConnections {

    // (self as UIPageViewControllerDataSourceWithConnections) doesn't work.
    // Workaround: use a different method name in the protocol

    func pageViewController(pageViewController: UIPageViewController,
        viewControllerBeforeViewController viewController: UIViewController
    ) -> UIViewController? {
        return pageViewController(pageViewController,
            viewControllerBeforeViewController: viewController
        )
    }

    func pageViewController(pageViewController: UIPageViewController,
        viewControllerAfterViewController viewController: UIViewController
    ) -> UIViewController? {
        return pageViewController(pageViewController,
            viewControllerAfterViewController: viewController
        )
    }


    // (self as UIPageViewControllerDataSourceWithConnections)
    // works for the optional methods.

    func presentationCountForPageViewController(pageViewController: UIPageViewController)
    -> Int {
        return (self as UIPageViewControllerDataSourceWithConnections)
            .presentationCountForPageViewController(pageViewController)
    }

    func presentationIndexForPageViewController(pageViewController: UIPageViewController)
    -> Int {
        return (self as UIPageViewControllerDataSourceWithConnections)
            .presentationIndexForPageViewController(pageViewController)
    }
}
  • My question is actually the question that can be read at the top of the page. It's specific, though as I said, the answer may apply to a larger set of questions. I believe I was clear about what does, and unexpectedly does not, compile. –  Jul 28 '15 at 00:44
  • I prefixed the "first part" with "when implementing". That means, "when putting a colon after a type name to say that it conforms to a protocol". The compiler will tell you that conformance is not happening, for UIPageViewControllerDataSource. Answering this question requires that someone knows that already, or finds out by copying my code into a project and working with it. –  Jul 28 '15 at 10:04

1 Answers1

6

When you have a problem like this, where you're wondering about the limits of the Swift language itself, it helps to reduce it to a simpler version of the problem.

First, let's ask: is it possible to extend a protocol-adopting protocol as a way of injecting default implementations of that protocol's requirements into an ultimate adopting class? Yes, it is; this code is legal:

protocol Speaker {
    func speak()
}
protocol DefaultSpeaker : Speaker {
}
extension DefaultSpeaker {
    func speak() {
        print("howdy")
    }
}
class Adopter : DefaultSpeaker {

}

Okay, so what else does your code do? Well, it also injects an additional requirement (the instance variable). Is that legal? Yes, it is. This code is legal too:

protocol Speaker {
    func speak()
}
protocol DefaultSpeaker : Speaker {
    var whatToSay : String {get}
}
extension DefaultSpeaker {
    func speak() {
        print(self.whatToSay)
    }
}
class Adopter : DefaultSpeaker {
    var whatToSay = "howdy"
}

So what is that Swift doesn't like? What haven't we done here, that your code does? It's the fact that the original protocol is @objc. If we change protocol Speaker to @objc protocol Speaker (and make all other necessary changes), the code stops compiling:

@objc protocol Speaker {
    func speak()
}
@objc protocol DefaultSpeaker : Speaker {
    var whatToSay : String {get}
}
extension DefaultSpeaker {
    func speak() {
        print(self.whatToSay)
    }
}
class Adopter : NSObject, DefaultSpeaker { // ERROR
    var whatToSay = "howdy"
}

I'm going to guess that this is because Objective-C knows nothing about protocol extensions. Since our implementation of the required protocol methods depends upon the protocol extension, we cannot adopt the protocol in a way that satisfies the compiler that the requirement has been satisfied from Objective-C's point of view. We have to implement the requirements right there in the class, where Objective-C can see our implementation (which is exactly what your solution does):

@objc protocol Speaker {
    func speak()
}
@objc protocol DefaultSpeaker : Speaker {
    var whatToSay : String {get}
}
extension DefaultSpeaker {
    func speak2() {
        print(self.whatToSay)
    }
}
class Adopter : NSObject, DefaultSpeaker {
    var whatToSay = "howdy"
    func speak() {
        self.speak2()
    }
}

So, I conclude that your solution is as good as it gets.

What you're doing is actually more like this, where we use an extension on the adopter class to inject the "hook" methods:

@objc protocol Speaker {
    func speak()
}
@objc protocol DefaultSpeaker : Speaker {
    var whatToSay : String {get}
}
extension DefaultSpeaker {
    func speak2() {
        print(self.whatToSay)
    }
}
class Adopter : NSObject {
}
extension Adopter : DefaultSpeaker {
    var whatToSay : String { return "howdy" }
    func speak() {
        self.speak2()
    }
}

That works because that last extension is something Objective-C can see: an extension on an Objective-C class is effectively a category, which Objective-C understands.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I filed a radar with a video tutorial. Although you can't use the I'm going with in Objetive-C directly, it compiles, and I think that's the clearest syntax. I'll either mark this as the answer, or create a new one, when I hear back from Apple. –  Jul 29 '15 at 13:46
  • 2
    I agree with your radar and was going to suggest that. You have a great use case, and it seems to me that whatever it is that exposes the Swift API to Objective-C could make more of an effort to tell Objective-C about the stuff that has entered the class by way of the protocol extension. However, I have a doubt that they can fix this, because of the odd dispatch rules involved here. We can always hope, though! – matt Jul 29 '15 at 14:13