Mapping a dictionary to an array isn't difficult. You have a few options, the map(_:)
method being my personal favorite for most situations. Converting an array to a dictionary is less trivial. Swift's Standard Library doesn't seem to offer an API to make this straightforward. In this episode, I show you two solutions.
An Example
Before I show you the two solutions I have in mind, we need an example. We define a struct with name AnalyticsClient
. The AnalyticsClient
struct is responsible for sending events to a third party analytics service. The API of the AnalyticsClient
struct is simple, a single method that accepts an Event
object and zero or more Property
objects as arguments.
struct AnalyticsClient {
func send(event: Event, properties: Property...) {
}
}
Event
is an enum that defines the events the analytics client supports. The raw value of each case is the name of the event.
enum Event: String {
// MARK: - Cases
case viewedProduct = "Viewed Product"
case purchasedProduct = "Purchased Product"
}
Property
is also an enum. Each case defines a property of an event and we use associated values to define the value of each property. The Property
enum defines a computed key
property and a computed value
property.
enum Property {
// MARK: - Cases
case product(id: String)
case price(Float)
case source(String)
// MARK: - Properties
var key: String {
switch self {
case .product:
return "product"
case .price:
return "price"
case .source:
return "source"
}
}
var value: String {
switch self {
case .product(id: let id):
return id
case .price(let price):
return "\(price)"
case .source(let source):
return source
}
}
}
Converting an Array to a Dictionary
The send(event:properties:)
method accepts zero or more Property
objects. The third party analytics service expects a dictionary of key-value pairs so we need to convert the sequence of properties to a dictionary of type [String: String]
.
The first solution is the most obvious one. We create a mutable dictionary of type [String: String]
and iterate through the sequence of properties that are passed to the send(event:properties:)
method. We use the computed key
and valueproperties of the
Property` enum to update the dictionary.
var eventProperties = [String: String]()
properties.forEach { property in
eventProperties[property.key] = property.value
}
While there isn't anything inherently wrong with this approach, I prefer another solution that is a little bit cleaner and requires less ceremony. We invoke the reduce(into:_:)
method on the sequence of properties. The reduce(into:_:)
method defines two parameters, the initial result and a closure that is invoked for each element of the sequence.
Because we want to convert the sequence of properties to a dictionary, the initial result is a dictionary of type [String: String]
. The second argument of the reduce(into:_:)
method is a closure that accepts the result and an element of the sequence. In the closure, we update the dictionary just like we did in the first solution. We assign the result of the reduce(into:_:)
method to a constant.
let eventProperties = properties.reduce(into: [String: String]()) { result, property in
result[property.key] = property.value
}
Both solutions are valid, but the solution that leverages the reduce(into:_:)
method is arguably cleaner and doesn't require eventProperties
to be mutable, that is, a variable.
Performance
Which solution you choose is up to your preference. The performance tests I ran indicate there is no significant difference in performance. I prefer the second solution as it is more concise and doesn't require a variable.
final class AnalyticsClientTests: XCTestCase {
func testPerformanceForEach() throws {
let properties = (0...1_000_000).map {
Property.product(id: "\($0)")
}
self.measure {
var dictionary = [String: String]()
properties.forEach { property in
dictionary[property.key] = property.value
}
}
}
func testPerformanceReduce() throws {
let properties = (0...1_000_000).map {
Property.product(id: "\($0)")
}
self.measure {
_ = properties.reduce(into: [String: String]()) { result, property in
result[property.key] = property.value
}
}
}
}