3
\$\begingroup\$

I am trying to make an IOS tool similar to Android's ValueAnimator... more or less

This is useful for situations where you simply want a value to "animate" over time, but rather than necessarily attaching that value to some view property, it is available to use as you wish. Imagine applying it to an audio filter, for example.

It works like a Timer/NSTimer except that is passes a value back to the selector which behaves differently depending on the arguments sent to the initializer.

Concerns:

Is there a way to tidy up the numerous functions into something like a dictionary? (I'm not that advanced at Swift)

Is there a way of accomplishing the same functionality -- passing a "listener" function in on initialization which will be called back with the animated value as an argument -- but without using a selector? ... I ask because of the nasty "MyNSDoubleObject" workaround.

Any other improvements... this is my first question on here.

Code:

import Foundation

// this class is a work-around for the fact that we can't pass a raw value (int, double, etc.) through a selector -- must be object
class MyNSDoubleObject: NSObject {
    var val: Double

    init(val: Double) {
        self.val = val
    }
}

class ValueAnimator : NSObject {

    // args
    private let sampleRate: Int
    private let functionType: FunctionType
    private let selector: Selector
    private let target: AnyObject

    // computed from args
    private let maxIterations: Int
    private let timeInterval: Double
    private var X_currentRepIndex: Int = 0 // domain
    private var F_of_X = MyNSDoubleObject(val: 0.0) // range

    // other
    private var timer = Timer()

    enum FunctionType {
        case SINE_WAVE_FROM_0_TO_1_TO_0
        case SINE_WAVE_FROM_0_TO_1
        case SINE_WAVE_FROM_1_TO_0
    }

    // Public functions
    init(durationInSeconds: Int, sampleRate: Int, functionType: FunctionType, selector: Selector, target: AnyObject) {
        self.sampleRate = sampleRate
        self.maxIterations = durationInSeconds * sampleRate
        self.timeInterval = 1.0/Double(sampleRate)
        self.functionType = functionType
        self.selector = selector
        self.target = target
    }

    func start() {
        timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: (#selector(timerCallback)), userInfo: nil, repeats: true)
    }

    // "Private" functions
    @objc func timerCallback() {

        switch functionType {
        case .SINE_WAVE_FROM_0_TO_1_TO_0:
            F_of_X.val = sine010(x: X_currentRepIndex)
        case .SINE_WAVE_FROM_0_TO_1:
            F_of_X.val = sine01(x: X_currentRepIndex)
        case .SINE_WAVE_FROM_1_TO_0:
            F_of_X.val = sine10(x: X_currentRepIndex)
        }

        X_currentRepIndex += 1

        _ = target.perform(selector, with: F_of_X)

        if X_currentRepIndex == (maxIterations+1) {
            timer.invalidate()
        }
    }

    private func sine010(x: Int) -> Double {
        return (-cos(2*Double.pi * (Double(x)/Double(maxIterations)))+1)/2
    }

    private func sine01(x: Int) -> Double {
        return (-cos(Double.pi * (Double(x)/Double(maxIterations)))+1)/2
    }

    private func sine10(x: Int) -> Double {
        return (cos(Double.pi * (Double(x)/Double(maxIterations)))+1)/2
    }

}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Is there a way to tidy up the numerous functions into something like a dictionary?

Sure, there are various options. But first the functions should be made independent of maxIterations, i.e. the computation

Double(currentIteration) / Double(maxIterations)

should be done in the caller so that the functions simplify to

private func sine010(x: Double) -> Double {
    return (-cos(2 * .pi * x) + 1)/2
}

thus avoiding code duplication. Note also the use of the “implicit member expression” .pi – the type Double is automatically inferred from the context.

Now you could define a dictionary mapping a function types to functions. The functions can be embedded directly as closures instead of defining global functions:

let functionDict : [FunctionType: (Double) -> Double] = [
    .SINE_WAVE_FROM_0_TO_1_TO_0: { (1 - cos(2 * .pi * $0)) / 2 },
    .SINE_WAVE_FROM_0_TO_1: { (1 - cos(.pi * $0)) / 2 },
    .SINE_WAVE_FROM_1_TO_0: { (1 + cos(.pi * $0)) / 2 },
]

The disadvantage is that you are now responsible to update the dictionary if new function types are added.

A perhaps better alternative is to make the function a computed property of the function type enumeration:

enum FunctionType {
    case SINE_WAVE_FROM_0_TO_1_TO_0
    case SINE_WAVE_FROM_0_TO_1
    case SINE_WAVE_FROM_1_TO_0

    var f: (Double) -> Double {
        switch self {
        case .SINE_WAVE_FROM_0_TO_1_TO_0: return { (1 - cos(2 * .pi * $0)) / 2 }
        case .SINE_WAVE_FROM_0_TO_1: return { (1 - cos(.pi * $0)) / 2 }
        case .SINE_WAVE_FROM_1_TO_0: return { (1 + cos(.pi * $0)) / 2 }
        }
    }
}

Now everything is in “one place” and the compiler can check the exhaustiveness of the switch statement.

A disadvantage of all the above definitions is that they can not be extended: There is no way that a user can define its own interpolation function and pass it to the value animator.

So what I would really do is to define a struct as the function wrapper, with static properties for predefined functions. I am calling it Interpolator now (resembling the Android TimeInterpolator).

class ValueAnimator {

    struct Interpolator {
        // A function mapping [0, 1] to [0, 1].
        let f: (Double) -> Double

        // Predefined interpolation functions
        static let sineWaveFrom0To1To0 = Interpolator(f: { (1 - cos(2 * .pi * $0)) / 2 } )
        static let sineWaveFrom0To1 = Interpolator(f: { (1 - cos(.pi * $0)) / 2 } )
        static let sineWaveFrom1To0 = Interpolator(f: { (1 + cos(.pi * $0)) / 2 } )
    }

    // ...
}

Note also that I switched to lower camel case for the property names, which is the standard for all Swift identifiers except for types.

Now a user can easily add more interpolation functions, e.g.

extension ValueAnimator.Interpolator {
    static let linear = ValueAnimator.Interpolator(f: { $0 } )
}

Is there a way of accomplishing the same functionality ... but without using a selector? ... I ask because of the nasty "MyNSDoubleObject" workaround.

First: The wrapper object is not needed even if you use selectors: Swift automatically wraps values in Objective-C compatible types if necessary. Which means that you can pass a floating point value through the selector:

let value: Double = ...
target.perform(selector, with: value)

and the receive can (conditionally) cast it back to a Double:

@objc func animate(obj: AnyObject) {
    guard let value = obj as? Double else { return }
    print(value)
}

So the MyNSDoubleObject workaround is not needed.

But it becomes much simpler if you replace the target/selector method by a simpler callback with a closure. Similarly for the local timer: With a block-based timer, the timer callback need not be Objective-C compatible, and the ValueAnimator class does not have to subclass NSObject anymore.


Some minor remarks: The

private let sampleRate: Int

property is not needed. Instead of initializing with an (inactive) timer

private var timer = Timer()

I would use an optional:

private var timer: Timer?

X_currentRepIndex and F_of_X to not follow the Swift naming conventions, the latter name is quite non-descriptive.


Putting it all together, the ValueAnimator class could look like this:

class ValueAnimator {

    struct Interpolator {
        // A function mapping [0, 1] to [0, 1].
        let f: (Double) -> Double

        // Predefined interpolation functions
        static let sineWaveFrom0To1To0 = Interpolator(f: { (1 - cos(2 * .pi * $0)) / 2 } )
        static let sineWaveFrom0To1 = Interpolator(f: { (1 - cos(.pi * $0)) / 2 } )
        static let sineWaveFrom1To0 = Interpolator(f: { (1 + cos(.pi * $0)) / 2 } )
    }

    // args
    private let interpolation: Interpolator
    private let callback: (Double) -> Void

    // computed from args
    private let maxIterations: Int
    private let timeInterval: Double
    private var currentIteration: Int = 0

    // Other properties
    private var timer: Timer?

    init(durationInSeconds: Int, sampleRate: Int, interpolation: Interpolator,
         callback: @escaping (Double) -> Void) {
        self.maxIterations = durationInSeconds * sampleRate
        self.timeInterval = 1.0 / Double(sampleRate)
        self.interpolation = interpolation
        self.callback = callback
    }

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { (timer) in
            let val = Double(self.currentIteration)/Double(self.maxIterations)
            self.callback(self.interpolation.f(val))
            self.currentIteration += 1
            if self.currentIteration > self.maxIterations {
                self.stop()
            }
        }
    }

    func stop() {
        timer?.invalidate()
        timer = nil
    }
}

and a sample usage could look like this:

class ViewController: ViewController {

    var valueAnimator: ValueAnimator?

    override func viewDidLoad() {
        valueAnimator = ValueAnimator(durationInSeconds: 2, sampleRate: 2,
                                      interpolation: .sineWaveFrom0To1To0)
        { [weak self] value in
            guard let self = self else { return }

            // Do something with value ...
        }
        valueAnimator?.start()
    }

}

Note the use of weak self in the closure to avoid a reference cycle.


Further thoughts:

  • Since ValueAnimator is a “pure Swift” class now it can be made generic to support other value types, such as integers.

  • Finally: If the goal is to animate visual elements: don't reinvent the wheel, use Core Animation.

\$\endgroup\$
3
  • \$\begingroup\$ Appreciate your answer. Thanks for taking the time! Learned a lot there. \$\endgroup\$
    – user177754
    Commented Nov 22, 2019 at 22:20
  • \$\begingroup\$ This is my first codereview. If I were to upload this to GitHub would that be bad etiquette / frowned upon? It seems weird. \$\endgroup\$
    – user177754
    Commented Nov 25, 2019 at 6:35
  • \$\begingroup\$ @BooberBunz: I don't see a problem there. A link to the Stack Exchange license terms is at the bottom of each page. I am not a lawyer and not a license expert, but as I understand it, you can republish any content as long as you properly attribute it. \$\endgroup\$
    – Martin R
    Commented Nov 25, 2019 at 7:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.