2
\$\begingroup\$

I have a service which connects to server using socket and stays connected, I used this enum to manage connection state:

enum ConnectionState {
  case none, connecting, failed(Error), connected, disconnected(Error), blocked
}

I wanted to have connectionState as property to know current connection state of service but in extension of service provider class.

Why in extension and not in class itself?
Because ServiceProvider class is not Swift class, its ObjC class and there is no way we I can declare ConnectionState swift enum with associated values in ObjC class.

So I used associative stored property in swift extension, but as ConnectionState has associated values I need to do little more.

First I created extension to enum to make dictionary convertible as:

extension ConnectionState {
  var dictPresentation: [String: Error?] {
    switch self {
    case .none: return ["none": nil]
    case .connecting: return ["connecting": nil]
    case .failed(let error): return ["failed": error]
    case .connected: return ["connected": nil]
    case .disconnected(let error): return ["disconnected": error]
    case .blocked: return ["blocked": nil]
    }
  }

  init(dict: [String: Error?]) {
    assert(!dict.isEmpty, "dict must contain one key")
    let key = dict.keys.first!
    let error: Error? = dict[key] ?? nil
    switch key {
    case "none": self = .none
    case "connecting": self = .connecting
    case "failed": self = .failed(error!)
    case "connected": self = .connected
    case "disconnected": self = .disconnected(error!)
    case "blocked": self = .blocked
    default:
        assert(false, "wrong key in dict")
        self = .none
    }
  }
}

Then used it in service provider class extension as:

private var ConnectionStateHolderKey: UInt8 = 7
extension ServiceProvider {
  private var _connectionStateHolder: NSDictionary {
    get {
        if let connectionStateDict = objc_getAssociatedObject(self, &ConnectionStateHolderKey) as? NSDictionary {
            return connectionStateDict
        }
        //return ["none": nil] gives error "nil is not compatible with expected dictionary value type 'Any'"
        //return ["none": nil as Any] gifs error "Nil is not compatible with 'Any' in coercion"

        let dict = NSDictionary()
        dict.setValue(nil, forKey: "none")
        return dict//default state = none
    }
    set {
        objc_setAssociatedObject(self, &ConnectionStateHolderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
  }

  var connectionState: ConnectionState {
    get {
        return ConnectionState(dict: _connectionStateHolder as! [String : Error?])
    }
    set {
      _connectionStateHolder = newValue.dictPresentation as NSDictionary
    }
  }
}

Is there anything that can be used to simplify this code and avoid dictionary conversion?
Any suggestions that can improve this code are welcomed!

\$\endgroup\$
4
\$\begingroup\$

First note that your var connectionState is computed property, not a stored property. It is computed using the get and set methods. Extensions cannot add stored properties to a type.

Your current implementation crashes if the connection state is retrieved without having been set before:

let provider = ServiceProvider()
print(provider.connectionState)

because at

let dict = NSDictionary()
dict.setValue(nil, forKey: "none")

dict is an immutable dictionary. Making it mutable does not solve the problem

let dict = NSMutableDictionary()
dict.setValue(nil, forKey: "none")

because the second statement has no effect and therefore the assertion

assert(!dict.isEmpty, "dict must contain one key")

fails.

You want to create a NSDictionary which – when bridged to [String: Error?] – becomes ["none": nil]. The difficulty here is that the value type of the Swift dictionary is an optional. But it is possible, using NSNull():

    get {
        if let connectionStateDict = objc_getAssociatedObject(self, &ConnectionStateHolderKey) as? NSDictionary {
            return connectionStateDict
        }
        return ["none": NSNull()]
    }

(compare SE-0140 Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull).

But: All these conversions between enum, Swift dictionary and NSDictionary are actually not needed, you can achieve the same much simpler.

As stated in Swift Blog: Objective-C id as Swift Any,

The Objective-C bridge in Swift 3 can in turn present any Swift value as an id-compatible object to Objective-C.

This means that you can pass an enum ConnectionState directly to objc_setAssociatedObject() and conditionally cast the return value from objc_getAssociatedObject() back to the enum. The Swift runtime automatically wraps the value into a class. (AnyObject not working in Xcode8 beta6? on Stack Overflow contains further information and links.)

So this is everything you need:

enum ConnectionState {
    case none, connecting, failed(Error), connected, disconnected(Error), blocked
}

private var ConnectionStateHolderKey: UInt8 = 7
extension ServiceProvider {

    var connectionState: ConnectionState {
        get {
            if let connectionState = objc_getAssociatedObject(self, &ConnectionStateHolderKey) as? ConnectionState {
                return connectionState
            }
            return .none
        }
        set {
            objc_setAssociatedObject(self, &ConnectionStateHolderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

More suggestions:

  • Use the nil-coalescing operator ?? to provide a default value in the getter method.
  • Restrict the scope of the associated objects key by making it a static property inside extension ServiceProvider.

Then the code would look like this:

extension ServiceProvider {

    private struct Keys {
        static var connectionState : UInt8 = 7
    } 

    var connectionState: ConnectionState {
        get {
            return (objc_getAssociatedObject(self, &ServiceProvider.Keys.connectionState) as? ConnectionState) ?? .none
        }
        set {
            objc_setAssociatedObject(self, &ServiceProvider.Keys.connectionState, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

Finally: Your enum has a case none for "no state". But the Swift way of representing "no value" is to use an optional:

enum ConnectionState {
    case connecting, failed(Error), connected, disconnected(Error), blocked
}

extension ServiceProvider {

    private struct Keys {
        static var connectionState : Int = 0
    } 

    var connectionState: ConnectionState? {
        get {
            return objc_getAssociatedObject(self, &ServiceProvider.Keys.connectionState) as? ConnectionState
        }
        set {
            objc_setAssociatedObject(self, &ServiceProvider.Keys.connectionState, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ I still wonder how .failed(Error) would be converted and stored if we directly save enum without converting to dictionary; testing this... \$\endgroup\$ – D4ttatraya Aug 21 '17 at 15:38
  • \$\begingroup\$ Can you please explain more on "Define the associated objects key locally"? I defined it private. \$\endgroup\$ – D4ttatraya Aug 21 '17 at 15:45
  • 1
    \$\begingroup\$ @D4ttatraya: Your private var ConnectionStateHolderKey is visible in the entire source file. My suggested ServiceProvider.Keys.connectionState is only visible inside the extension, i.e. the visibility is limited to the scope where the value is needed. \$\endgroup\$ – Martin R Aug 21 '17 at 17:42
1
\$\begingroup\$

My Suggestion: instead backporting Swift to objc better look forward to Swift

I would create a wrapping swift class/struct ServiceConnection witch has 2 properties:

  • serviceProvider
  • connectionState
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.