10

I make a protocol:

protocol SomeProtocol {
    func getData() -> String
}

I make a struct that conforms to it:

struct SomeStruct: SomeProtocol {
    func getData() -> String {
        return "Hello"
    }
}

Now I want every UIViewController to have a property called source, so I can do something like…

class MyViewController : UIViewController {
    override func viewDidLoad() {
        self.title = source.getData()
    }
}

To accomplish this, I create a protocol to define the property:

protocol SomeProtocolInjectable {
    var source: SomeProtocol! { get set }
}

Now I just need to extend the view controller with this property:

extension UIViewController: SomeProtocolInjectable {
    // ???
}

How can I hack together a stored property that will work with a protocol type?

What hasn't worked:

  • var source: SomeProtocol! obviously doesn't work because extensions don't have stored properties
  • I can't use Objective-C associated objects because a protocol isn't an object
  • I can't wrap it in a class (this does work for other value types, but not protocols)

Any other suggestions?

Community
  • 1
  • 1
Aaron Brager
  • 65,323
  • 19
  • 161
  • 287

3 Answers3

6

Any protocol object can be converted into a type-erased class. Build an AnySomeProtocol and store that.

private var sourceKey: UInt8 = 0

final class AnySomeProtocol: SomeProtocol {
    func getData() -> String { return _getData() }
    init(_ someProtocol: SomeProtocol) { _getData = someProtocol.getData }
    private let _getData: () -> String
}

extension UIViewController: SomeProtocolInjectable {
    var source: SomeProtocol! {
        get {
            return objc_getAssociatedObject(self, &sourceKey) as? SomeProtocol
        }
        set(newValue) {
            objc_setAssociatedObject(self, &sourceKey, AnySomeProtocol(newValue), .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

class MyViewController : UIViewController {
    override func viewDidLoad() {
        self.title = source.getData()
    }
}

The caller can only use this to access the protocol methods. You can't force it back into its original type with as, but you should avoid that anyway.

As a side note, I'd really recommend making source return SomeProtocol? rather than SomeProtocol!. There's nothing here that promises that source will be set. You don't even set it until viewDidLoad.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • If I'm understanding this correctly, this copies the state from whatever implemented `SomeProtocol`. But aside from that, wouldn't this effectively replace a `SomeStruct` with an `AnySomeProtocol`? `SomeStruct` will have different implementation. For example, one type might get data from a web service, but another from Core Data. – Aaron Brager Jan 21 '16 at 21:50
  • This doesn't copy the state. It forwards to the struct's implementation. _getData isn't a String. It's a function that returns a String. Even though the struct is inaccessible, the closure captures the struct (or captures whatever else you pass that implements this protocol). This is very much like AnySequence or AnyGenerator in the stdlib. See http://robnapier.net/erasure – Rob Napier Jan 21 '16 at 22:30
  • Thanks! Wonderful blog post too. – Aaron Brager Jan 21 '16 at 23:58
  • BTW, I'm using `SomeProtocol!` on purpose: I want the app to crash as soon as possible if I forget to set `source` somewhere. But generally I agree with you about avoiding implicitly unwrapped optionals. – Aaron Brager Jan 22 '16 at 00:01
  • @RobNapier This answer is a little old. Have you modified this approach at all? – Brody Robertson Sep 23 '19 at 03:29
  • 1
    @BrodyRobertson No. – Rob Napier Sep 23 '19 at 13:14
5

You can hack around with a static and the view controllers hash:

struct SomeProtocol {/*....*/}

struct DataProxy {
    var data: [Int: SomeProtocol]
}



protocol SomeProtocolInjectable {
    var source: SomeProtocol! { get set }
}

extension UIViewController: SomeProtocolInjectable {

    static var dataProxy = DataProxy(data: [:])

    var source: SomeProtocol! {
        get{
            return UIViewController.dataProxy.data[self.hashValue]
        }
        set{
            UIViewController.dataProxy.data[self.hashValue] = newValue
        }
    }

}
Aviel Gross
  • 9,770
  • 3
  • 52
  • 62
  • 1
    Note that this relies on undocumented behavior. It happens to work because UIViewController implements hash by returning its address pointer, but that isn't promised. Two view controllers are permitted to have the same hash (they just probably won't). Instead you could index on the pointer to the object: `Unmanaged.passUnretained(self).toOpaque()` (which is a valid dictionary key, but the pointer is completely unsafe to use). Note that this approach never frees the stored data, even after the view controller is destroyed. – Rob Napier Jan 21 '16 at 21:35
  • Clever! But I agree with @RobNapier, I'd like to avoid counting on the undocumented hashing behavior. +1 for creativity though. – Aaron Brager Jan 21 '16 at 23:59
-2

How about adding a default implementation for getData(), having a dummy struct implementation for the protocol, and use that as a default value for the source variable:

protocol SomeProtocol {
    func getData() -> String
}

extension SomeProtocol {
    func getData() -> String {
        return "Hello"
    }
}

protocol SomeProtocolInjectable {
    var source: SomeProtocol { get set }
}

struct DummyProtocolImplementation: SomeProtocol {

}

class MyViewController : UIViewController {
    var _source: SomeProtocol = DummyProtocolImplementation()

    override func viewDidLoad() {
        self.title = source.getData()
    }
}

extension MyViewController: SomeProtocolInjectable {
    var source: SomeProtocol { get { return _source } set { _source = newValue } }
}

I took the liberty to extend MyViewController instead of UIViewController, as the latter doesn't know about the protocol anyhow.

Cristik
  • 30,989
  • 25
  • 91
  • 127