Reputation: 7367
I have been creating a SwiftUI component, and I want this component to have a property that implements a protocol. The specific use case is drawing an Axis for a chart, which is based on a scale.
There are several concrete implementations of the scale that transform data from an input domain to an output range. The two I'm starting with is a Linear scale that converts from an Double
input domain to an output range represented with a Double
. The other is a Date/Time based scale that converted from a Date
based input domain to an output range represented by a Double
.
Scale is the protocol, and I took a stab at roughly defining it as:
public protocol Scale {
associatedtype InputDomain: Comparable // input domain
var isClamped: Bool { get }
// input values
var domain: ClosedRange<InputDomain> { get }
// output values
var range: ClosedRange<Double> { get }
/// converts a value between the input "domain" and output "range"
///
/// - Parameter inputValue: a value within the bounds of the ClosedRange for domain
/// - Returns: a value within the bounds of the ClosedRange for range, or NaN if it maps outside the bounds
func scale(_ inputValue: InputDomain) -> Double
/// converts back from the output "range" to a value within the input "domain". The inverse of scale()
///
/// - Parameter outputValue: a value within the bounds of the ClosedRange for range
/// - Returns: a value within the bounds of the ClosedRange for domain, or NaN if it maps outside the bounds
func invert(_ outputValue: Double) -> InputDomain
/// returns an array of the locations within the ClosedRange of range to locate ticks for the scale
///
/// - Parameter count: a number of ticks to display, defaulting to 10
/// - Returns: an Array of the values within the ClosedRange of the input range
func ticks(count: Int) -> [InputDomain]
}
The LinearScale and TimeScale structs conform the the protocol, each defining a typealias InputDomain = Double
and typealias InputDomain = Date
respectively.
The issue comes when I try to use this protocol to describe the kind of struct (a scale) that is used more generically with the SwiftUI component:
public struct AxisView: View {
let scale: Scale
public var body: some View { ... }
}
The compiler provides the error:
Protocol 'Scale' can only be used as a generic constraint because it has Self or associated type requirements
I'm not sure of the best way to tackle this problem, to resolve the compiler error/constraints. Should I be doing something to make the SwiftUI component a generic, or should I be not using an associated type with the protocol?
Or is there another way to think about structuring this code to support a variety of scale types, using protocols and structs?
UPDATE: I got an answer to the original question, but it's not entirely gelling for me. I added the generic definition to the enclosing type (my implementations of Scale
).
What I'm not clear on is why this is needed? After I added the generic marker on the enclosing struct, the compiler error went away. I'm assuming this is a place where there were several options that the swift compiler could have taken, and telling it "yeah, I want this to be a generic" is one path - what are possible other ones?
I also noticed that even though it was defined as a generic class, the specific class that I used was often inferred by the swift compiler. So I didn't need to fully specify the type with the generic syntax. For example, I could use
LinearScale()
instead of LinearScale<Double>
(), and it would infer the correct generic. Is that expected?
Upvotes: 0
Views: 117
Reputation: 271410
You should make your view generic as well:
public struct AxisView<ScaleType: Scale>: View {
let scale: ScaleType
public var body: some View { ... }
}
Upvotes: 2