heckj
heckj

Reputation: 7367

how to properly use a protocol that has an associated type to describe a property in another object

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

Answers (1)

Sweeper
Sweeper

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

Related Questions