Swift - Converting a Dictionary to a KeyValuePair [duplicate] - ios

In Swift Charts the signature for chartForegroundStyleScale to set the ShapeStyle for each data series is:
func chartForegroundStyleScale<DataValue, S>(_ mapping: KeyValuePairs<DataValue, S>) -> some View where DataValue : Plottable, S : ShapeStyle
The KeyValuePairs initialiser (init(dictionaryLiteral: (Key, Value)...)) only takes a variadic parameter so any attempt to initialise a foreground style from an array (in my case <String, Color>) results in the error:
Cannot pass array of type '[(String, Color)]' as variadic arguments of type '(String, Color)'
In my application the names of the chart series are set dynamically from the data so although I can generate a [String : Color] dictionary or an array of (String, Color) tuples I can't see that it's possible to pass either of these into chartForegroundStyleScale? Unless I'm missing something this seems like a odd limitation in Swift charts that the series names need to be hard coded for this modifier?

OK I've found an approach that works as long as an arbitrary limitation to the number of entries is acceptable (example below with max size of 4:
func keyValuePairs<S, T>(_ from: [(S, T)]) -> KeyValuePairs<S, T> {
switch from.count {
case 1: return [ from[0].0 : from[0].1 ]
case 2: return [ from[0].0 : from[0].1, from[1].0 : from[1].1 ]
case 3: return [ from[0].0 : from[0].1, from[1].0 : from[1].1, from[2].0 : from[2].1 ]
default: return [ from[0].0 : from[0].1, from[1].0 : from[1].1, from[2].0 : from[2].1, from[3].0 : from[3].1 ]
}
In my case I know that there won't be more than 20 mappings so this func can just be extended to accommodate that number.
Not ideal, but it works...

You could also pass an array of colors to .chartForegroundStyleScale(range:). As long as you add the colors to the array in the same order you add your graph marks it should work fine.
Not incredibly elegant either, but this approach works with an arbitrary number or entries.
struct GraphItem: Identifiable {
var id = UUID()
var label: String
var value: Double
var color: Color
}
struct ContentView: View {
let data = [
GraphItem(label: "Apples", value: 2, color: .red),
GraphItem(label: "Pears", value: 3, color: .yellow),
GraphItem(label: "Melons", value: 5, color: .green)
]
var body: some View {
Chart {
ForEach(data, id: \.label) { item in
BarMark(
x: .value("Count", item.value),
y: .value("Fruit", item.label)
)
.foregroundStyle(by: .value("Fruit", item.label))
}
}
.chartForegroundStyleScale(range: graphColors(for: data))
}
func graphColors(for input: [GraphItem]) -> [Color] {
var returnColors = [Color]()
for item in input {
returnColors.append(item.color)
}
return returnColors
}
}

Related

Binding a Stepper with a Dictionary SwiftUI

I'm not sure if this is possible but I'm currently trying to create a list of steppers using a dictionary[String: Int]. Using the stepper I'm hoping to change the qty amount in the dictionary. I tried binding the value to the stepper by first doing $basic[name] and then that didn't work and so I ended up with $basic[keyPath: name] which resulted in fewer errors but still wasn't working. In the beginning I was having problems of not wanting to change the order of the dictionary that I made, and so I ended up with the ForEach below which worked for not changing the order of dictionary, however, I'm wondering if that's one of the reasons that the binding isn't working.
import SwiftUI
struct AllSuppliesStruct {
#State var basic = ["Regular Staples": 0, "Big Staples": 0]
var body: some View {
Form {
//Basic Supplies
ForEach(basic.sorted(by: >), id: \.key) { name, qty in
Stepper("\(name), \(qty)", value: $basic[keyPath: name], in: 0...10)
}
}
}
}
Goal:
If I pressed on the stepper only once for both Regular and Big Staples then I expect this in the dictionary
basic = ["Regular Staples": 1, "Big Staples": 1]
You can manually create a Binding that acts on basic and pass that to Stepper:
struct AllSuppliesStruct: View {
#State var basic = ["Regular Staples": 0, "Big Staples": 0]
var body: some View {
Form {
ForEach(basic.sorted(by: >), id: \.key) { name, qty in
Stepper(
"\(name), \(qty)",
value: .init(
get: { basic[name]! },
set: { basic[name] = $0 }
),
in: 0...10
)
}
}
}
}

How to use Swift's higher order functions for parsing dynamic dictionary data in swift?

I am trying to parse the following json and want to retrieve the "key" of a dictionary whose value matches with the given value.
{ "OuterArrayHolder" :
[
{
"dictDynamicKey" : ["dynamicValue1", "dynamicValue2", "dynamicValue3"]
},
{
"dictAnotherDynamicKey" : ["dynamicValue4", "dynamicValue5", "dynamicValue6"]
},
]
}
[Note: Here in above json, all the keys and values are dynamic except "OuterArrayHolder".]
I have implemented it in a non-Swifty way and currently getting the expected output, but I am not getting how to accomplish the same behaviour using swift's higher-order functions.
Input : "dynamicValue2"
Expected output : "dictDynamicKey"
Current solution:
let inputValue = "dynamicValue2"
if !outerArrayHolder.isEmpty {
for dynamicDict in outerArrayHolder {
for (key, value) in dynamicDict {
if value.empty || !value.contains(inputValue) {
continue
} else {
//here if inputValue matches in contianed array (value is array in dictionary) then I want to use its "repective key" for further businisess logic.
}
}
}
}
I want to reduce these two for loops and want to use higher-order functions to achieve the exact behavior, Any help in this regard is really appreciated.
Can we convert your algorithm to a functional style? Yes. Is it a good idea? Probably not in this case. But here's how.
You didn't give any type information, so I'll use this type:
let outerArrayHolder: [[String: Any]] = [
[
"dictDynamicKey": ["dynamicValue1", "dynamicValue2", "dynamicValue3"]
],
[
"dictAnotherDynamicKey" : ["dynamicValue4", "dynamicValue5", "dynamicValue6"]
],
]
And you want to find the key corresponding to the array that contains inputValue:
let inputValue = "dynamicValue2"
The functional strategy is to map each dictionary in outerArrayHolder to its first key that has a matching value. If a dictionary has no such key, the dictionary is mapped to nil. Then we throw away the nils and take the first remaining value.
We can do it with filter, as requested:
let key = outerArrayHolder.lazy
.compactMap {
$0.lazy
.filter { ($0.value as? [String])?.contains(inputValue) ?? false }
.map { $0.key }
.first }
.first
But we can save a lazy and a first using first(where:):
let key = outerArrayHolder.lazy
.compactMap({
$0
.first(where: { ($0.value as? [String])?.contains(inputValue) ?? false })
.map { $0.key }
}).first
I don't see what this has to do with higher-order functions. If the outer key is known, I would simply write
// just building your structure
let d1 = ["dictDynamicKey" : ["dynamicValue1", "dynamicValue2", "dynamicValue3"]]
let d2 = ["dictAnotherDynamicKey" : ["dynamicValue4", "dynamicValue5", "dynamicValue6"]]
let d = ["OuterArrayHolder" : [d1, d2]]
// this is the actual code:
func find(_ target:String) -> String? {
for dict in d["OuterArrayHolder"]! {
for (k,v) in dict {
if v.contains(target) {return k}
}
}
return nil
}
That's just what you're doing, only it's clean.
There's no higher order function that does precisely what you're looking for. The closest is first(where:), but the problem is that the result is just Bool, and you don't have a way to cleanly fish out data related to the found case.
You could write something like:
extension Sequence {
func findFirst<T>(where predicate: (Element) throws -> T?) rethrows -> T? {
for element in self {
if let result = try predicate(element) {
return result
}
}
return nil
}
}
and then use it like:
let dictionaries = [
[
"dictDynamicKey" : ["dynamicValue1", "dynamicValue2", "dynamicValue3"]
],
[
"dictAnotherDynamicKey" : ["dynamicValue4", "dynamicValue5", "dynamicValue6"]
],
]
let desiredValue = "dynamicValue2"
extension Sequence {
func findFirst<T>(where predicate: (Element) throws -> T?) rethrows -> T? {
for element in self {
if let result = try predicate(element) {
return result
}
}
return nil
}
}
let result = dictionaries.findFirst(where: { dict in
dict.findFirst(where: { key, values in
values.contains(desiredValue) ? key : nil
})
})
print(result as Any) // => Optional("dictDynamicKey")
But it's probably more complexity than it's probably worth. I would recommend Matt's solution.
Scaled solution
You haven't clarified on this, but I suspect that you probably need to do this a bunch of times. In that case, linear searching through this gets really slow. By searching for keys by their values, you're not taking advantage of the key benefit of dictionaries: constant-time access to a value by its key. Your code is:
Linear-searching through the array of dictionaries, introduces an O(dictionaries.count) factor
For each dict in the array in #1, linear-searching through the key/value pairs, which introduces a O(dict.count) factor
For each key/value pair in the dict in #2, linear-searching through array of values, which introduces a O(valueArray.count) factor.
The total time complexity multiplies up to O(dictionaries.count * averageDict.count * averageValueArray.count), which gets really slow really quick.
Instead, you can spend some compute cost up-front, to create a new data structure that is better able to service the kinds of queries you want to run on it. In this case, you can "invert" a dictionary.
extension Dictionary {
func inverted<T>() -> [T: Key] where Dictionary.Value == [T] {
let invertedKeyValuePairs = self
.lazy
.flatMap { oldKey, oldValues in
oldValues.map { oldValue in (key: oldValue, value: oldKey) as (T, Key) }
}
return Dictionary<T, Key>(uniqueKeysWithValues: invertedKeyValuePairs)
}
}
// Example usage:
let valuesByKeys = [
"a": [1, 2, 3],
"b": [4, 5, 6]
]
let keysPerValue = valuesByKeys.inverted()
keysPerValue.forEach { key, value in print("key: \(key), value: \(value)") }
// Which results in:
// key: 3, value: a
// key: 4, value: b
// key: 5, value: b
// key: 1, value: a
// key: 6, value: b
// key: 2, value: a
Given such an inverted implementation, you can invert each dict of your input set, and merge them all together:
let invertedDictionary = Dictionary(uniqueKeysWithValues: dictionaries.flatMap { $0.inverted() })
invertedDictionary.forEach { key, value in print("key: \(key), value: \(value)") }
// Result:
key: dynamicValue6, value: dictAnotherDynamicKey
key: dynamicValue1, value: dictDynamicKey
key: dynamicValue2, value: dictDynamicKey
key: dynamicValue3, value: dictDynamicKey
key: dynamicValue4, value: dictAnotherDynamicKey
key: dynamicValue5, value: dictAnotherDynamicKey
You can store and share this dictionary, which can give constant time (O(1)) access to the key that was associated with any desired value:
print(invertedDictionary[desiredValue] as Any) // => Optional("dictDynamicKey")

How to remove a pair from a dictionary for a specified key?

I want to write a function with a parameter, which shall be used for a comparison with a key of a dictionary. The function iterates a collection and checks if the case has a pair with this key. If it has, I want to remove that pair, leave the other in that case and move on with the next case.
I've created a function filterAndExtract(). However it only iterates and do nothing. When comparing (Bool) the parameter and the key in each case, it doesn't work as expected. I want to know how to identify a key in a pair, so I can do stuff with the cases in the collection. Thanks in advance!
enum Tags: String {
case one = "One"
case two = "Two"
case three = "Three"
}
struct Example {
var title: String
var pair: [Tags: String]
}
let cases = [
Example(title: "Random example One", pair: [Tags.one: "First preview", Tags.two: "Second preview"]),
Example(title: "Random example Two", pair: [Tags.two: "Thrid preview", Tags.three: "Forth preview"]),
Example(title: "Random example Three", pair: [Tags.three: "Fifth preview", Tags.one: "Sixth preview"])
]
func filterAndExtract(collection: [Example], tag: Tags) {
for var item in collection {
let keys = item.pair.keys
for key in keys {
if key == tag {
item.pair.removeValue(forKey: key)
}
}
}
for i in collection {
print("\(i.title) and \(i.pair.values) \nNEXT TURN--------------------------------------------------\n")
}
}
//Results:
//Random example One and ["Second preview", "First preview"]
//NEXT TURN--------------------------------------------------
//Random example Two and ["Thrid preview", "Forth preview"]
//NEXT TURN--------------------------------------------------
//Random example Three and ["Fifth preview", "Sixth preview"]
//NEXT TURN--------------------------------------------------
//Solution (how I want it to look at the end):
for var i in cases {
i.pair.removeValue(forKey: .three)
print("\(i.title) and \(i.pair.values) \nNEXT TURN--------------------------------------------------\n")
}
//Random example One and ["Second preview", "First preview"]
//NEXT TURN--------------------------------------------------
//Random example Two and ["Thrid preview"]
//NEXT TURN--------------------------------------------------
//Random example Three and ["Sixth preview"]
//NEXT TURN--------------------------------------------------
Swift collections are value types. Whenever you assign a collection to a variable you'll get a copy of the object.
To modify the parameter collections you have to make it mutable and you have to modify the value directly inside collections.
func filterAndExtract(collection: [Example], tag: Tags) {
var collection = collection
for (index, item) in collection.enumerated() {
let keys = item.pair.keys
for key in keys {
if key == tag {
collection[index].pair.removeValue(forKey: key)
}
}
}
for i in collection {
print("\(i.title) and \(i.pair.values) \nNEXT TURN--------------------------------------------------\n")
}
}
First of all, it's much more cleaner and reliable if you encapsulate the remove function into the Example {
struct Example {
var title: String
var pair: [Tags: String]
mutating func remove(key: Tags) -> String? {
return pair.removeValue(forKey: key)
}
}
Second of all, you can use map for tasks like this:
func filterAndExtract(collection: [Example], tag: Tags) -> [Example] {
return collection.map { item -> Example in
var edited = item
edited.remove(key: tag)
print("\(edited.title) and \(edited.pair.values) \nNEXT TURN--------------------------------------------------\n")
return edited
}
}
I return some values in both functions, so you can use them if you want.

Create height array in metric and imperial for UIPickerView?

I'm trying to fill a UIPickerView with options using a static variable. Is there a more swifty way in creating a list of metric height instead of a for-loop?
Here's what I got:
static var heightMetric: [(key: String, value: String)] {
var items: (key: String, value: String) = []
for item in 30...330 {
items.append(("\(item)", "\(item) cm"))
}
return items
}
For the imperial form, any idea on a good way to create the list of options in a format as 5' 8" to fill a UIPickerView?
No need for the for-loop actually:
static var heightMetric: [(key: String, value: String)] = {
return (30...330).map {
("\($0)", "\($0) cm")
}
}()
You can even drop the explicit type and simply write static var heightMetric = { ...
Regarding the foot and inch form I do not know of any built-in functionality for that. You either have to calculate the value based on the centimeter value by doing a little bit of math or create a map similar to the above one where you might make use of the % operator to split the number into feet and inches. For example:
static var heightMetricImperial = {
return (12...120).map {
("\($0)", "\($0 / 12)' \($0 % 12)\"")
}
}()
Complete example:
class K {
static var heightMetric = {
(30...330).map {
("\($0)", "\($0) cm")
}
}()
static var heightMetricImperial = {
(12...120).map {
("\($0)", "\($0 / 12)' \($0 % 12)\"")
}
}()
}
print(K.heightMetric)
print(K.heightMetricImperial)

How to sort 1 array in Swift / Xcode and reorder multiple other arrays by the same keys changes

Sorry for the complex wording of the question. My main experience is with PHP and it has a command called array_multisort. The syntax is below:
bool array_multisort ( array &$array1 [, mixed $array1_sort_order = SORT_ASC [, mixed $array1_sort_flags = SORT_REGULAR [, mixed $... ]]] )
It lets you sort 1 array and the reorder multiple other arrays based on the key changes in the original.
Is there an equivalent command in Swift / Xcode 7.2?
I have currently have a set of arrays:
FirstName
Age
City
Country
Active
Active is an array of time in seconds that a user has been active within my app. I would like to order that descending or ascending and the other arrays to change to remain consistent.
You could create an array of indexes in sorted order and use it as a mapping:
var names = [ "Paul", "John", "David" ]
var ages = [ 35, 42, 27 ]
let newOrder = names.enumerate().sort({$0.1<$1.1}).map({$0.0})
names = newOrder.map({names[$0]})
ages = newOrder.map({ages[$0]})
[EDIT] Here's an improvement on the technique :
It's the same approach but does the sorting and assignment in one step.
(can be reassigned to original arrays or to separate ones)
(firstNames,ages,cities,countries,actives) =
{(
$0.map{firstNames[$0]},
$0.map{ages[$0]},
$0.map{cities[$0]},
$0.map{countries[$0]},
$0.map{actives[$0]}
)}
(firstNames.enumerated().sorted{$0.1<$1.1}.map{$0.0})
[EDIT2] and an Array extension to make it even easier to use if you are sorting in place:
extension Array where Element:Comparable
{
func ordering(by order:(Element,Element)->Bool) -> [Int]
{ return self.enumerated().sorted{order($0.1,$1.1)}.map{$0.0} }
}
extension Array
{
func reorder<T>(_ otherArray:inout [T]) -> [Element]
{
otherArray = self.map{otherArray[$0 as! Int]}
return self
}
}
firstNames.ordering(by: <)
.reorder(&firstNames)
.reorder(&ages)
.reorder(&cities)
.reorder(&countries)
.reorder(&actives)
combining the previous two:
extension Array
{
func reordered<T>(_ otherArray:[T]) -> [T]
{
return self.map{otherArray[$0 as! Int]}
}
}
(firstNames,ages,cities,countries,actives) =
{(
$0.reordered(firstNames),
$0.reordered(ages),
$0.reordered(cities),
$0.reordered(countries),
$0.reordered(actives)
)}
(firstNames.ordering(by:<))
I would go with #AntonBronnikov suggestion, and put all your properties into an struct, making an Array of that particular struct and then sorting it.
This data is clearly related and it's a cleaner approach.
Edit this is valid for 2 arrays:
Adding to #AlainT answer, but using zip:
var names = [ "Paul", "John", "David" ]
var ages = [ 35, 42, 27 ]
let sortedTuple = zip(names, ages).sort { $0.0.0 < $0.1.0 }
Something more generic:
names.enumerate().sort({$0.1<$1.1}).map({ (name: $0.1, age: ages[$0.0]) })
I believe AlainT:s solution is to prefer, but to extend the variety of options, below follows a solution mimicking what a zip5 method could let us achive (in case we could use zip for zipping together 5 sequences instead of its limit of 2):
/* example arrays */
var firstName: [String] = ["David", "Paul", "Lisa"]
var age: [Int] = [17, 27, 22]
var city: [String] = ["London", "Rome", "New York"]
var country: [String] = ["England", "Italy", "USA"]
var active: [Int] = [906, 299, 5060]
/* create an array of 5-tuples to hold the members of the arrays above.
This is an approach somewhat mimicking a 5-tuple zip version. */
var quinTupleArr : [(String, Int, String, String, Int)] = []
for i in 0..<firstName.count {
quinTupleArr.append((firstName[i], age[i], city[i], country[i], active[i]))
}
/* sort w.r.t. 'active' tuple member */
quinTupleArr.sort { $0.4 < $1.4 }
/* map back to original arrays */
firstName = quinTupleArr.map {$0.0}
age = quinTupleArr.map {$0.1}
city = quinTupleArr.map {$0.2}
country = quinTupleArr.map {$0.3}
active = quinTupleArr.map {$0.4}

Resources