Bridging Swift Generics with Darwin Calls

Seth Willits was working on an interpolation protocol that would allow conforming constructs to interpolate from one value to another, regardless of their underlying types. It needed to work for CGFloat as well as Double, and be decomposable and useful for CGPoint and CGRect as well as any custom Swift Polygon struct.

This created interesting challenges, as some of the most fundamental animation curves use exponentiation, which is not built into the Swift standard library. So I turned to pow, which is only defined for float and double:

public func powf(_: Float, _: Float) -> Float
public func pow(_: Double, _: Double) -> Double

In the past, I’ve created horrible bridging solution that permits migration from most floating point values to double. It’s not beautiful but I decided to pull it out from my toolkit and introduce it to this problem.

What I did was to build an extension on BinaryFloatingPoint that returned an interpolated value along a styled curve. My hack let me create a single interpolate method that applied across floating-point. I bridged to Double to use the Darwin pow function:

There’s an interesting CGFloat bug in play here, where the value history does not display as a graph but the numbers are right and work properly during animation.

Here’s the code. Shout out your improvements and alternatives.

9 Comments

  • Hi,

    I’d have written the first extension as:

    extension BinaryFloatingPoint {
    public var doubleValue: Double {
    switch self {
    case let value as Double: return value
    case let value as Float: return Double(value)
    case let value as Float80: return Double(value)
    case let value as CGFloat: return Double(value)
    default: fatalError("Unsupported floating point type")
    }
    }
    }

  • Hi Erica,

    The following rewrite of your code moves the curve functions to the InterpolationCurve enum, where I think they conceptually belong, rather than in an extension on a numeric protocol. An additional benefit is that it doesn’t need your extension on BinaryFloatingPoint that provides a doubleValue property.

    import Darwin // for pow(_:_:)

    public enum InterpolationCurve {

    case linear, easeIn, easeOut, easeInOut

    public func f(_ x: Double) -> Double {
    switch self {
    case .linear:
    return x
    case .easeIn:
    return pow(x, 3)
    case .easeOut:
    return 1 - pow(1 - x, 3)
    case .easeInOut where x Self {
    return self + (other - self) * Self(curve.f(fraction))
    }
    }

    for fraction in stride(from: 0.0, through: 1.0, by: 0.05) {
    0.0.interpolate(to: 1.0, by: fraction, of: .linear)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeIn)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeOut)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeInOut)
    }

    import CoreGraphics // for CGPoint

    public protocol Interpolating {
    func interpolate(to other: Self, by fraction: Double, of curve: InterpolationCurve) -> Self
    }

    extension CGPoint: Interpolating {

    public func interpolate(to other: CGPoint, by fraction: Double, of curve: InterpolationCurve) -> CGPoint {
    return CGPoint(
    x: self.x.interpolate(to: other.x, by: fraction, of: curve),
    y: self.y.interpolate(to: other.y, by: fraction, of: curve)
    )
    }
    }

    let p1 = CGPoint.zero
    let p2 = CGPoint(x: 1, y: 1)

    for fraction in stride(from: 0.0, through: 1.0, by: 0.05) {
    p1.interpolate(to: p2, by: fraction, of: .easeInOut)
    }

  • The blog somehow mangled my code (besides the annoying double-spacing it creates). I’ll try reposting the InterpolatingCurve enum.

    public enum InterpolationCurve {

    case linear, easeIn, easeOut, easeInOut

    public func f(_ x: Double) -> Double {
    switch self {
    case .linear:
    return x
    case .easeIn:
    return pow(x, 3)
    case .easeOut:
    return 1 - pow(1 - x, 3)
    case .easeInOut where x < 0.5:
    return 0.5 * pow(2 * x , 3)
    case .easeInOut:
    return 1 - 0.5 * pow(2 * (1 - x), 3)
    }
    }
    }

  • It’s OK now. 😀

  • Arrrrrggghhh! Now I see there are other lines missing in my first attempt. I’ll try posting the whole thing again.

    import Darwin // for pow(_:_:)

    public enum InterpolationCurve {

    case linear, easeIn, easeOut, easeInOut

    public func f(_ x: Double) -> Double {
    switch self {
    case .linear:
    return x
    case .easeIn:
    return pow(x, 3)
    case .easeOut:
    return 1 - pow(1 - x, 3)
    case .easeInOut where x Self {
    return self + (other - self) * Self(curve.f(fraction))
    }
    }

    for fraction in stride(from: 0.0, through: 1.0, by: 0.05) {
    0.0.interpolate(to: 1.0, by: fraction, of: .linear)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeIn)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeOut)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeInOut)
    }

    import CoreGraphics // for CGPoint

    public protocol Interpolating {
    func interpolate(to other: Self, by fraction: Double, of curve: InterpolationCurve) -> Self
    }

    extension CGPoint: Interpolating {

    public func interpolate(to other: CGPoint, by fraction: Double, of curve: InterpolationCurve) -> CGPoint {
    return CGPoint(
    x: self.x.interpolate(to: other.x, by: fraction, of: curve),
    y: self.y.interpolate(to: other.y, by: fraction, of: curve)
    )
    }
    }

    let p1 = CGPoint.zero
    let p2 = CGPoint(x: 1, y: 1)

    for fraction in stride(from: 0.0, through: 1.0, by: 0.05) {
    p1.interpolate(to: p2, by: fraction, of: .easeInOut)
    }

  • Nope, the same lines as vanished the first time have vanished again. I’ll try a series of posts.

    Part 1:

    import Darwin // for pow(_:_:)

    public enum InterpolationCurve {

    case linear, easeIn, easeOut, easeInOut

    public func f(_ x: Double) -> Double {
    switch self {
    case .linear:
    return x
    case .easeIn:
    return pow(x, 3)
    case .easeOut:
    return 1 - pow(1 - x, 3)
    case .easeInOut where x < 0.5:
    return 0.5 * pow(2 * x , 3)
    case .easeInOut:
    return 1 - 0.5 * pow(2 * (1 - x), 3)
    }
    }
    }

  • Part 2:

    extension BinaryFloatingPoint {

    public func interpolate(to other: Self, by fraction: Double, of curve: InterpolationCurve = .linear) -> Self {
    return self + (other - self) * Self(curve.f(fraction))
    }
    }

    for fraction in stride(from: 0.0, through: 1.0, by: 0.05) {
    0.0.interpolate(to: 1.0, by: fraction, of: .linear)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeIn)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeOut)
    0.0.interpolate(to: 1.0, by: fraction, of: .easeInOut)
    }

  • Part 3:

    import CoreGraphics // for CGPoint

    public protocol Interpolating {
    func interpolate(to other: Self, by fraction: Double, of curve: InterpolationCurve) -> Self
    }

    extension CGPoint: Interpolating {

    public func interpolate(to other: CGPoint, by fraction: Double, of curve: InterpolationCurve) -> CGPoint {
    return CGPoint(
    x: self.x.interpolate(to: other.x, by: fraction, of: curve),
    y: self.y.interpolate(to: other.y, by: fraction, of: curve)
    )
    }
    }

    let p1 = CGPoint.zero
    let p2 = CGPoint(x: 1, y: 1)

    for fraction in stride(from: 0.0, through: 1.0, by: 0.05) {
    p1.interpolate(to: p2, by: fraction, of: .easeInOut)
    }

  • Success at last (fingers crossed). Let me know if it doesn’t compile.