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!
2 Answers 2
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)
}
}
}
-
\$\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– D4ttatraya2017年08月21日 15:38:05 +00:00Commented Aug 21, 2017 at 15:38 -
\$\begingroup\$ Can you please explain more on "Define the associated objects key locally"? I defined it
private
. \$\endgroup\$D4ttatraya– D4ttatraya2017年08月21日 15:45:43 +00:00Commented Aug 21, 2017 at 15:45 -
1\$\begingroup\$ @D4ttatraya: Your
private var ConnectionStateHolderKey
is visible in the entire source file. My suggestedServiceProvider.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– Martin R2017年08月21日 17:42:50 +00:00Commented Aug 21, 2017 at 17:42
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
Explore related questions
See similar questions with these tags.