Aaron Brager Aaron Brager - 5 months ago 29
Swift Question

Swift extension storage for protocols

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?

Answer

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.