Protocol extensions cannot fulfill CLLocationManagerDelegate conformance?

1.2k views Asked by At

I'm trying to implement the CLLocationManagerDelegate protocol requirements via a protocol extension, but the location manager doesn't see it in the protocol extension and fails. However, it works with the same code when moved into the class.

Here's what I'm doing:

class ViewController: UIViewController, MyLocationProtocol {
    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
        locationManager.distanceFilter = 1000.0
        locationManager.delegate = self

        // Below crashes when implementation in protocol extension
        locationManager.requestLocation()
    }

}

protocol MyLocationProtocol: CLLocationManagerDelegate {
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
    func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
}

extension MyLocationProtocol /*where Self: UIViewControll*/ { // Tried using where clause but still no go :(

    // Not being triggered by CLLocationManagerDelegate! :(
    // Move to ViewController class and error goes away
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("MyLocationProtocol: locationManager: didUpdateLocations")
    }

    func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
        print("MyLocationProtocol: locationManager: didFailWithError")
    }

}

Notice in extension MyLocationProtocol I'm putting the didUpdateLocations and didFailWithError implementations there. They never get trigger and actually crash saying: 'Delegate must respond to locationManager:didUpdateLocations:'. If I move the same didUpdateLocations and didFailWithError code to ViewController, everything works as expected.

Is there something I'm missing on why this is not working through protocol extensions? The class is recognized as comforming to CLLocationManagerDelegate, otherwise it would fail at locationManager.delegate = self. Any ideas on how to make this work or is there a bug somewhere?

3

There are 3 answers

2
user3441734 On

protocol extension is pure Swift staff. The rules for dispatch for protocol extensions are:

IF the inferred type of a variable is the protocol

  • AND the method is defined in the original protocol THEN the runtime type’s implementation is called, irrespective of whether there is a default implementation in the extension.
  • AND the method is NOT defined in the original protocol THEN the default implementation is called.

IF the inferred type of the variable is the type THEN the type’s implementation is called.

Taking all this in account ...

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

import Foundation
import MapKit


class Lm: NSObject, CLLocationManagerDelegate {
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [AnyObject]) {
        print("\(locations)")
    }
}


class C:Lm {
    let lm = CLLocationManager()
    override init() {
        super.init()
        lm.delegate = self
        lm.startUpdatingLocation()
    }

}
let c = C()
/*
2016-02-22 08:41:56.506 Untitled Page 10[32000:11547708] ### Failed to load Addressbook class CNContactNameFormatter
[<+48.71408491,+21.20868516> +/- 65.00m (speed -1.00 mps / course -1.00) @ 22/02/16 08 h 41 min 57 s Central European Standard Time]
[<+48.71408491,+21.20868516> +/- 65.00m (speed -1.00 mps / course -1.00) @ 22/02/16 08 h 41 min 57 s Central European Standard Time]
[<+48.71415732,+21.20859246> +/- 65.00m (speed -1.00 mps / course -1.00) @ 22/02/16 08 h 41 min 57 s Central European Standard Time]
....
*/

other option is to do something like ...

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

import Foundation
import MapKit

class MyLocationManager: CLLocationManager, CLLocationManagerDelegate {
    override init() {
        super.init()
        delegate = self
    }
}
extension MyLocationManager {
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [AnyObject]) {
        print("\(locations)")
    }
}
class C {
    let lm = MyLocationManager()
    init() {
        lm.startUpdatingLocation()
    }
}

if all types are known at compile time and there is no 'optional' method in protocol, it works 'as expected'

protocol P { func foo() }
extension P { func foo() { print("p") } }
protocol P1: P {}
extension P1 { func foo() { print("p1") } }
class C: P {}
class C1: P1 {}
let c = C()
let c1 = C1()
let p:P = C()
let p1:P = C1()

c.foo()  // p
c1.foo() // p1
p.foo()  // p
p1.foo() // p1

for further reading see this and this

1
Ronald On

Protocol extension is one of the new things from swift 2.0. This allow you to define the behavior on protocol themselves. If you have implemented the method definition both in the confirming class and your protocol extension, at run time the method implementation in the confirming class will be called. But optional protocol is actually a byproduct of objective c. That means If you want to define optional protocol in swift you need to insert @objc attribute before the protocol declaration. When I experimented the protocol extension like you have explained, it works well when the protocol is not optional. But when the protocol is optional the application got crashed. CLLocationManagerDelegate delegate methods are declared as optional. I guess that may be the reason.

view controller 1

class ViewController: UIViewController,MyViewControllerProtocol {

var myViewController:NewViewController?

override func viewDidLoad() {
    super.viewDidLoad()

    let sb = UIStoryboard(name: "Main", bundle: nil)
    myViewController = sb.instantiateViewControllerWithIdentifier("newViewController") as? NewViewController
    myViewController?.delegate = self
    myViewController?.view.frame = self.view.bounds
    self.view.addSubview((myViewController?.view)!)
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
}

func newViewControllerLoaded(){
    print("protocol definition called inside class")
}

}

protocol MyViewControllerProtocol: NewViewControllerProtocol {
   func newViewControllerLoaded()
}

extension MyViewControllerProtocol{

func newViewControllerLoaded(){
   print("protocol definition called inside extension")
}

}

View controller 2

protocol NewViewControllerProtocol {

func newViewControllerLoaded()
}

class NewViewController: UIViewController {

var delegate: NewViewControllerProtocol?

override func viewDidLoad() {
    delegate?.newViewControllerLoaded()
}

}

output with protocol implementation in ViewController

protocol definition called inside class

output after removing the protocol implementation from ViewController

protocol definition called inside extension

One way to fix your problem is to apply the extension to your class then the protocol implementation will be inside your class. But this not a protocol extension.

extension ViewController /*where Self: UIViewControll*/ { // Tried using where clause but still no go :(

// Not being triggered by CLLocationManagerDelegate! :(
// Move to ViewController class and error goes away
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    print("MyLocationProtocol: locationManager: didUpdateLocations")
}

func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
    print("MyLocationProtocol: locationManager: didFailWithError")
}

}
0
Pradeep K On

I find this behaviour a bit strange. However for your current problem you can use this solution.

Basically the idea is to introduce a proxy object wherein you implement your logic for handling the location in a protocol and write a class that conforms to that protocol. Then in your ViewController you create an instance of the class (proxy) and also implement the CLLocationManagerDelegate methods and just pass the callback methods to the proxy object. This will help you separate out the location handling logic from the ViewController. You can still use the MyLocationManagerDelegateClass independently in the Watch app.

Hope this helps.

class ViewController: UIViewController, CLLocationManagerDelegate
{
    let locationManager = CLLocationManager()
    let proxyLocationManagerDelegate = MyLocationManagerDelegateClass()

    override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
        locationManager.distanceFilter = 1000.0
        // Below crashes when implementation in protocol extension
        locationManager.delegate = self
        locationManager.requestLocation()
    }

    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("ViewController: locationManager: didUpdateLocations")
        proxyLocationManagerDelegate.locationManager(manager, didUpdateLocations: locations)
    }

    func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
        print("ViewController: locationManager: didFailWithError")
        proxyLocationManagerDelegate.locationManager(manager, didFailWithError: error)
    }
}

class MyLocationManagerDelegateClass : NSObject, MyCLLocationManagerDelegate {

}

protocol MyCLLocationManagerDelegate : CLLocationManagerDelegate  {
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
    func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
}

extension MyCLLocationManagerDelegate /*where Self: UIViewControll*/ { // Tried using where clause but still no go :(

    // Not being triggered by CLLocationManagerDelegate! :(
    // Move to ViewController class and error goes away
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("MyLocationProtocol: locationManager: didUpdateLocations")
    }

    func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
        print("MyLocationProtocol: locationManager: didFailWithError")
    }

}