Search code examples
swiftdictionarynulloption-typesubscript

Swift dictionary subscript with optional values


Dears,

this is my code testable in a playground:

import Foundation
import UIKit

enum Error : Swift.Error, LocalizedError {

    case failedTypeCastingUITableViewCell(name: String, type: Any.Type, reuseIdentifier: String?)

    var domain: String {
        return ""
    }

    var code: Int {
        return 0
    }

    var nserror : NSError {
        return NSError(domain: self.domain,
                       code: self.code,
                       userInfo: self.userInfo)
    }

//    var userInfo : [String: Any]? {
//        var userInfo : [String: Any] = [:]
//
//        switch self {
//        case .failedTypeCastingUITableViewCell(let name, let type, let reuseIdentifier):
//            userInfo = [
//                "param_name": name
//               ,"param_type": "\(type)"
//               ,"param_identifier": reuseIdentifier
//            ]
//            return userInfo
//        }
//    }

    var userInfo : [String: Any]? {
        var userInfo : [String: Any] = [:]

        switch self {
        case .failedTypeCastingUITableViewCell(let name, let type, let reuseIdentifier):
            userInfo = [
                "param_name": name
               ,"param_type": "\(type)"
            ]
            userInfo["param_identifier"] = reuseIdentifier
            return userInfo
        }
    }

}

print(Error.failedTypeCastingUITableViewCell(name: "", type: UITableViewCell.self, reuseIdentifier: nil).userInfo)

this is the result I get from print and is what I want to achieve with commented code:

Optional(["param_name": "", "param_type": "UITableViewCell"])

this is the result I get from commented code instead:

Optional(["param_identifier": nil, "param_type": "UITableViewCell", "param_name": ""])

I know It have to work this way, but my question is can I get rid of this in some way? ie. custom init? custom subscript?


Solution

  • Given the concrete example, this is certainly possible, and in a number of ways.

    One way is to use key/value pairs, and filter out what is nil:

    let kv = [
        ("param_name", name),
        ("param_type", "\(type)"),
        ("param_identifier", reuseIdentifier)
        ].filter{$1 != nil}.map{($0, $1!)} // <== this is your magic line
    
    userInfo = Dictionary(uniqueKeysWithValues: kv)
    

    Another way is with reduce:

    userInfo = [
        ("param_name", name),
        ("param_type", "\(type)"),
        ("param_identifier", reuseIdentifier)
        ]
        .reduce(into: [:]) { (d, kv) in d[kv.0] = kv.1 }
    

    For more readability, you could extract that to an extension:

    extension Dictionary {
        init<S>(uniqueKeysWithNonNilValues seq: S)
            where S : Sequence, S.Element == (Key, Value?) {
                self = seq.reduce(into: [:]) { (d, kv) in d[kv.0] = kv.1 }
        }
    }
    

    And then the calling code would be pretty nice IMO:

    userInfo = Dictionary(uniqueKeysWithNonNilValues: [
        ("param_name", name),
        ("param_type", "\(type)"),
        ("param_identifier", reuseIdentifier)
    ])
    

    But we could push it just a little closer to your proposed syntax, and maybe that's nicer because it feels like a Dictionary again:

    extension Dictionary {
        init(withNonNilValues seq: KeyValuePairs<Key, Value?>) {
            self = seq.reduce(into: [:]) { (d, kv) in d[kv.0] = kv.1 }
        }
    }
    
    userInfo = Dictionary(withNonNilValues: [
        "param_name": name,
        "param_type": "\(type)",
        "param_identifier": reuseIdentifier,
        ])