

DGRunKeeperSwitch 源码解析
source link: http://satanwoo.github.io/2016/02/05/DGRunKeeperSwitch/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

DGRunKeeperSwitch 源码解析
DGRunKeeperSwitch是非常有趣的自定义的Segment Control的实现,从其Github上的展现效果来看,可以发现在 同一个 UILabel中的文本竟然可以展现出两种不同的颜色,是不是很奇妙?今天就让我们来看看它是如何实现的。

打开项目,发现这个项目真的很简单,就一个文件,DGRunkeeperSwitch.swift
,并且实现也只有接近260行左右。
既然这个项目是个UI的开源库,我们主要还是先从界面层级入手。和Glow的开源库(GLCalendar)不同,这个是纯手写的控件,因此无法从.xib文件来快速了解,所以我们把目标首先投向相关的UIKit子属性,包括如下:
// 1.
private var titleLabelsContentView = UIView()
private var leftTitleLabel = UILabel()
private var rightTitleLabel = UILabel()
// 2.
private var selectedTitleLabelsContentView = UIView()
private var selectedLeftTitleLabel = UILabel()
private var selectedRightTitleLabel = UILabel()
// 3.
private(set) var selectedBackgroundView = UIView()
private var titleMaskView: UIView = UIView()
其中第一部分我们一看命名就很容易理解了,有一个ContentView
作为container
,包含了segment control
对应的左右两个Label。
然后来看第二部分,第二部分从命名上也很直观,感觉上和第一部分是一致的,但是却可能代表的是选中的状态。不过我们很奇怪,作者为什么要构建一个一模一样的来表征不同的状态呢,直接用一个变量比如 var selected = false
进行样式的控制不可以吗?
好,先别急,这里卖个关子,我们继续往下看。
第三部分,selectedBackgroundView
和titleMaskView
,从名字看,也不能一下子了解含义,我们先全局搜索下相关连的代码,与titleMaskView
相关的内容如下:
titleMaskView.backgroundColor = .blackColor()
selectedTitleLabelsContentView.layer.mask = titleMaskView.layer
看起来是用titleMaskView
给之前可能的选中状态的selectedTitleLabelsContentView
加了一层遮罩。
由于遮罩是白色的地方不显示,黑色的地方(准确来说是非白色的区域)显示,因此我们可以理解上述代码是通过titleMaskView
来显示selectedTitleLabelsContentView
中的内容(也就是两个UILabel),非titleMaskView
区域自动隐藏了。
addObserver(self, forKeyPath: "selectedBackgroundView.frame", options: .New, context: nil)
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "selectedBackgroundView.frame" {
titleMaskView.frame = selectedBackgroundView.frame
}
}
哦,看完上述这段代码,我开始有点恍然大悟了,通过监听selectedBackgroundView.frame
,我们实时改变titleMaskView
的frame
。而通过实际运行项目,我们可以很容易理解selectedBackgroundView
就是用户可拖拽的选项高亮条。
到这,我渐渐有点理解作者为什么要构建两个完全一样的contentView,并都包含左右两个UILabel了。
作者应该是对于titleLabelsContentView
设定为普通状态的Label,左右两个Label都是未选中的颜色状态,同时将selectedTitleLabelsContentView
设定为选定状态,左右Label都使用了选中时候的颜色状态,然后通过titleMaskView
进行遮罩,这样,selectedTitleLabelsContentView
其余部分就被隐藏,会显示出下部titleLabelsContentView
普通状态的Label颜色。
嘿嘿,读一下剩下的源代码,和我的猜测一致,不得不说,我真是太聪明了,这个思路真是太赞了。
如何真正实现一个好的UI库
看到这个小标题,可能有人会产生疑惑,实现好一个UI库不就是功能正确,效果正常吗?错!
我认为这只是基本的两点,还有如下几点需要包含:
- 使用正确的类型
- 在正确的函数中做正确的事
- 暴露不过多也不过少的属性
- 抛出、监听相对应的事件
- 根据不同屏幕大小、屏幕方向进行适配
- 横竖屏情况都能展示良好
第一,从DGRunkeeperSwitch来看,首先由于其模仿的是UISegmentControl,所以自然而然的应该继承与UIControl而不是UIView。有人要问有啥区别,简单来说就是UIControl将UIView中能接受的Touch事件,转换成了更高级的UIEvent,比如UITouchUpInside。
第二,作者通过init函数进行初始化,通过layoutSubview进行页面布局,而不是像很多人自己写代码时将很多东西一窝蜂的堆到了init中。
提供了颜色、字体、边距以及动画弹性等属性给外部调用,同时将不应该暴露的内部UIKit变量进行私有化,并将
selectedIndex
通过private(set)
对外设置为只读。在切换Segment选择后,抛出了相应的
sendActionsForControlEvents(.ValueChanged)
用于给外部监听。
效果之外的重点
作者在实现这个项目之中,有几点是比较值得注意的:
利用元组同时赋值多个属性
public var leftTitle: String {
set { (leftTitleLabel.text, selectedLeftTitleLabel.text) = (newValue, newValue) }
get { return leftTitleLabel.text! }
}
在Swift中引入了一个元组的新类型,我们可以利用这个数据结构同时给多个属性赋值。
private(set)
private(set) public var selectedIndex: Int = 0
作者在实现过程中保留了一个selectedIndex
变量,但是这个类对外只读,对内可以读写,因此用了private(set)。
这相当于在Objective-C时代,我们在.h文件中声明 @property(nonatomic, strong, readonly) Class *A
然后又在.m文件中,声明 @property(nonatomic, strong, readwrite) Class *A
UIView和CALayer
很多人写iOS的时候,分不清UIView和CALayer之间的区别,很多人都理解成了继承的关系。大错特错!
- 实际上UIView里面有个成员变量是CALayer,而CALayer的delegate是UIView(这会涉及到很多的隐式动画之类的,不展开了)
- UIView可以接受Touch事件,而Layer不行
- UIView有个layerClass的类型方法,可以被复写,用于改变这个UIView对应的基础Layer类型,比如你可以将赋值CAGradientLayer给这个View
在本项目中,作者复写了layerClass,如下:
override public class func layerClass() -> AnyClass {
return DGRunkeeperSwitchRoundedLayer.self
}
好啦,今天就差不多到这啦~下周再见。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK