I looked at this answer, and it appears the response may be "you can't get there, from here," but I have to try...
I am building a dynamic SwiftUI Charts generator, and need to figure out how to create legends, on the fly.
The chartForegroundStyleScale decorator takes a KeyValuePair, as an initializer argument, but it appears as if KeyValuePair can only be built, using literals. I need to be able to build the legend programmatically (hopefully, without having to manually create the views by hand, which will work, but is a pain).
Here's the code I have now, which DOES NOT work:
/* ##################################################### */
/**
Returns a chart legend KeyValuePairs instance.
*/
var legend: KeyValuePairs<String, Color> {
var dictionaryLiterals = [(String, Color)]()
rows.forEach { $0.plottableData.forEach { dictionaryLiterals.append(($0.description, $0.color)) } }
var ret = KeyValuePairs<String, Color>()
dictionaryLiterals.forEach { ret.append($0) }
return ret
}
Can I get there, from here?
EDITED TO ADD: Yeah, I know that there's issues with the code, but I'll burn that bridge, when I get to it. The sample is good enough to show what I'm trying to do. If I can't get this working, I'll be building a view, instead, which will be a different operation, entirely.
I looked at this answer, and it appears the response may be "you can't get there, from here," but I have to try...
I am building a dynamic SwiftUI Charts generator, and need to figure out how to create legends, on the fly.
The chartForegroundStyleScale decorator takes a KeyValuePair, as an initializer argument, but it appears as if KeyValuePair can only be built, using literals. I need to be able to build the legend programmatically (hopefully, without having to manually create the views by hand, which will work, but is a pain).
Here's the code I have now, which DOES NOT work:
/* ##################################################### */
/**
Returns a chart legend KeyValuePairs instance.
*/
var legend: KeyValuePairs<String, Color> {
var dictionaryLiterals = [(String, Color)]()
rows.forEach { $0.plottableData.forEach { dictionaryLiterals.append(($0.description, $0.color)) } }
var ret = KeyValuePairs<String, Color>()
dictionaryLiterals.forEach { ret.append($0) }
return ret
}
Can I get there, from here?
EDITED TO ADD: Yeah, I know that there's issues with the code, but I'll burn that bridge, when I get to it. The sample is good enough to show what I'm trying to do. If I can't get this working, I'll be building a view, instead, which will be a different operation, entirely.
This has been an issue for me for a while now but reading this thread, I followed @david suggestion of chartForegroundStyleScale(domain:mapping:).
This worked for me! (finally)
The first part uses computed properties to combine the categories and the colors into their own matching arrays.
let showRate: Bool
let showRetries: Bool
let rateLegend = ["Tx Rate": Color.blue.opacity(0.5), "Rx Rate": Color.gray.opacity(0.5)]
let retriesLegend = ["Tx Retry Ratio": Color.mint.opacity(0.7), "Rx Retry Ratio": Color.purple]
var categories: [String] {
var combinedCategories: [String] = []
if showRate { combinedCategories.append(contentsOf: rateLegend.keys) }
if showRetries { combinedCategories.append(contentsOf: retriesLegend.keys) }
return combinedCategories
}
var colors: [Color] {
var combinedColors: [Color] = []
if showRate { combinedColors.append(contentsOf: rateLegend.values) }
if showRetries { combinedColors.append(contentsOf: retriesLegend.values) }
return combinedColors
}
Then on the chart, the following modifier does the trick.
.chartForegroundStyleScale(domain: categories, mapping: { category in
if let index = categories.firstIndex(of: category) {
return colors[index]
} else {
return .clear
}
}
)
There is no public API for this.
If you're looking for crazy workarounds, you can use the UnsafePointer APIs or unsafeBitCast(_:to:)
.
It just so happens that KeyValuePairs
is a wrapper around a single [(Key, Value)]
field. Swift guarantees that single-element structs have the same memory layout as the thing they wrap, so we can hack around this with the unsafe APIs. It's also @frozen
, so that layout is unlikely to change.
Here's a demo that shows how unsafe mutable pointers can be used to modify a pre-existing KeyValuePairs
value.
extension KeyValuePairs {
mutating func modifyAsArray(_ body: (UnsafeMutablePointer<Array<(Key, Value)>>) -> Void) {
// ⚠️ Total hack!
// Relies on the internal implementation details of `KeyValuePairs`,
// name that it's a wrapper around a single array and `@frozen`.
// https://github.com/swiftlang/swift/blob/97b1548stdlib/public/core/KeyValuePairs.swift#L75-L78
withUnsafeMutablePointer(to: &self) { selfP in
selfP.withMemoryRebound(to: [(Key, Value)].self, capacity: 1, body)
}
}
}
var kvp: KeyValuePairs<String, Int> = [:]
print(kvp) // empty to start
let dicts: [[String: Int]] = [
["k1": 1, "k2": 2],
["k1": 3, "k3": 4],
]
kvp.modifyAsArray { arrayPointer in
for dict in dicts {
for pair in dict {
arrayPointer.pointee.append(pair)
}
}
}
print(kvp) // => ["k2": 2, "k1": 1, "k3": 4, "k1": 3]
Here's a different approach, which let's you create a new KeyValuePairs
value from an array of pairs of your choosing:
extension KeyValuePairs {
init(fromArray array: [(Key, Value)]) {
// ⚠️ Total hack!
// Relies on the internal implementation details of `KeyValuePairs`,
// name that it's a wrapper around a single array and `@frozen`.
// https://github.com/swiftlang/swift/blob/97b1548stdlib/public/core/KeyValuePairs.swift#L75-L78
self = unsafeBitCast(array, to: Self.self)
}
}
let dicts: [[String: Int]] = [
["k1": 1, "k2": 2],
["k1": 3, "k3": 4],
]
let array: [(String, Int)] = dicts.flatMap { $0 }
let kvp = KeyValuePairs(fromArray: array)
print(kvp) // => ["k2": 2, "k1": 1, "k3": 4, "k1": 3]
Just to add some closure, this is what I did. I built my own view, but Ben Toner’s answer addressed the question that I asked, so it gets the greencheck.
/* ############################################# */
/**
This is the color to use, when a row is selected.
*/
public let RCVS_LegendSelectionColor = Color.red
/* ############################################# */
// MARK: - Legend Element Data Class -
/* ############################################# */
/**
This is used to generate the legend.
*/
class RCVS_LegendElement: Identifiable {
/* ############################################# */
/**
Make me identifiable.
*/
let id = UUID()
/* ############################################# */
/**
The name should be hashable.
*/
let name: String
/* ############################################# */
/**
We can change the color of a named item
*/
var color: Color
/* ############################################# */
/**
Default initializer. If you don't specify any arguments, the selection item is created.
- parameter name: The name of the legend item.
- parameter color: The color to associate with the name.
*/
init(name inName: String = "SLUG-SELECTED-LEGEND-LABEL".localizedVariant, color inColor: Color = RCVS_LegendSelectionColor) {
name = inName
color = inColor
}
}
/* ###################################################################################################################################### */
// MARK: - Chart Legend View -
/* ###################################################################################################################################### */
/**
This displays a horizontal row of legend elements for a chart.
*/
struct RCVST_ChartLegend: View {
/* ############################################# */
/**
This contains the built legend elements from which we'll build our display.
*/
@State private var _legendElements: [RCVS_LegendElement]
/* ############################################# */
/**
This returns a horizontal row of titles, preceded by dots. All will be the color they represent.
*/
var body: some View {
HStack(spacing: 8) {
ForEach(_legendElements) { inLegendElement in
HStack(spacing: 2) {
Circle()
.fill(inLegendElement.color)
.frame(width: 8)
Text(inLegendElement.name)
.font(.system(size: 10))
.italic()
.foregroundStyle(inLegendElement.color)
}
}
}
}
/* ############################################# */
/**
Initializer
- parameter legendElements: an array of legend elements that will be used to build the view.
*/
init(legendElements inLegendElements: [RCVS_LegendElement]) {
_legendElements = inLegendElements
}
}
chartForegroundStyleScale
method? Looking at the other overrides, I foundchartForegroundStyleScale(domain:mapping:)
, which actually takes a collection as its input rather than aKeyValuePairs
. – David Pasztor Commented Jan 2 at 16:58KeyValuePairs
object, which is only a problem that needs solving in case there's no alternative solution for the real issue you're encountering. – David Pasztor Commented Jan 2 at 17:01chartForegroundStyleScale(range:type:)
method instead? – Leo Dabus Commented Jan 2 at 17:18