简体   繁体   中英

Swift: How to remove a null value from Dictionary?

I'm new in Swift and I have a problem with filtering NULL values from JSON file and setting it into Dictionary. I getting JSON response from the server with null values and it crashes my app.

Here is JSON response:

"FirstName": "Anvar",
"LastName": "Azizov",
"Website": null,
"About": null,

I will be very appreciated for help to deal with it.

UPD1: At this moment I decided to do it in a next way:

if let jsonResult = responseObject as? [String: AnyObject] {                    
    var jsonCleanDictionary = [String: AnyObject]()

    for (key, value) in enumerate(jsonResult) {
      if !(value.1 is NSNull) {
         jsonCleanDictionary[value.0] = value.1
      }
    }
}

Swift 5

Use compactMapValues :

dictionary.compactMapValues { $0 }

compactMapValues has been introduced in Swift 5. For more info see Swift proposal SE-0218 .

Example with dictionary

let json = [
    "FirstName": "Anvar",
    "LastName": "Azizov",
    "Website": nil,
    "About": nil,
]

let result = json.compactMapValues { $0 }
print(result) // ["FirstName": "Anvar", "LastName": "Azizov"]

Example including JSON parsing

let jsonText = """
  {
    "FirstName": "Anvar",
    "LastName": "Azizov",
    "Website": null,
    "About": null
  }
  """

let data = jsonText.data(using: .utf8)!
let json = try? JSONSerialization.jsonObject(with: data, options: [])
if let json = json as? [String: Any?] {
    let result = json.compactMapValues { $0 }
    print(result) // ["FirstName": "Anvar", "LastName": "Azizov"]
}

Swift 4

I would do it by combining filter with mapValues :

dictionary.filter { $0.value != nil }.mapValues { $0! }

Examples

Use the above examples just replace let result with

let result = json.filter { $0.value != nil }.mapValues { $0! }

You can create an array containing the keys whose corresponding values are nil:

let keysToRemove = dict.keys.array.filter { dict[$0]! == nil }

and next loop through all elements of that array and remove the keys from the dictionary:

for key in keysToRemove {
    dict.removeValueForKey(key)
}

Update 2017.01.17

The force unwrapping operator is a bit ugly, although safe, as explained in the comments. There are probably several other ways to achieve the same result, a better-looking way of the same method is:

let keysToRemove = dict.keys.filter {
  guard let value = dict[$0] else { return false }
  return value == nil
}

I ended up with this in Swift 2 :

extension Dictionary where Value: AnyObject {

    var nullsRemoved: [Key: Value] {
        let tup = filter { !($0.1 is NSNull) }
        return tup.reduce([Key: Value]()) { (var r, e) in r[e.0] = e.1; return r }
    }
}

Same answer but for Swift 3 :

extension Dictionary {

    /// An immutable version of update. Returns a new dictionary containing self's values and the key/value passed in.
    func updatedValue(_ value: Value, forKey key: Key) -> Dictionary<Key, Value> {
        var result = self
        result[key] = value
        return result
    }

    var nullsRemoved: [Key: Value] {
        let tup = filter { !($0.1 is NSNull) }
        return tup.reduce([Key: Value]()) { $0.0.updatedValue($0.1.value, forKey: $0.1.key) }
    }
}

Things get a lot easier in Swift 4 . Just use Dictionary's filter directly.

jsonResult.filter { !($0.1 is NSNull) }

Or if you don't want to remove the relevant keys, you can do this:

jsonResult.mapValues { $0 is NSNull ? nil : $0 }

Which will replace the NSNull values with nil instead of removing the keys.

Assuming that you just want to filter out any NSNull values from a dictionary, this is probably one of the better ways of going about it. It's future-proofed against Swift 3, as far as I know for now:

(With thanks to AirspeedVelocity for the extension, translated to Swift 2)

import Foundation

extension Dictionary {
/// Constructs [key:value] from [(key, value)]

  init<S: SequenceType
    where S.Generator.Element == Element>
    (_ seq: S) {
      self.init()
      self.merge(seq)
  }

  mutating func merge<S: SequenceType
    where S.Generator.Element == Element>
    (seq: S) {
      var gen = seq.generate()
      while let (k, v) = gen.next() {
        self[k] = v
      }
  }
}

let jsonResult:[String: AnyObject] = [
  "FirstName": "Anvar",
  "LastName" : "Azizov",
  "Website"  : NSNull(),
  "About"    : NSNull()]

// using the extension to convert the array returned from flatmap into a dictionary
let clean:[String: AnyObject] = Dictionary(
  jsonResult.flatMap(){ 
    // convert NSNull to unset optional
    // flatmap filters unset optionals
    return ($0.1 is NSNull) ? .None : $0
  })
// clean -> ["LastName": "Azizov", "FirstName": "Anvar"]

Suggesting this approach, flattens optional values and also compatible with Swift 3

String key, optional AnyObject? value dictionary with nil values:

let nullableValueDict: [String : AnyObject?] = [
    "first": 1,
    "second": "2",
    "third": nil
]

// ["first": {Some 1}, "second": {Some "2"}, "third": nil]

nil values removed and transformed into non optional value dictionary

nullableValueDict.reduce([String : AnyObject]()) { (dict, e) in
    guard let value = e.1 else { return dict }
    var dict = dict
    dict[e.0] = value
    return dict
}

// ["first": 1, "second": "2"]

Redeclaration var dict = dict is needed due to removal of var parameters in swift 3, so for swift 2,1 it could be;

nullableValueDict.reduce([String : AnyObject]()) { (var dict, e) in
    guard let value = e.1 else { return dict }
    dict[e.0] = value
    return dict
}

Swift 4, would be;

let nullableValueDict: [String : Any?] = [
    "first": 1,
    "second": "2",
    "third": nil
]

let dictWithoutNilValues = nullableValueDict.reduce([String : Any]()) { (dict, e) in
    guard let value = e.1 else { return dict }
    var dict = dict
    dict[e.0] = value
    return dict
}

Swift 5

compactMapValues(_:)

Returns a new dictionary containing only the key-value pairs that have non-nil values as the result of transformation by the given closure.

let people = [
    "Paul": 38,
    "Sophie": 8,
    "Charlotte": 5,
    "William": nil
]

let knownAges = people.compactMapValues { $0 }

You can replace the null values to empty string by following function.

func removeNullFromDict (dict : NSMutableDictionary) -> NSMutableDictionary
{
    let dic = dict;

    for (key, value) in dict {

        let val : NSObject = value as! NSObject;
        if(val.isEqual(NSNull()))
        {
            dic.setValue("", forKey: (key as? String)!)
        }
        else
        {
            dic.setValue(value, forKey: key as! String)
        }

    }

    return dic;
}

Since Swift 4 provides method reduce(into:_:) for class Dictionary you can remove nil-values from Dictionary with the following function:

func removeNilValues<K,V>(dict:Dictionary<K,V?>) -> Dictionary<K,V> {

    return dict.reduce(into: Dictionary<K,V>()) { (currentResult, currentKV) in

        if let val = currentKV.value {

            currentResult.updateValue(val, forKey: currentKV.key)
        }
    }
}

You can test it like that:

let testingDict = removeNilValues(dict: ["1":nil, "2":"b", "3":nil, "4":nil, "5":"e"])
print("test result is \(testingDict)")

Swift5

Use compactMapValues to create a dictionary extension for future usage

extension Dictionary where Key == String, Value == Optional<Any> {
    func discardNil() -> [Key: Any] {
        return self.compactMapValues({ $0 })
    }
}

How to use

public func toDictionary() -> [String: Any] {
    let emailAddress: String? = "test@mail.com"
    let phoneNumber: String? = "xxx-xxx-xxxxx"
    let newPassword: String = "**********"

    return [
        "emailAddress": emailAddress,
        "phoneNumber": phoneNumber,
        "passwordNew": newPassword
    ].discardNil()
}

I just had to solve this for a general case where NSNulls could be nested in the dictionary at any level, or even be part of an array:

extension Dictionary where Key == String, Value == Any {

func strippingNulls() -> Dictionary<String, Any> {

    var temp = self
    temp.stripNulls()
    return temp
}

mutating func stripNulls() {

    for (key, value) in self {
        if value is NSNull {
            removeValue(forKey: key)
        }
        if let values = value as? [Any] {
            var filtered = values.filter {!($0 is NSNull) }

            for (index, element) in filtered.enumerated() {
                if var nestedDict = element as? [String: Any] {
                    nestedDict.stripNulls()

                    if nestedDict.values.count > 0 {
                        filtered[index] = nestedDict as Any
                    } else {
                        filtered.remove(at: index)
                    }
                }
            }

            if filtered.count > 0 {
                self[key] = filtered
            } else {
                removeValue(forKey: key)
            }
        }

        if var nestedDict = value as? [String: Any] {

            nestedDict.stripNulls()

            if nestedDict.values.count > 0 {
                self[key] = nestedDict as Any
            } else {
                self.removeValue(forKey: key)
            }
        }
    }
}

}

I hope this is helpful to others and I welcome improvements!

(Note: this is Swift 4)

Following is the solution, when JSON have sub-dictionaries . This will go-through all the dictionaries , sub-dictionaries of JSON and remove NULL (NSNull) key-value pair from the JSON .

extension Dictionary {

    func removeNull() -> Dictionary {
        let mainDict = NSMutableDictionary.init(dictionary: self)
        for _dict in mainDict {
            if _dict.value is NSNull {
                mainDict.removeObject(forKey: _dict.key)
            }
            if _dict.value is NSDictionary {
                let test1 = (_dict.value as! NSDictionary).filter({ $0.value is NSNull }).map({ $0 })
                let mutableDict = NSMutableDictionary.init(dictionary: _dict.value as! NSDictionary)
                for test in test1 {
                    mutableDict.removeObject(forKey: test.key)
                }
                mainDict.removeObject(forKey: _dict.key)
                mainDict.setValue(mutableDict, forKey: _dict.key as? String ?? "")
            }
            if _dict.value is NSArray {
                let mutableArray = NSMutableArray.init(object: _dict.value)
                for (index,element) in mutableArray.enumerated() where element is NSDictionary {
                    let test1 = (element as! NSDictionary).filter({ $0.value is NSNull }).map({ $0 })
                    let mutableDict = NSMutableDictionary.init(dictionary: element as! NSDictionary)
                    for test in test1 {
                        mutableDict.removeObject(forKey: test.key)
                    }
                    mutableArray.replaceObject(at: index, with: mutableDict)
                }
                mainDict.removeObject(forKey: _dict.key)
                mainDict.setValue(mutableArray, forKey: _dict.key as? String ?? "")
            }
        }
        return mainDict as! Dictionary<Key, Value>
    }
 }

Trim Null form NSDictionary to get ride of crash Pass Dictionary to this function and get result as NSMutableDictionary

func trimNullFromDictionaryResponse(dic:NSDictionary) -> NSMutableDictionary {
    let allKeys = dic.allKeys
    let dicTrimNull = NSMutableDictionary()
    for i in 0...allKeys.count - 1 {
        let keyValue = dic[allKeys[i]]
        if keyValue is NSNull {
            dicTrimNull.setValue("", forKey: allKeys[i] as! String)
        }
        else {
            dicTrimNull.setValue(keyValue, forKey: allKeys[i] as! String)
        }
    }
    return dicTrimNull
}

Try to unwrap it with the expected type of values and check for the nill or empty if yes then remove that value from the dictionary.

This will work if the values in the dictionary is has empty values

//
//  Collection+SF.swift
//  PanoAI
//
//  Created by Forever Positive on 2021/1/30.
//

import Foundation

extension Array where Element == Optional<Any>{
    var trimed:[Any] {
       var newArray = [Any]()
        for item in self{
            if let item = item as? [Any?] {
                newArray.append(contentsOf: item.trimed)
            }else if item is NSNull || item == nil {
                //skip
            }else if let item = item{
                newArray.append(item)
            }
        }
        return newArray
    }
}

extension Dictionary where Key == String, Value == Optional<Any> {
    var trimed:[Key: Any] {
        return self.compactMapValues({ v in
            if let dic = v as? [String:Any?] {
                return dic.trimed
            }else if let _ = v as? NSNull {
                return nil;
            }else if let array = v as? [Any?] {
                return array.trimed;
            }else{
                return v;
            }
        })
    }
}

// MARK: - Usage
//let dic:[String:Any?] = ["a":"a","b":nil,"c":NSNull(),"d":["1":"1","2":nil,"3":NSNull(),"4":["a","b",nil,NSNull()]],"e":["a","b",nil,NSNull()]]
//print(dic.trimed)

Another quick way to do it in case you're not in Swift 5 yet... This will produce the same effect as compactMapValues .

Same dictionary but with no optionals.

let cleanDictionary = originalDictionary.reduce(into: [String: Any]()) { $0[$1.key] = $1.value }

Quick playground:

let originalDictionary = [
    "one": 1,
    "two": nil,
    "three": 3]

let cleanDictionary = originalDictionary.reduce(into: [String: Any]()) { $0[$1.key] = $1.value }    
for element in cleanDictionary {
    print(element)
}

output:

["one": 1, "three": 3]

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