Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift extension storage for protocols

Tags:

swift

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?

like image 956
Aaron Brager Avatar asked Jan 21 '16 20:01

Aaron Brager


2 Answers

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.

like image 130
Rob Napier Avatar answered Sep 20 '22 14:09

Rob Napier


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
        }
    }

}
like image 27
Aviel Gross Avatar answered Sep 18 '22 14:09

Aviel Gross