Map array of objects to Dictionary in Swift

101,098

Solution 1

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 2

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 3

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

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

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"]
Share:
101,098
iOSGeek
Author by

iOSGeek

Updated on December 18, 2021

Comments

  • iOSGeek
    iOSGeek over 2 years

    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

    • Zonker.in.Geneva
      Zonker.in.Geneva over 4 years
      A little late to the game, but do you want a dictionary of [position:name] or [position:[name]]? If you have two people in the same position, your dictionary will only keep the last one encountered in your loop....I have a similar question for which I'm trying to find a solution, but I want the result to be like [1: [p1, p3], 2: [p2]]
    • mfaani
      mfaani over 4 years
      Their's a built in function. See here. It's basically Dictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
  • SinisterMJ
    SinisterMJ over 7 years
    I like this approach but is this more efficient than simply creating a loop to load a dictionary? I worry about performance because it seems like for every item it has to create a new mutable copy of an ever larger dictionary...
  • Tj3n
    Tj3n over 7 years
    it's actually a loop, this way is simplified way to write it, the performance is the same or somehow better than normal loop
  • possen
    possen over 6 years
    Kendall, see my answer below, it may make it faster than looping when using Swift 4. Although, I have not benchmarked it to confirm that.
  • Varrry
    Varrry almost 6 years
    In a fact according to docs 'grouping' initializer creates dictionary with array as value (that's 'grouping' means, right?), and in the case above it will be more like [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. Not the expected result, but it can be farther mapped to flat dictionary if you wish.
  • Mark A. Donohoe
    Mark A. Donohoe over 5 years
    Do not use this!!! As @KendallHelmstetterGelner pointed out, the problem with this approach is every iteration, it's copying the entire dictionary in its current state, so in the first loop, it's copying a one-item dictionary. The second iteration it's copying a two-item dictionary. That's a HUGE performance drain!! The better way to do it would be to write a convenience init on the dictionary that takes your array and adds all items in it. That way it's still a one-liner for the consumer, but it eliminates the entire alloc mess this has. Plus, you can pre-allocate based on the array.
  • bugloaf
    bugloaf over 5 years
    I don't think performance will be an issue. The Swift compiler will recognize that none of the old copies of the dictionary get used and probably won't even create them. Remember the first rule of optimizing your own code: "Don't do it." Another maxim of computer science is that efficient algorithms are only useful when N is large, and N is almost never large.
  • Lucas van Dongen
    Lucas van Dongen about 5 years
    I am a bit confused about why it only seems to work with the positional ($0, $1) arguments and not when I name them. Edit: nevermind, the compiler perhaps tripped up because I still tried to return the result.
  • Zonker.in.Geneva
    Zonker.in.Geneva over 4 years
    Eureka! This is the droid I've been looking for! This is perfect and greatly simplify things in my code.
  • Mark A. Donohoe
    Mark A. Donohoe over 4 years
    Dead links to the initializers. Can you please update to use permalink?
  • Mackie Messer
    Mackie Messer over 4 years
    @MarqueIV I fixed the broken links. Not sure what you are referring to with permalink in this context?
  • Mark A. Donohoe
    Mark A. Donohoe over 4 years
    Permalinks are links websites use that will never change. So for instance, instead of something like 'www.SomeSite.com/events/today/item1.htm' which may change day to day, most sites will set up a second 'permalink' like 'www.SomeSite.com?eventid=12345' which will never change, and thus, it's safe to use in links. Not every one does it, but most pages from the big sites will offer one.
  • adamjansch
    adamjansch almost 4 years
    The thing that's unclear in this answer is what $0 represents: it is the inout dictionary that we are "returning" – though not really returning, as modifying it is the equivalent of returning due to it's inout-ness.