Map array of objects to Dictionary in Swift

IosArraysSwiftDictionary

Ios Problem Overview


I have an array of Person's objects:

class Person {
   let name:String
   let position:Int
}

and the array is:

let myArray = [p1,p1,p3]

I want to map myArray to be a Dictionary of [position:name] the classic solution is:

var myDictionary =  [Int:String]()
        
for person in myArray {
    myDictionary[person.position] = person.name
}

is there any elegant way by Swift to do that with the functional approach map, flatMap... or other modern Swift style

Ios Solutions


Solution 1 - Ios

Since Swift 4 you can do @Tj3n's approach more cleanly and efficiently using the into version of reduce It gets rid of the temporary dictionary and the return value so it is faster and easier to read.

Sample code setup:

struct Person { 
    let name: String
    let position: Int
}
let myArray = [Person(name:"h", position: 0), Person(name:"b", position:4), Person(name:"c", position:2)]

Into parameter is passed empty dictionary of result type:

let myDict = myArray.reduce(into: [Int: String]()) {
    $0[$1.position] = $1.name
}

Directly returns a dictionary of the type passed in into:

print(myDict) // [2: "c", 0: "h", 4: "b"]

Solution 2 - Ios

Okay map is not a good example of this, because its just same as looping, you can use reduce instead, it took each of your object to combine and turn into single value:

let myDictionary = myArray.reduce([Int: String]()) { (dict, person) -> [Int: String] in
    var dict = dict
    dict[person.position] = person.name
    return dict
}

//[2: "b", 3: "c", 1: "a"]

In Swift 4 or higher please use the below answer for clearer syntax.

Solution 3 - Ios

Since Swift 4 you can do this very easily. There are two new initializers that build a dictionary from a sequence of tuples (pairs of key and value). If the keys are guaranteed to be unique, you can do the following:

let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 3)]

Dictionary(uniqueKeysWithValues: persons.map { ($0.position, $0.name) })

=> [1: "Franz", 2: "Heinz", 3: "Hans"]

This will fail with a runtime error if any key is duplicated. In that case you can use this version:

let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 1)]

Dictionary(persons.map { ($0.position, $0.name) }) { _, last in last }

=> [1: "Hans", 2: "Heinz"]

This behaves as your for loop. If you don't want to "overwrite" values and stick to the first mapping, you can use this:

Dictionary(persons.map { ($0.position, $0.name) }) { first, _ in first }

=> [1: "Franz", 2: "Heinz"]

Swift 4.2 adds a third initializer that groups sequence elements into a dictionary. Dictionary keys are derived by a closure. Elements with the same key are put into an array in the same order as in the sequence. This allows you to achieve similar results as above. For example:

Dictionary(grouping: persons, by: { $0.position }).mapValues { $0.last! }

=> [1: Person(name: "Hans", position: 1), 2: Person(name: "Heinz", position: 2)]

Dictionary(grouping: persons, by: { $0.position }).mapValues { $0.first! }

=> [1: Person(name: "Franz", position: 1), 2: Person(name: "Heinz", position: 2)]

Solution 4 - Ios

You may write custom initializer for Dictionary type, for example from tuples:

extension Dictionary {
    public init(keyValuePairs: [(Key, Value)]) {
        self.init()
        for pair in keyValuePairs {
            self[pair.0] = pair.1
        }
    }
}

and then use map for your array of Person:

var myDictionary = Dictionary(keyValuePairs: myArray.map{($0.position, $0.name)})

Solution 5 - Ios

How about a KeyPath based solution?

extension Array {
  func dictionary<Key, Value>(withKey key: KeyPath<Element, Key>, value: KeyPath<Element, Value>) -> [Key: Value] {
    reduce(into: [:]) { dictionary, element in
      let key = element[keyPath: key]
      let value = element[keyPath: value]
      dictionary[key] = value
    }
  }
}

This is how you use it:

struct HTTPHeader {
  let field: String, value: String
}

let headers = [
  HTTPHeader(field: "Accept", value: "application/json"),
  HTTPHeader(field: "User-Agent", value: "Safari")
]

headers.dictionary(withKey: \.field, value: \.value) // ["Accept": "application/json", "User-Agent": "Safari"]

Solution 6 - Ios

This is what I have been using

struct Person {
    let name:String
    let position:Int
}
let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 3)]

var peopleByPosition = [Int: Person]()
persons.forEach{peopleByPosition[$0.position] = $0}

Would be nice if there was a way to combine the last 2 lines so that peopleByPosition could be a let.

We could make an extension to Array that does that!

extension Array {
    func mapToDict<T>(by block: (Element) -> T ) -> [T: Element] where T: Hashable {
        var map = [T: Element]()
        self.forEach{ map[block($0)] = $0 }
        return map
    }
}

Then we can just do

let peopleByPosition = persons.mapToDict(by: {$0.position})

Solution 7 - Ios

You can use a reduce function. First I've created a designated initializer for Person class

class Person {
  var name:String
  var position:Int

  init(_ n: String,_ p: Int) {
    name = n
    position = p
  }
}

Later, I've initialized an Array of values

let myArray = [Person("Bill",1), 
               Person("Steve", 2), 
               Person("Woz", 3)]

And finally, the dictionary variable has the result:

let dictionary = myArray.reduce([Int: Person]()){
  (total, person) in
  var totalMutable = total
  totalMutable.updateValue(person, forKey: total.count)
  return totalMutable
}

Solution 8 - Ios

extension Array {
    func mapToDict<T>(by block: (Element) -> T ) -> [T: Element] where T: Hashable {
        var map = [T: Element]()
        self.forEach{ map[block($0)] = $0 }
        return map
    }
}

Solution 9 - Ios

Maybe something like this?

myArray.forEach({ myDictionary[$0.position] = $0.name })

Solution 10 - Ios

extension Array {

    func toDictionary() -> [Int: Element] {
        self.enumerated().reduce(into: [Int: Element]()) { $0[$1.offset] = $1.element }
    }
    
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioniOSGeekView Question on Stackoverflow
Solution 1 - IospossenView Answer on Stackoverflow
Solution 2 - IosTj3nView Answer on Stackoverflow
Solution 3 - IosMackie MesserView Answer on Stackoverflow
Solution 4 - IosShadow OfView Answer on Stackoverflow
Solution 5 - IoslucameghView Answer on Stackoverflow
Solution 6 - IosdatincView Answer on Stackoverflow
Solution 7 - IosDavidView Answer on Stackoverflow
Solution 8 - IosprashantView Answer on Stackoverflow
Solution 9 - IosVarun GoyalView Answer on Stackoverflow
Solution 10 - Iosuser_View Answer on Stackoverflow