Map array of objects to Dictionary in Swift
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"]
iOSGeek
Updated on December 18, 2021Comments
-
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 over 4 yearsA 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 over 4 yearsTheir's a built in function. See here. It's basically
Dictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
-
-
SinisterMJ over 7 yearsI 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 over 7 yearsit's actually a loop, this way is simplified way to write it, the performance is the same or somehow better than normal loop
-
possen over 6 yearsKendall, 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 almost 6 yearsIn 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 over 5 yearsDo 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 over 5 yearsI 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 about 5 yearsI 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 over 4 yearsEureka! This is the droid I've been looking for! This is perfect and greatly simplify things in my code.
-
Mark A. Donohoe over 4 yearsDead links to the initializers. Can you please update to use permalink?
-
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 over 4 yearsPermalinks 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 almost 4 yearsThe thing that's unclear in this answer is what
$0
represents: it is theinout
dictionary that we are "returning" – though not really returning, as modifying it is the equivalent of returning due to it'sinout
-ness.