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
}
}
1 Answer 1
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.
-
\$\begingroup\$ Appreciate your answer. Thanks for taking the time! Learned a lot there. \$\endgroup\$user177754– user1777542019年11月22日 22:20:42 +00:00Commented 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– user1777542019年11月25日 06:35:46 +00:00Commented 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– Martin R2019年11月25日 07:59:26 +00:00Commented Nov 25, 2019 at 7:59