2
\$\begingroup\$

I have a situation in my client-server app where I have to send my network strength to server and also receive other clients' strength from server.

So I created enum as:

enum NetworkType: Int {
    case none, gprs, two_g, three_g, four_g, lte, wifi
}
extension NetworkType: Comparable {
    public static func < (a: NetworkType, b: NetworkType) -> Bool {
        return a.rawValue < b.rawValue
    }
}

I can use this as:

if NetworkType.two_g < NetworkType.three_g { 
    print("low network") 
}

But for sending to server, I have to convert it to String and vice a versa.
So I added one more extension as:

extension NetworkType {

    static var networkDict: [String: NetworkType] = ["gprs": .gprs,
                                                     "2g": .two_g,
                                                     "3g": .three_g,
                                                     "4g": .four_g,
                                                     "lte": .lte,
                                                     "wifi": .wifi];
    init(rawString: String) {
        let loweredStr = rawString.lowercased()
        self = NetworkType.networkDict[loweredStr] ?? .none
    }

    func rawString() -> String? {
        return NetworkType.networkDict.allKeys(forValue: self).first
    }
}

//Dictionary extension for 'key for value'
extension Dictionary where Value: Equatable {
    func allKeys(forValue val: Value) -> [Key] {
        return self.filter { $1 == val }.map { $0.0 }
    }
}

let network = NetworkType(rawString: "gprs") //gprs, while receiving from server
let rawString = NetworkType.gprs.rawString() //"gprs", while sending to server

It is working fine, no issues.
Jus curious to know if theres any better way to achieve this.

\$\endgroup\$
1
\$\begingroup\$

Is the .none case really needed? It serves as a "fallback" in init(rawString: String) if an unknown string is received from the network. But it is not listed in the mapping dictionary, i.e. it can not be converted back to a string.

My suggestion is to remove that case, and use a failable initializer for the conversion from a string:

init?(rawString: String) {
    let loweredStr = rawString.lowercased()
    guard let type = NetworkType.networkDict[loweredStr] else {
        return nil
    }
    self = type
}

Using a custom extension for the conversion to a string is not really necessary, the same can be achieved directly with

func rawString() -> String? {
    return NetworkType.networkDict.first(where: { $1 == self }).map { $0.0 }
}

But the real question is: Why does this return an optional? It would return nil if one of the enum cases is not covered in the dictionary. Let us assume that you add another case five_g in the enumeration and forget to update the dictionary. Now NetworkType.five_g.rawString() returns nil. What string do you send to the server?

One possible approach would be to force-unwrap the string, so that this programming error is detected early:

func rawString() -> String {
    return NetworkType.networkDict.first(where: { $1 == self }).map { $0.0 }!
}

But there is still a chance that the error remains undetected during development and the deployed application crashes later.


A safer (and actually shorter and easier) approach is to define a string-based enumeration, which implicitly defines the conversion to strings and back:

enum NetworkType: String {
    case gprs
    case two_g = "2g"
    case three_g = "3g"
    case four_g = "4g"
    case lte
    case wifi
}

To make the comparison work, an ordinal number is associated with each case:

extension NetworkType: Comparable {
    var ordinal: Int {
        switch self {
        case .gprs: return 1
        case .two_g: return 2
        case .three_g: return 3
        case .four_g: return 4
        case .lte: return 5
        case .wifi: return 6
        }
    }

    public static func < (a: NetworkType, b: NetworkType) -> Bool {
        return a.ordinal < b.ordinal
    }
}

Now the conversion is done using the built-in methods:

// String to NetworkType:
if let network = NetworkType(rawValue: "gprs") {
    // ..
} else {
    // unknown string from network
}

// NetworkType to String:
let rawString = NetworkType.gprs.rawValue

The conversion to a string cannot fail anymore. If another case is added, the compiler ensures that it is added to var ordinal as well.

\$\endgroup\$
  • \$\begingroup\$ if removed none how can handle no-connection? not for sending but for other uses like checking while trying to connect to server, etc \$\endgroup\$ – D4ttatraya Jul 27 '17 at 15:20
  • \$\begingroup\$ If you use an optional NetworkType can "no connection" can be represented as nil. – But that was just a suggestion, you can keep your none case if that fits your needs better. \$\endgroup\$ – Martin R Jul 27 '17 at 15:22
  • \$\begingroup\$ @D4ttatraya Thanks for the edit suggestion. But I intentionally did not assign the raw string if it is equal to the case name, because that is the default. \$\endgroup\$ – Martin R Jul 27 '17 at 16:12
  • \$\begingroup\$ Ohhkay then! Nice tip actually \$\endgroup\$ – D4ttatraya Jul 27 '17 at 16:13

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.