arrays - Adding Elements to a KeyValuePair<X, Y> - Stack Overflow

admin2025-05-01  7

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.

Share Improve this question edited Jan 2 at 15:17 Chris Marshall asked Jan 2 at 15:11 Chris MarshallChris Marshall 5,33210 gold badges51 silver badges79 bronze badges 6
  • 1 I believe the answer really is "you can't get there, from here". KeyValuePairs only takes a dictionary literal and as a matter of fact before Swift 5 the name of the type was DictionaryLiteral. – Joakim Danielson Commented Jan 2 at 15:28
  • 1 Are you sure you're using the correct override of the chartForegroundStyleScale method? Looking at the other overrides, I found chartForegroundStyleScale(domain:mapping:), which actually takes a collection as its input rather than a KeyValuePairs. – David Pasztor Commented Jan 2 at 16:58
  • 2 On that note, your question in its current form might be misleading. The real problem you're trying to solve is building the legend programmatically (which might be solved via a different Swift Charts method), not how to dynamically create a KeyValuePairs 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:01
  • Thanks for that! I'll see if It can give me what I want. I tend to find SwiftUI docs in the oddest places. – Chris Marshall Commented Jan 2 at 17:03
  • Why don't you use chartForegroundStyleScale(range:type:) method instead? – Leo Dabus Commented Jan 2 at 17:18
 |  Show 1 more comment

3 Answers 3

Reset to default 1

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
    }
}

转载请注明原文地址:http://www.anycun.com/QandA/1746110787a91816.html