简体   繁体   中英

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?

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 .

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

}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM