今天Swift练习的是准备尝试把UICountingLabel这个Github Star数超过500的库用Swift重写一遍。
UICountingLabel源码解析
整个uICountingLabel的本质就是基于一个NSTimer来计算当前时间内应该显示什么值,
- (CGFloat)currentValue {
if (self.progress >= self.totalTime) {
return self.destinationValue;
}
CGFloat percent = self.progress / self.totalTime;
CGFloat updateVal = [self.counter update:percent];
return self.startingValue + (updateVal * (self.destinationValue - self.startingValue));
}
这个值通过不同的插值方式得到,这个插值方式的具体实现时通过self.counter的不同子类进行实现。
@implementation UILabelCounterLinear
-(CGFloat)update:(CGFloat)t
{
return t;
}
@end
@implementation UILabelCounterEaseIn
-(CGFloat)update:(CGFloat)t
{
return powf(t, kUILabelCounterRate);
}
@end
@implementation UILabelCounterEaseOut
-(CGFloat)update:(CGFloat)t{
return 1.0-powf((1.0-t), kUILabelCounterRate);
}
@end
@implementation UILabelCounterEaseInOut
-(CGFloat) update: (CGFloat) t
{
int sign =1;
int r = (int) kUILabelCounterRate;
if (r % 2 == 0)
sign = -1;
t *= 2;
if (t < 1)
return 0.5f * powf(t, kUILabelCounterRate);
else
return sign * 0.5f * (powf(t-2, kUILabelCounterRate) + sign * 2);
}
这里用子类化这么说是不严谨的,因为这其实就类似于Java或者C#是面向了接口编程了而已,因为这里的每个类都只是实现了update这个函数接口而已。
然后实现里相对比较直观,
-(void)countFrom:(CGFloat)startValue to:(CGFloat)endValue withDuration:(NSTimeInterval)duration {
if (duration == 0.0) {
// No animation
[self setTextValue:endValue];
[self runCompletionBlock];
return;
}
self.lastUpdate = [NSDate timeIntervalSinceReferenceDate];
switch(self.method)
{
case UILabelCountingMethodLinear:
self.counter = [[UILabelCounterLinear alloc] init];
break;
case UILabelCountingMethodEaseIn:
self.counter = [[UILabelCounterEaseIn alloc] init];
break;
case UILabelCountingMethodEaseOut:
self.counter = [[UILabelCounterEaseOut alloc] init];
break;
case UILabelCountingMethodEaseInOut:
self.counter = [[UILabelCounterEaseInOut alloc] init];
break;
}
NSTimer *timer = [NSTimer timerWithTimeInterval:(1.0f/30.0f) target:self selector:@selector(updateValue:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
self.timer = timer;
}
首先如果动画的时间是0,就默认直接回调。但是,我想说duration == 0.0,浮点数这么判断,真的没问题吗?
由于NSTimer无法得知确切得知道执行了多少,所以这里要记录上一步回调的lastUpdate。
根据不同的方法选择不同的Counter进行插值,然后创建NSTimer,这里我们要特别注意两句话:
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
这表示NSTimer默认加入的是RunLoop的Default Mode,但是Default Mode在进入Tracking Mode的时候(也就是当用户滑动的时候)会被阻塞,影响动画的执行。因此要特别加入TrackingMode。
但是我又想说了,NSRunLoopCommonModes难道不是包含UITrackingRunLoopMode吗?天呐,还是我理解的不对,重复添加的意义呢!!!!
然后在NSTimer的回调函数中,
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
self.progress += now - self.lastUpdate;
self.lastUpdate = now;
if (self.progress >= self.totalTime) {
[self.timer invalidate];
self.timer = nil;
self.progress = self.totalTime;
}
[self setTextValue:[self currentValue]];
if (self.progress == self.totalTime) {
[self runCompletionBlock];
}
就是计算当前的progress进度,更新相应的值而已,没有难度。
Swift 重写
先来效果:
CADisplayLink vs NSTimer
首先我们来回忆下Objective-C版本中的一些逻辑,使用NSTimer动态根据进度更新时间。但是我们知道,NSTimer有一个很大的问题就是他的“刷新率”不够准确。如果NSTimer设置了每两秒更新一次,那么如果RunLoop中有个耗时的任务,就会讲这个更新任务推迟,用时间轴来理解:
0 -> 2s (NSTimer回调) -> 3s (耗时的任务) -> 2s (NSTimer回调)。
那么这个会导致什么问题呢?NSTimer的回调和屏幕刷新不是同步的。因此,在Swift版本的重写中,我采用了CADisplayLink来改写,它的好处我们同样用时间轴来理解:
0 -> 2s(CADisplayLink回调) -> 3s(耗时的任务) -> 1s(空闲) -> 2s(CADisplayLink回调)
可以看出,CADisplayLink始终是保持和屏幕刷新率一样。
这里强调一下,不论是NSTimer抑或是CADisplayLink,既然他们都是基于RunLoop的,就无法脱离被所处同一个RunLoop里的其他任务所影响的宿命,所以网上那些说CADisplayLink不会被阻塞的说法都是错误的。
Swift 面向接口编程
之前我们提过“子类化“这个说法是不准确的,因此,我们在Swift中,可以基于protocol-oriented进行编程。
let WZCountRate:Float = 3.0
protocol Interpolation {
func update(val:Float) -> Float;
}
struct Linear:Interpolation {
func update(val: Float) -> Float {
return val
}
}
struct EaseIn:Interpolation {
func update(val: Float) -> Float {
return powf(val, WZCountRate)
}
}
struct EaseOut:Interpolation {
func update(val: Float) -> Float {
return 1 - powf((1 - val), WZCountRate)
}
}
struct EaseInOut:Interpolation {
func update(val: Float) -> Float {
var sign:Float = 1
let r = Int(WZCountRate)
if (r % 2 == 0) {
sign = -1
}
var t = val * 2
if (t < 1) {
return 0.5 * powf(t, WZCountRate)
} else {
return 0.5 * (powf(t - 2.0, WZCountRate) + sign * 2) * sign
}
}
}
可以看到,我们用四个Struct结构体遵从了Interpolation这个接口,实现了四种不同的插值方法。
别的方面没什么特别难的,
Access Control 对Selector Callback的影响
我们都知道,Swift对类引入了Access Control这一机制,默认情况下是internal的权限。由于CADisplayLink还是基于Selector设置回调的,如下:
let displayLink = CADisplayLink(
target: self,
selector: Selector("displayTick:")
)
当这个displayTick函数处于public或者internal权限的时候,没有任何问题。而当你想声明如
private func displayTick()
的时候,就会产生doesn’t recognize selector的runtime error。
原因如下:
基于Selector的回调方式还是采用了Objective-C传统的runtime 查函数表的方式,但是在Swift中声明为private的函数,对于Objective-C Runtime是不可见的。因此,如果你需要让私有函数可以被查询到,你需要添加@objc关键词。
@objc private func displayTick()
当然啦, 对于IBOutlets, IBActions 以及 Core Data 相关属性来说,默认是可以被Objective-C Runtime查询到的。
最后,附上项目地址