本文译自:How To Make a Custom Control in Swift

用户界面控件是所有应用程序重要的组成部分之一。它们以图形组件的方式呈现给用户,用户可以通过它们与应用程序进行交互。苹果提供了一套控件,例如 UITextFieldUIButtonUISwitch。通过工具箱中的这些已有控件,我们可以创建各式各样的用户界面。

然而,有时候你希望界面做得稍微的与众不同,那么此时苹果提供的这些控件就无法满足你的需求。

自定义控件,除了是自己构建二外,与苹果提供的,没什么差别。也就是说,自定义控件不存在于 UIKit 框架。自定义控件跟苹果提供的标准控件一样,应该是通用,并且多功能的。你也会发现,互联网上有一些积极的开发者乐意分享他们自定义的控件。

本文中,你将实现一个自己的 RangeSlider 自定义控件。这个控件是一个两端都可以滑动的,也就是说,你可以通过该控件获得最小值和最大值。你将会接触到这样一些概念:对现有控件的扩展,设计和实现 自定义控件的 API,甚至还能学到如何分享你的自定义控件到开发社区中。

注意:本文截稿时,我们还不会贴出关于 iOS 8 beta 版本的截图。所有文中涉及到的截图都是在iOS 8之前的版本中得到的,不过结果非常类似。

目录:

  • 开始
  • Images vs. CoreGraphics
  • 添加默认的控件属性
  • 添加交互逻辑
  • 添加触摸处理
  • 值改变的通知
  • 结合 Core Graphics 对控件进行修改
  • 处理控件属性的改变
  • 何去何从?

开始

假设你在开发一个应用程序,该程序提供搜索商品价格列表。通过这个假象的应用程序允许用户对搜索结果进行过滤,以获得一定价格范围的商品。你可能会提供这样一个用户界面:两个 UISlider 控件,一个用于设置最低价格,另外一个设置最高价格。然而,这样的设计,不能够让用户很好的感知价格的范围。要是能够提供一个 slider,两端可以分别设置用于搜索的最高和最低的价格范围,就更好了。

你可以通过创建一个 UIView 的子类,然后为可视的价格范围定做一个 view。这对于应用程序内部来说,是 ok的,但是要想移植到别的程序中,就需要花更多的精力了。

最好的办法是将构建一个新的尽可能通用的 UI 控件,这样就能在任意的合适场合中重用。这也是自定义控件的本质。

启动 Xcode,File/New/Project,选中 iOS/Application/Single View Application 模板,然后点击 Next。在接下来的界面中,输入 CustomSliderExample 当做工程名,然后是 Organization NameOrganization Identifier,然后,一定要确保选中 Swift 语言,iPhone 选中,Use Core Data 不要选。

最后,选择一个保存工程的地方并单击 Create

首先,我们需要做出决定的就是创建自定义控件需要继承自哪个类,或者对哪个类进行扩展。

位了使自定义控件能够在应用程序中使用,你的类必须是 UIView 的一个子类。

如果你注意观察苹果的 UIKit 参考,会发现框架中的许多控件,例如 UILabelUIWebView 都是直接继承自 UIView 的。然而,也有极少数,例如 UIButtonUISwitch 是继承自 UIControl 的,如下继承图所示:

注意:iOS 中 UI 组件的完整类继承图,请看 UIKit Framework 参考。

UIControl 实现了 target-action 模式,这是一种将变化通知订阅者的机制。UIControl 同样还有一些与控件状态相关的属性。在本文中的自定义空间中,将使用到 target-action 模式,所以从 UIControl 开始继承使用将是一个非常好的切入点。

在 Project Navigator 中右键单击 CustomSliderExample,选择 New File…,然后选择 iOS/Source/Cocoa Touch Class 模板,并单击 Next。将类命名位 RangeSlider,在 Subclass of 字段中输入 UIControl,并确保语言是 Swift。然后单击 Next,并在默认存储位置中 Create 出新的类。

虽然编码非常让人愉悦,不过你可能也希望尽快看到自定义控件在屏幕中熏染出来的模样!在写自定义控件相关的任何代码之前,你应该先把这个控件添加到 view controller中,这样就可以实时观察控件的演进程度。

打开 ViewController.swift,用下面的内容替换之:

 1 import UIKit
 2
 3 class ViewController: UIViewController {
 4 let rangeSlider = RangeSlider(frame: CGRectZero)
 5
 6 override func viewDidLoad() {
 7 super.viewDidLoad()
 8
 9 rangeSlider.backgroundColor = UIColor.redColor()
10 view.addSubview(rangeSlider)
11 }
12
13 override func viewDidLayoutSubviews() {
14 let margin: CGFloat = 20.0
15 let width = view.bounds.width - 2.0 * margin
16 rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length,
17 width: width, height: 31.0)
18 }
19 }

上面的代码根据指定的 frame 实例化了一个全新的控件,然后将其添加到 view 中。为了在应用程序背景中凸显出控件,我们将控件的背景色被设置位了红色。如果不把控件的背景色设置为红色,那么控件中什么都没有,可能会想,控件去哪里了!:]

编译并运行程序,将看到如下类似界面:

在开始给控件添加可视元素之前,应该先定义几个属性,用以在控件中记录下各种信息。这也是开始应用程序编程接口 (API) 的开始。

注意:控件中定义的方法和属性是你决定用来暴露给别的开发者使用的。稍后你将看到 API 设计相关的内容,现在只需要紧跟就行!

添加默认的控件属性

打开 RangeSlider.swift,用下面的代码替换之:

1 import UIKit
2
3 class RangeSlider: UIControl {
4 var minimumValue = 0.0
5 var maximumValue = 1.0
6 var lowerValue = 0.2
7 var upperValue = 0.8
8 }

上面定义的四个属性用来描述控件的状态,提供最大值和最小值,以及有用户设置的 upper 和 lower 两个值。

好的控件设计,应该提供一些默认的属性值,否则将你的控件绘制到屏幕中时,看起来会有点奇怪。

现在是时候开始做控件的交互元素了,我们分别用两个 thumbs 表示高和低两个值,并且让这两个 thumbs 能够滑动。

Images vs. CoreGraphics

在屏幕中渲染控件有两种方法:

1、Images - 为控件构建不同的图片,这些图片代表控件的各种元素。
2、Core Graphics - 利用 layers 和 Core Graphics 组合起来熏染控件。

这两种方法都有利有弊,下面来看看:

Images - 利用图片来构建控件是最简单的一种方法 - 只要你知道如何绘制图片!:] 如果你想要让开发者能够修改控件的外观,那么你应该将这些图片以 UIImage 属性的方式暴露出去。

通过图片的方式来构建的控件,给使用控件的人提供了非常大的灵活度。开发者可以改变每一个像素,以及控件的详细外观,不过这需要非常熟练的图形设计技能 - 并且通过代码非常难以对控件做出修改。

Core Graphics - 利用 Core Graphics 构建控件意味着你必须自己编写渲染控件的代码,这就需要付出更多的代价。不过,这种方法可以创建更加灵活的 API。

使用 Core Graphics,可以把控件的所有特征都参数化,例如颜色、边框厚度和弧度 - 几乎每一个可视元素都通过绘制完成!这种方法运行开发者对控件做出任意调整,以适配相应的需求。

本文中,你将学到第二种技术 - 利用 Core Graphics 来熏染控件。

主要:有趣的时,苹果建议在他们提供的控件中使用图片。这可能是苹果知道每个控件的大小,他们不希望程序中出现太多的定制。也就是说,他们希望所有的应用程序,都具有相似的外观和体验。

打开 RangeSlider.swift 将下面的 import 添加到文件的顶部,也就是 import UIKit 下面:

1 import QuartzCore

将下面的属性添加到 RangeSlider 中,也就是我们刚刚定义的那行代码下面:

1 let trackLayer = CALayer()
2 let lowerThumbLayer = CALayer()
3 let upperThumbLayer = CALayer()
4
5 var thumbWidth: CGFloat {
6 return CGFloat(bounds.height)
7 }

这里有 3 个 layer - trackLayer, lowerThumbLayer, 和 upperThumbLayer - 用来熏染滑块控件的不同组件。thumbWidth 用来布局使用。

接下来就是控件默认的一些图形属性。

RangeSlider 类中,添加一个 初始化方法,以及一个 helper 方法:

 1 override init(frame: CGRect) {
 2 super.init(frame: frame)
 3
 4 trackLayer.backgroundColor = UIColor.blueColor().CGColor
 5 layer.addSublayer(trackLayer)
 6
 7 lowerThumbLayer.backgroundColor = UIColor.greenColor().CGColor
 8 layer.addSublayer(lowerThumbLayer)
 9
10 upperThumbLayer.backgroundColor = UIColor.greenColor().CGColor
11 layer.addSublayer(upperThumbLayer)
12
13 updateLayerFrames()
14 }
15
16 required init(coder: NSCoder) {
17 super.init(coder: coder)
18 }
19
20 func updateLayerFrames() {
21 trackLayer.frame = bounds.rectByInsetting(dx: 0.0, dy: bounds.height / 3)
22 trackLayer.setNeedsDisplay()
23
24 let lowerThumbCenter = CGFloat(positionForValue(lowerValue))
25
26 lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,
27 width: thumbWidth, height: thumbWidth)
28 lowerThumbLayer.setNeedsDisplay()
29
30 let upperThumbCenter = CGFloat(positionForValue(upperValue))
31 upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,
32 width: thumbWidth, height: thumbWidth)
33 upperThumbLayer.setNeedsDisplay()
34 }
35
36 func positionForValue(value: Double) -> Double {
37 let widthDouble = Double(thumbWidth)
38 return Double(bounds.width - thumbWidth) * (value - minimumValue) /
39 (maximumValue - minimumValue) + Double(thumbWidth / 2.0)
40 }

初始化方法简单的创建了 3 个 layer,并将它们以 children 的身份添加到控件的 root layer 中,然后通过 updateLayerFrames 对这些 layer 的位置进行更新定位! :]

最后,positionForValue 方法利用一个简单的比例,对控件的最小和最大值的范围做了一个缩放,将值映射到屏幕中确定的一个位置。

接下来,override一下 frame,通过将下面的代码添加到 RangeSlider.swift 中,实现对属性的观察:

1 override var frame: CGRect { 2 didSet { 3 updateLayerFrames() 4 } 5 }

当 frame 发生变化时,属性观察者会更新 layer frame。这一步是必须的,因为当控件初始化时,传入的 frame 并不是最终的 frame,就像 ViewController.swift 中的。

编译并运行程序,可以看到滑块初具形状!看起来,如下图所示:

还记得吗,红色是整个控件的背景色。蓝色是滑块的轨迹,绿色 thumb 是两个代表两端的值。

现在控件看起来有形状了,不过几乎所有的控件都提供了相关方法,让用户与之交互。

针对本文中的控件,用户必须能够通过拖拽 2 个 thumb 来设置控件的范围。你将处理这些交互,并通过控件更新 UI 和暴露的属性。

添加交互逻辑

本文的交互逻辑需要存储那个 thumb 被拖拽了,并将效果反应到 UI 中。控件的 layer 是放置该逻辑的最佳位置。

跟之前一样,在 Xcode 中创建一个新的 Cocoa Touch Class,命名为 RangeSliderThumbLayer,继承自 CALayer

用下面的代码替换掉 RangeSliderThumbLayer.swift 文件中的内容:

1 import UIKit
2 import QuartzCore
3
4 class RangeSliderThumbLayer: CALayer {
5 var highlighted = false
6 weak var rangeSlider: RangeSlider?
7 }

上面的代码中简单的添加了两个属性:一个表示这个 thumb 是否 高亮 (highlighted),另外一个引用回父 range slider。由于 RangeSlider 有两个 thumb layer,所以将这里的引用设置位 weak,避免循环引用。

打开 RangeSlider.swift,修改一下 lowerThumbLayerupperThumbLayer 两个属性的类型,用下面的代码替换掉它们的定义:

1 let lowerThumbLayer = RangeSliderThumbLayer() 2 let upperThumbLayer = RangeSliderThumbLayer()

还是在 RangeSlider.swift 中,找到 init,将下面的代码添加进去:

1 lowerThumbLayer.rangeSlider = self 2 upperThumbLayer.rangeSlider = self

上面的代码简单的将 layer 的 rangeSlider 属性设置为 self

编译并运行程序,界面看起来没有什么变化。

现在你已经有了 slider 的thumb layer - RangeSliderThumbLayer,然后需要给控件添加拖拽 thumb 的功能。

添加触摸处理

打开 RangeSlider.swift,将下面这个属性添加进去:

1 var previousLocation = CGPoint()

这个属性用来跟踪记录用户的触摸位置。

那么你该如何来跟踪控件的各种触摸和 release 时间呢?

UIControl 提供了一些方法来跟踪触摸。UIControl 的子类可以 override 这些方法,以实现自己的交互逻辑。

在自定义控件中,我们将 override 3 个 UIControl 关键的方法:beginTrackingWithTouch, continueTrackingWithTouchendTrackingWithTouch

将下面的方法添加到 RangeSlider.swift 中:

 1 override func beginTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) -> Bool {
 2 previousLocation = touch.locationInView(self)
 3
 4 // Hit test the thumb layers
 5 if lowerThumbLayer.frame.contains(previousLocation) {
 6 lowerThumbLayer.highlighted = true
 7 } else if upperThumbLayer.frame.contains(previousLocation) {
 8 upperThumbLayer.highlighted = true
 9 }
10
11 return lowerThumbLayer.highlighted || upperThumbLayer.highlighted
12 }

当首次触摸控件时,会调用上面的方法。

代码中,首先将触摸事件的坐标转换到控件的坐标空间。然后检查每个 thumb,是否触摸位置在其上面。方法中返回的值将决定 UIControl 是否继续跟踪触摸事件。

如果任意一个 thumb 被 highlighted 了,就继续跟踪触摸事件。

现在,有了初始的触摸事件,我们需要处理用户在屏幕上移动的事件了。

将下面的方法添加到 RangeSlider.swift 中:

 1 func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {
 2 return min(max(value, lowerValue), upperValue)
 3 }
 4
 5 override func continueTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) -> Bool {
 6 let location = touch.locationInView(self)
 7
 8 // 1. Determine by how much the user has dragged
 9 let deltaLocation = Double(location.x - previousLocation.x)
10 let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height)
11
12 previousLocation = location
13
14 // 2. Update the values
15 if lowerThumbLayer.highlighted {
16 lowerValue += deltaValue
17 lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue)
18 } else if upperThumbLayer.highlighted {
19 upperValue += deltaValue
20 upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue)
21 }
22
23 // 3. Update the UI
24 CATransaction.begin()
25 CATransaction.setDisableActions(true)
26
27 updateLayerFrames()
28
29 CATransaction.commit()
30
31 return true
32 }

boundValue 会将传入的值控制在某个确定的范围。通过这个方法比嵌套调用 min/max 更容易理解。

下面我们根据注释,来分析一下 continueTrackingWithTouch 方法都做了些什么:

  1. 首先计算出位置增量,这个值决定着用户手指移动的数值。然后根据控件的最大值和最小值,对这个增量做转换。
  2. 根据用户滑动滑块的距离,修正一下 upper 或 lower 值。
  3. 设置 CATransaction 中的 disabledActions。这样可以确保每个 layer 的frame 立即得到更新,并且不会有动画效果。最后,调用 updateLayerFrames 方法将 thumb 移动到正确的位置。

至此,已经编写了移动滑块的代码 - 不过我们还要处理触摸和拖拽事件的结束。

将下面方法添加到 RangeSlider.swift 中:

1 override func endTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) {
2 lowerThumbLayer.highlighted = false
3 upperThumbLayer.highlighted = false
4 }

上面的代码简单的将两个 thumb 还原位 non-highlighted 状态。

编译并运行程序,尝试移动滑块!现在你应该可以移动 thumb 了。

你可能注意到当在移动滑块时,可以在控件之外的范围对其拖拽,然后手指回到控件内,也不会丢失跟踪。其实这在小屏幕的设备上,是非常重要的一个功能。

值改变的通知

现在你已经有一个可以交互的控件了 - 用户可以对其进行操作,以设置范围的大小值。但是如何才能把这些值的改变通知调用者:控件有新的值了呢?

这里有多种模式可以实现值改变的通知: NSNotificationKey-Value-Observing (KVO), delegate 模式,target-action 模式等。有许多选择!

面对这么多的通知方式,那么我们该怎么选择呢?

如果你研究过 UIKit 控件,会发现它们并没有使用 NSNotification,也不鼓励使用 KVO。所以为了保持与 UIKit 的一致性,我们可以先排除这两种方法。另外的两种模式:delegate 和 target-action 被广泛用于 UIKit 中。

Delegate 模式 - delegate 模式需要提供一个 protocol,里面有一些用于通知的方法。控件中有一个属性,一般命名位 delegate,它可以是任意实现该协议的类。经典的一个示例就是 UITableView 提供了 UITableViewDelegate protocol。注意,控件只接受单个 delegate 实例。一个 delegate 方法可以使用任意的参数,所以可以给这样的方法传递尽可能多的信息。

Target-action 模式 - UIControl 基类已经提供了 target-action 模式。当控件状态发生了改变,target 会获得相应 action 的通知,该 action 是在 UIControlEvents 枚举值做定义的。我们可以给控件的 action 提供多个 target,另外还可以创建自定义事件 (查阅 UIControlEventApplicationReserved),自定义事件的数量不得超过 4 个。控件 action 针对某个事件,无法传送任意的信息,所以当事件触发时,不能用它来传递额外的信息。

这两种模式关键不同点如下:

  • 多播 (Multicast) - target-action 模式可以对改变事件进行多播通知,而 delegate 模式只能绑定到单个 delegate 实例上。
  • 灵活 (Flexibility) - 在 delegate 模式中,你可以定义自己的 protocol,这就意味着你可以控制信息的传递量。而 target-action 是无法传递额外信息的,客户端只能在收到事件后,自行查询信息。

我们的 slider 控件不会有大量的状态变化,也不需要提供大量的通知。唯一真正改变的就是控件的 upper 和 lower 值。

基于这样的情况,使用 target-action 模式是最好的。这也是为什么在本文开头的时候告诉你为什么这个控件要继承自 UIControl

slider 的值是在 continueTrackingWithTouch:withEvent: 方法中进行更新的,所以这个方法也是添加通知代码的地方。

打开 RangeSlider.swift,定位到 continueTrackingWithTouch 方法,然后将下面的代码添加到 return true 语句前面:

1 sendActionsForControlEvents(.ValueChanged)

上面的这行代码就能将值改变事件通知给任意的订阅者 target。

现在我们应该对这个事件进行订阅,并当事件来了以后,作出相应的处理。

打开 ViewController.swift,将下面这行代码添加到 viewDidLoad 尾部:

1 rangeSlider.addTarget(self, action: "rangeSliderValueChanged:", forControlEvents: .ValueChanged)

通过上面的代码,每次 slider 发送 UIControlEventValueChanged action 时,都会调用 rangeSliderValueChanged 方法。

将下面的代码添加到 ViewController.swift 中:

1 func rangeSliderValueChanged(rangeSlider: RangeSlider) {
2 println("Range slider value changed: (\(rangeSlider.lowerValue) \(rangeSlider.upperValue))")
3 }

当 slider 值发生变化是,上面这个方法简单的将 slider 的值打印出来。

编译并运行程序,并移动一下 slider,可以在控制台中看到控件的值,如下所示:

1 Range slider value changed: (0.217687089658687 0.68610299780487)
2 Range slider value changed: (0.217687089658687 0.677356642119739)
3 Range slider value changed: (0.217687089658687 0.661807535688662)
4 Range slider value changed: (0.217687089658687 0.64625847374385)
5 Range slider value changed: (0.217687089658687 0.631681214268632)
6 Range slider value changed: (0.217687089658687 0.621963056113908)
7 Range slider value changed: (0.217687089658687 0.619047604218864)
8 Range slider value changed: (0.217687089658687 0.61613215232382)

看到 控件五颜六色的,你可能不高心,它开起来就像水果沙拉一样!

现在是时候给控件换换面目了!

结合 Core Graphics 对控件进行修改

首先,首选更新一下slider thumb 移动的轨迹图形。

跟之前一样,给工程添加另外一个继承自 CALayer 的子类,命名为 RangeSliderTrackLayer

打开刚刚添加的文件 RangeSliderTrackLayer.swift,然后用下面的内容替换之:

1 import UIKit
2 import QuartzCore
3
4 class RangeSliderTrackLayer: CALayer {
5 weak var rangeSlider: RangeSlider?
6 }

上面的代码添加了一个到 slider 控件的引用,跟之前 thumb layer 做的一样。

打开 RangeSlider.swift 文件,找到 trackLayer 属性,用刚刚创建的这个类对其实例化,如下所示:

1 let trackLayer = RangeSliderTrackLayer()

接下来,找到 init 并用下面的代码替换之:

 1 init(frame: CGRect) {
 2 super.init(frame: frame)
 3
 4 trackLayer.rangeSlider = self
 5 trackLayer.contentsScale = UIScreen.mainScreen().scale
 6 layer.addSublayer(trackLayer)
 7
 8 lowerThumbLayer.rangeSlider = self
 9 lowerThumbLayer.contentsScale = UIScreen.mainScreen().scale
10 layer.addSublayer(lowerThumbLayer)
11
12 upperThumbLayer.rangeSlider = self
13 upperThumbLayer.contentsScale = UIScreen.mainScreen().scale
14 layer.addSublayer(upperThumbLayer)
15 }

上面的代码确保新的 track layer 引用到 range slider - 并没有再用那可怕的颜色了!然后将 contentsScale 因子设置位与设备的屏幕一样,这样可以确保所有的内容在 retina 显示屏中没有问题。

下面还有一个事情需要做,就是将 viewDidLoad 中的如下代码移除掉:

1 rangeSlider.backgroundColor = UIColor.redColor()

编译并运行程序,看到什么了呢?

什么东西都没有?这是正确的!

不要烦恼 - 我们只不过移除掉了在 layer 中花哨的测试颜色。控件依旧存在 - 只不过现在是白色的!

由于许多开发者希望能够通过编码对控件做各种配置,以使其外观能够效仿一些流行的程序,所以我们给 slider 添加一些属性,运行开发者对其外观做出一些定制。

打开 RangeSlider.swift,将下面的属性添加到已有属性下面:

1 var trackTintColor = UIColor(white: 0.9, alpha: 1.0)
2 var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)
3 var thumbTintColor = UIColor.whiteColor()
4
5 var curvaceousness : CGFloat = 1.0

这些颜色属性的目的非常容易理解,但是 curvaceousness?这个属性在这里有点趣味 - 稍后你将发现其用途!

接下来,打来 RangeSliderTrackLayer.swift

这个 layer 用来渲染两个 thumb 滑动的轨迹。目前它继承自 CALayer,仅仅是绘制一个单一颜色。

为了绘制轨迹,需要实现方法 drawInContext:,并利用 Core Pgraphics APIs 来进行渲染。

注意:要想深入学习 Core Graphics,建议阅读 Core Graphics 101 教程。

将下面这个方法添加到 RangeSliderTrackLayer 中:

 1 override func drawInContext(ctx: CGContext!) {
 2 if let slider = rangeSlider {
 3 // Clip
 4 let cornerRadius = bounds.height * slider.curvaceousness / 2.0
 5 let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
 6 CGContextAddPath(ctx, path.CGPath)
 7
 8 // Fill the track
 9 CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor)
10 CGContextAddPath(ctx, path.CGPath)
11 CGContextFillPath(ctx)
12
13 // Fill the highlighted range
14 CGContextSetFillColorWithColor(ctx, slider.trackHighlightTintColor.CGColor)
15 let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue))
16 let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue))
17 let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
18 CGContextFillRect(ctx, rect)
19 }
20 }

一旦 track 形状确定,控件的背景色就会被填充,另外高亮范围也会被填充。

编译并运行程序,会看到新的 track layer 被完美的渲染出来!如下图所示:

给暴露出来的属性设置不同的值,观察一下它们是如何反应到控件渲染中的。

如果你对 curvaceousness 做什么的还存在疑惑,那么试着修改一下它看看!

接下来我们使用相同的方法来绘制 thumb layer。

打开 RangeSliderThumbLayer.swift,然后将下面的方法添加到属性声明的下方:

 1 override func drawInContext(ctx: CGContext!) {
 2 if let slider = rangeSlider {
 3 let thumbFrame = bounds.rectByInsetting(dx: 2.0, dy: 2.0)
 4 let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0
 5 let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)
 6
 7 // Fill - with a subtle shadow
 8 let shadowColor = UIColor.grayColor()
 9 CGContextSetShadowWithColor(ctx, CGSize(width: 0.0, height: 1.0), 1.0, shadowColor.CGColor)
10 CGContextSetFillColorWithColor(ctx, slider.thumbTintColor.CGColor)
11 CGContextAddPath(ctx, thumbPath.CGPath)
12 CGContextFillPath(ctx)
13
14 // Outline
15 CGContextSetStrokeColorWithColor(ctx, shadowColor.CGColor)
16 CGContextSetLineWidth(ctx, 0.5)
17 CGContextAddPath(ctx, thumbPath.CGPath)
18 CGContextStrokePath(ctx)
19
20 if highlighted {
21 CGContextSetFillColorWithColor(ctx, UIColor(white: 0.0, alpha: 0.1).CGColor)
22 CGContextAddPath(ctx, thumbPath.CGPath)
23 CGContextFillPath(ctx)
24 }
25 }
26 }

一旦定义好了 thumb 的形状路径,就会将其形状填充好。注意绘制微弱的阴影看起来的效果就是 thumb 上方的轨迹。接下来是绘制边框。最后,如果 thumb 是高亮的 - 也就是被移动状态 - 那么就绘制微弱的灰色阴影效果。

在运行之前,还有最后一件事情要做。按照下面的代码对 highlighted 属性的定义做出修改:

1 var highlighted: Bool = false { 2 didSet { 3 setNeedsDisplay() 4 } 5 }

这里,定义了一个属性观察者,这样当每次 highlighted 属性修改时,相应的 layer 都会得到重绘。这会使得触摸事件发生时,填充色发生轻微的变动。

再次编译并运行程序,这下看起来会非常的有形状,如下图所示:

不难发现,用 Core Graphics 来绘制控件是非常值得做的。使用 Core Graphics 可以做出比通过图片渲染方法更通用的控件。

处理控件属性的改变

那么到现在,还有什么事情要做呢?控件现在看起来已经非常的华丽了,它的外观是通用的,并且也支持 target-action 通知。

貌似已经做完了?

思考一下,如果当控件熏染之后,如果通过代码对 slider 的属性做了修改,会发生什么?例如,你希望修改一下 slider 的默认值,或者修改一下 track highlight,表示出一个有效范围。

目前,还没有任何代码来观察属性的设置情况。我们需要将其添加到控件中。我们需要实现属性观察者,来更新控件的 frame 或者重绘控件。打开 RangeSlider.swift,按照下面的代码对属性的声明作出修改:

 1 var minimumValue: Double = 0.0 {
 2 didSet {
 3 updateLayerFrames()
 4 }
 5 }
 6
 7 var maximumValue: Double = 1.0 {
 8 didSet {
 9 updateLayerFrames()
10 }
11 }
12
13 var lowerValue: Double = 0.2 {
14 didSet {
15 updateLayerFrames()
16 }
17 }
18
19 var upperValue: Double = 0.8 {
20 didSet {
21 updateLayerFrames()
22 }
23 }
24
25 var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) {
26 didSet {
27 trackLayer.setNeedsDisplay()
28 }
29 }
30
31 var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) {
32 didSet {
33 trackLayer.setNeedsDisplay()
34 }
35 }
36
37 var thumbTintColor: UIColor = UIColor.whiteColor() {
38 didSet {
39 lowerThumbLayer.setNeedsDisplay()
40 upperThumbLayer.setNeedsDisplay()
41 }
42 }
43
44 var curvaceousness: CGFloat = 1.0 {
45 didSet {
46 trackLayer.setNeedsDisplay()
47 lowerThumbLayer.setNeedsDisplay()
48 upperThumbLayer.setNeedsDisplay()
49 }
50 }

一般情况,我们需要根据依赖的属性,调用 setNeedsDisplay 方法将对于的 layer 进行重新处理。setLayerFrames 方法会对控件的布局作出调整。

现在,找到 updateLayerFrames,然后将下面的代码添加到该方法的顶部:

1 CATransaction.begin() 2 CATransaction.setDisableActions(true)

并将下面的代码添加到方法的尾部:

1 CATransaction.commit()

上面的代码将整个 frame 的更新封装到一个事物处理中,这样可以让界面重绘变得流畅。同样还明确的把 layer 中的动画禁用掉,跟之前一样,这样 layer frame 的更新会变得即时。

由于现在每当 upper 和 lower 值发生变动时, frame 会自动更新了,所以,找到 continueTrackingWithTouch 方法,并将下面的代码删除掉:

1 // 3. Update the UI
2 CATransaction.begin()
3 CATransaction.setDisableActions(true)
4
5 updateLayerFrames()
6
7 CATransaction.commit()

上面的这些代码就能够确保属性变化时,能够反应到 slider 控件中。

为了确保代码无误,我们需要写点测试 case 进行测试。

打开 ViewController.swift,并将下面代码添加到 viewDidLoad: 尾部:

1 let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC))
2 dispatch_after(time, dispatch_get_main_queue()) {
3 self.rangeSlider.trackHighlightTintColor = UIColor.redColor()
4 self.rangeSlider.curvaceousness = 0.0
5 }

上面的代码会在暂停 1 秒钟之后,对控件的一些属性做出更新。其中将 track highlight 的颜色修改为红色,并修改了 slider 和 thumb 的形状。

编译并运行程序,一秒钟之后,你看到 slider 由:

变为:

很容易不是吗?

上面刚刚添加到 view controller 中的代码,演示了一个非常有趣,而又经常被忽略的内容 - 对开发的自定义控件做充分的测试。当你在开发一个自定义控件时,你需要负责对所有的属性和外观做出验证。这里有一个好的方法就是创建不同的按钮和滑块 (它们连接到控件的不同属性) 对控件做出测试。这样,你就可以实时修改控件的属性,并实时观察到它们的结果。

何去何从?

现在我们的 range slider 控件已经完成开发,并可以在程序中使用了!你可以在这里下载到完整的工程(方便的话给个小小的star...)。

不过,创建通用性自定义控件的一个关键好处就是你可以将其用于不同的工程 - 并且分享给别的开发者使用。

准备好了吗?

实际上还没有。在分享自定义控件之前,还有一些事情需要考虑:

希望通过本文的学习,你已经能愉悦的创建 slider 控件了,可能你还希望构建自己的自定义控件。如果你做了,可以在本文的评论中分享一下 - 我们非常想看到你的创作!(分享了)

本文转载自(破船之家)

转载于:https://www.cnblogs.com/chenyihang/p/5640079.html

如何用 Swift 语言构建一个自定控件相关推荐

  1. c语言字母圣诞树,如何用C语言画一个“圣诞树”.pdf

    如如何何用用C语语言言画画一一个个"圣圣诞诞树树" 如何用C语言画一个"圣诞树",我使用了左右镜像的Sierpinski triangle,每层减去上方一小块, ...

  2. 用C语言制作爱心,如何用C语言画一个“心形”

    程序yuan眼中的浪漫如何用C语言画一个"心形"? 在你们的世界里,是不是觉得程序员一点浪漫都不懂?其实不是的,程序yuan的世界也是很浪漫滴. 看 这个图片是不是很浪漫,有木有很 ...

  3. 如何用C语言写一个web服务器的基础功能

    我们都知道,学一门语言,只是单独看了就不写的话是很容易出现眼高手低的,所以,今天摩杜云要给大家分享的内容,就是如何用C语言写一个web服务器的基础功能,希望大家看完有所收获. 服务器架构 目标架构 以 ...

  4. 教你如何 构建基本的用户控件

    创建一个简单用户控件是开始自定义控件的好方法.本章主要介绍创建一个基本的颜色拾取器.接下来分析如何将这个控件分解成功能更强大的基于模板的控件. 创建基本的颜色拾取器很容易.然而,创建自定义颜色拾取器仍 ...

  5. WPF系列 自定控件

    引言 WPF中微软提供了一些基本的控件,但是工作中这些基础的控件往往不能满足我们的需求,这个时候我们就需要根据实际的需求去开发自己的控件,但要注意不是所有功能不满足的情况都需要通过自定义控件来实现.实 ...

  6. MFC小程序003------MFC使用WebBrowser组件,在对话框中创建滚动视图,动态创建一个静态文本控件并设置鼠标单击的消息响应...

    MFC小程序截图: 一.在MFC中简单使用WebBrowser的ActiveX插件的方法: 见博文:  http://blog.csdn.net/supermanking/article/detail ...

  7. SAP UI5 初学者教程之三:开始接触第一个 SAP UI5 控件 试读版

    Jerry 从 2014 年加入 SAP成都研究院 CRM Fiori 开发团队之后开始接触 SAP UI5,曾经在 SAP 社区和"汪子熙"微信公众号上发表过多篇关于 SAP U ...

  8. C++ 一个程序获取另一个程序Edit控件的内容

    转载地址:https://www.cnblogs.com/lujin49/p/4796502.html //一个程序获取另一个程序Edit控件的内容 //根据指定程序的标题名获取改程序窗口的句柄 HW ...

  9. 用html+css+js实现一个无限级树形控件

    题目描述: 用html+css+js实现一个无限级树形控件,功能如下: 1.利用html.css展示一个树形菜单 2.点击箭头图标展开子项 3.点击父节点勾选图标能全选或全取消,点击叶节点勾选图标可以 ...

最新文章

  1. 在一个夜黑风高的夜晚,坐在教室里学多线程
  2. Windows PowerShell in Action
  3. IE6下PNG图片透明效果(PNG图片做背景也可以)
  4. Oracle segment啥意思,关于oracle数据库段segment的小结
  5. 有了SDS,硬件还重要吗?
  6. 华为-RH5885 V3 远程KVM
  7. JS 数组(遍历 二维数组)
  8. 已经围上为何不算目_万达为何至今不上市,并非王健林真的钱多,许家印的境遇说明一切...
  9. python加粗字体_Python:openpyxl将字体更改为粗体 - python
  10. 信源编码的三种方式与实现
  11. 【腾讯云技术沙龙预告】云端数据库的设计之美
  12. 【贪玩巴斯】一文学会检索三要素:检索字段、检索词、检索算法检索(二)——「一文学会检索三要素:检索字段、检索词、检索算法」 2021-09-18
  13. 研究生学位论文文献检索小技巧——妙用谷歌学术搜索
  14. python计算加权平均分_python – 使用pandas数据帧计算加权平均值
  15. 网站流量统计分析工具,谷歌seo网站流量统计分析工具推荐
  16. Python实现自动给视频打码,减少不宜画面出现...
  17. 渗透中超全的Google hack语法
  18. AUTOCAD使用笔记
  19. 基于Cortex-A53内核Linux系统gec6818开发板的电子自助点餐设计
  20. 计算机考研分数403,总分403分过来人分享成功考研经验_跨考网

热门文章

  1. 福禄克宣布推出 FiberLert,口袋大小的实时光纤探测器
  2. 攻击防护手段_如何防护ddos流量攻击?
  3. 程序高手和菜鸟的区别是什么?
  4. Java 类在 Tomcat 中是如何加载的?
  5. Web安全的三个攻防姿势
  6. svd奇异值分解_NCL专辑 | 奇异值分解(SVD)
  7. 云开发技术应用python_云开发技术应用(Python)
  8. dockerfile源码安装mysql_docker容器详解五: dockerfile实现tomcat环境以及源码安装mysql...
  9. JDK 8 新特性 之 default关键字
  10. 从头开始vue创建项目_我正在以设计师的身份开始一个被动的收入项目。 从头开始。...