如何用 Swift 语言构建一个自定控件

用户界面控件是全部应用程序重要的组成部分之一。它们以图形组件的方式呈现给用户,用户能够经过它们与应用程序进行交互。苹果提供了一套控件,例如 UITextField,UIButton,UISwitch。经过工具箱中的这些已有控件,咱们能够建立各式各样的用户界面。css

然而,有时候你但愿界面作得稍微的不同凡响,那么此时苹果提供的这些控件就没法知足你的需求。html

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

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

注意:本文截稿时,咱们还不会贴出关于 iOS 8 beta 版本的截图。全部文中涉及到的截图都是在iOS 8以前的版本中获得的,不过结果很是相似。github

目录:编程

开始

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

你能够经过建立一个 UIView 的子类,而后为可视的价格范围定作一个 view。这对于应用程序内部来讲,是 ok的,可是要想移植到别的程序中,就须要花更多的精力了。api

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

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

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

首先,咱们须要作出决定的就是建立自定义控件须要继承自哪一个类,或者对哪一个类进行扩展。

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

若是你注意观察苹果的 UIKit 参考,会发现框架中的许多控件,例如 UILabel 和 UIWebView 都是直接继承自 UIView 的。然而,也有极少数,例如 UIButton 和 UISwitch 是继承自 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,用下面的内容替换之:

import UIKit

class ViewController: UIViewController { let rangeSlider = RangeSlider(frame: CGRectZero) override func viewDidLoad() { super.viewDidLoad() rangeSlider.backgroundColor = UIColor.redColor() view.addSubview(rangeSlider) } override func viewDidLayoutSubviews() { let margin: CGFloat = 20.0 let width = view.bounds.width - 2.0 * margin rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length, width: width, height: 31.0) } } 

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

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

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

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

添加默认的控件属性

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

import UIKit

class RangeSlider: UIControl { var minimumValue = 0.0 var maximumValue = 1.0 var lowerValue = 0.2 var upperValue = 0.8 } 

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

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

如今是时候开始作控件的交互元素了,咱们分别用两个 thumbs 表示高和低两个值,而且让这两个 thumbs 可以滑动。

Images vs. CoreGraphics

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

一、Images – 为控件构建不一样的图片,这些图片表明控件的各类元素。二、Core Graphics – 利用 layers 和 Core Graphics 组合起来熏染控件。

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

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

经过图片的方式来构建的控件,给使用控件的人提供了很是大的灵活度。开发者能够改变每个像素,以及控件的详细外观,不过这须要很是熟练的图形设计技能 – 而且经过代码很是难以对控件作出修改。

Core Graphics – 利用 Core Graphics 构建控件意味着你必须本身编写渲染控件的代码,这就须要付出更多的代价。不过,这种方法能够建立更加灵活的 API。

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

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

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

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

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

let trackLayer = CALayer()
let lowerThumbLayer = CALayer() let upperThumbLayer = CALayer() var thumbWidth: CGFloat { return CGFloat(bounds.height) } 

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

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

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

override init(frame: CGRect) {
    super.init(frame: frame) trackLayer.backgroundColor = UIColor.blueColor().CGColor layer.addSublayer(trackLayer) lowerThumbLayer.backgroundColor = UIColor.greenColor().CGColor layer.addSublayer(lowerThumbLayer) upperThumbLayer.backgroundColor = UIColor.greenColor().CGColor layer.addSublayer(upperThumbLayer) updateLayerFrames() } required init(coder: NSCoder) { super.init(coder: coder) } func updateLayerFrames() { trackLayer.frame = bounds.rectByInsetting(dx: 0.0, dy: bounds.height / 3) trackLayer.setNeedsDisplay() let lowerThumbCenter = CGFloat(positionForValue(lowerValue)) lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0, width: thumbWidth, height: thumbWidth) lowerThumbLayer.setNeedsDisplay() let upperThumbCenter = CGFloat(positionForValue(upperValue)) upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0, width: thumbWidth, height: thumbWidth) upperThumbLayer.setNeedsDisplay() } func positionForValue(value: Double) -> Double { let widthDouble = Double(thumbWidth) return Double(bounds.width - thumbWidth) * (value - minimumValue) / (maximumValue - minimumValue) + Double(thumbWidth / 2.0) } 

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

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

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

override var frame: CGRect { didSet { updateLayerFrames() } } 

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

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

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

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

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

添加交互逻辑

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

跟以前同样,在 Xcode 中建立一个新的 Cocoa Touch Class,命名为 RangeSliderThumbLayer,继承自 CALayer。

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

import UIKit
import QuartzCore

class RangeSliderThumbLayer: CALayer { var highlighted = false weak var rangeSlider: RangeSlider? } 

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

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

let lowerThumbLayer = RangeSliderThumbLayer()
let upperThumbLayer = RangeSliderThumbLayer() 

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

lowerThumbLayer.rangeSlider = self
upperThumbLayer.rangeSlider = self 

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

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

如今你已经有了 slider 的thumb layer – RangeSliderThumbLayer,而后须要给控件添加拖拽 thumb 的功能。

添加触摸处理

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

var previousLocation = CGPoint()

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

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

UIControl 提供了一些方法来跟踪触摸。UIControl 的子类能够 override 这些方法,以实现本身的交互逻辑。

在自定义控件中,咱们将 override 3 个 UIControl 关键的方法:beginTrackingWithTouch, continueTrackingWithTouch 和 endTrackingWithTouch。

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

override func beginTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) -> Bool { previousLocation = touch.locationInView(self) // Hit test the thumb layers if lowerThumbLayer.frame.contains(previousLocation) { lowerThumbLayer.highlighted = true } else if upperThumbLayer.frame.contains(previousLocation) { upperThumbLayer.highlighted = true } return lowerThumbLayer.highlighted || upperThumbLayer.highlighted } 

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

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

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

如今,有了初始的触摸事件,咱们须要处理用户在屏幕上移动的事件了。

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

func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double { return min(max(value, lowerValue), upperValue) } override func continueTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) -> Bool { let location = touch.locationInView(self) // 1. Determine by how much the user has dragged let deltaLocation = Double(location.x - previousLocation.x) let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height) previousLocation = location // 2. Update the values if lowerThumbLayer.highlighted { lowerValue += deltaValue lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue) } else if upperThumbLayer.highlighted { upperValue += deltaValue upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue) } // 3. Update the UI CATransaction.begin() CATransaction.setDisableActions(true) updateLayerFrames() CATransaction.commit() return true } 

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

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

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

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

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

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

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

编译并运行程序,尝试移动滑块!如今你应该能够移动 thumb 了。

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

值改变的通知

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

这里有多种模式能够实现值改变的通知: NSNotification,Key-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 语句前面:

sendActionsForControlEvents(.ValueChanged) 

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

如今咱们应该对这个事件进行订阅,并当事件来了之后,做出相应的处理。

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

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

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

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

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

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

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

Range slider value changed: (0.117670682730924 0.390361445783134) Range slider value changed: (0.117670682730924 0.38835341365462) Range slider value changed: (0.117670682730924 0.382329317269078) Range slider value changed: (0.117670682730924 0.380321285140564) Range slider value changed: (0.119678714859438 0.380321285140564) Range slider value changed: (0.121686746987952 0.380321285140564) 

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

如今是时候给控件换换面目了!

结合 Core Graphics 对控件进行修改

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

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

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

import UIKit
import QuartzCore

class RangeSliderTrackLayer: CALayer { weak var rangeSlider: RangeSlider? } 

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

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

let trackLayer = RangeSliderTrackLayer()

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

init(frame: CGRect) {
    super.init(frame: frame)  trackLayer.rangeSlider = self trackLayer.contentsScale = UIScreen.mainScreen().scale layer.addSublayer(trackLayer) lowerThumbLayer.rangeSlider = self lowerThumbLayer.contentsScale = UIScreen.mainScreen().scale layer.addSublayer(lowerThumbLayer) upperThumbLayer.rangeSlider = self upperThumbLayer.contentsScale = UIScreen.mainScreen().scale layer.addSublayer(upperThumbLayer) } 

上面的代码确保新的 track layer 引用到 range slider – 并无再用那可怕的颜色了!而后将 contentsScale 因子设置位与设备的屏幕同样,这样能够确保全部的内容在 retina 显示屏中没有问题。

下面还有一个事情须要作,就是将 viewDidLoad 中的以下代码移除掉:

rangeSlider.backgroundColor = UIColor.redColor()

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

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

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

因为许多开发者但愿可以经过编码对控件作各类配置,以使其外观可以效仿一些流行的程序,因此咱们给 slider 添加一些属性,运行开发者对其外观作出一些定制。

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

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

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

接下来,打来 RangeSliderTrackLayer.swift。

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

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

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

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

override func drawInContext(ctx: CGContext!) {
    if let slider = rangeSlider { // Clip let cornerRadius = bounds.height * slider.curvaceousness / 2.0 let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius) CGContextAddPath(ctx, path.CGPath) // Fill the track CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor) CGContextAddPath(ctx, path.CGPath) CGContextFillPath(ctx) // Fill the highlighted range CGContextSetFillColorWithColor(ctx, slider.trackHighlightTintColor.CGColor) let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue)) let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue)) let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height) CGContextFillRect(ctx, rect) } } 

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

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

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

若是你对 curvaceousness 作什么的还存在疑惑,那么试着修改一下它看看!

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

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

override func drawInContext(ctx: CGContext!) {
    if let slider = rangeSlider { let thumbFrame = bounds.rectByInsetting(dx: 2.0, dy: 2.0) let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0 let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius) // Fill - with a subtle shadow let shadowColor = UIColor.grayColor() CGContextSetShadowWithColor(ctx, CGSize(width: 0.0, height: 1.0), 1.0, shadowColor.CGColor) CGContextSetFillColorWithColor(ctx, slider.thumbTintColor.CGColor) CGContextAddPath(ctx, thumbPath.CGPath) CGContextFillPath(ctx) // Outline CGContextSetStrokeColorWithColor(ctx, shadowColor.CGColor) CGContextSetLineWidth(ctx, 0.5) CGContextAddPath(ctx, thumbPath.CGPath) CGContextStrokePath(ctx) if highlighted { CGContextSetFillColorWithColor(ctx, UIColor(white: 0.0, alpha: 0.1).CGColor) CGContextAddPath(ctx, thumbPath.CGPath) CGContextFillPath(ctx) } } } 

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

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

var highlighted: Bool = false {
    didSet {
        setNeedsDisplay() } } 

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

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

不难发现,用 Core Graphics 来绘制控件是很是值得作的。使用 Core Graphics 能够作出比经过图片渲染方法更通用的控件。

处理控件属性的改变

那么到如今,还有什么事情要作呢?控件如今看起来已经很是的华丽了,它的外观是通用的,而且也支持 target-action 通知。

貌似已经作完了?

思考一下,若是当控件熏染以后,若是经过代码对 slider 的属性作了修改,会发生什么?例如,你但愿修改一下 slider 的默认值,或者修改一下 track highlight,表示出一个有效范围。

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

var minimumValue: Double = 0.0 { didSet { updateLayerFrames() } } var maximumValue: Double = 1.0 { didSet { updateLayerFrames() } } var lowerValue: Double = 0.2 { didSet { updateLayerFrames() } } var upperValue: Double = 0.8 { didSet { updateLayerFrames() } } var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) { didSet { trackLayer.setNeedsDisplay() } } var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) { didSet { trackLayer.setNeedsDisplay() } } var thumbTintColor: UIColor = UIColor.whiteColor() { didSet { lowerThumbLayer.setNeedsDisplay() upperThumbLayer.setNeedsDisplay() } } var curvaceousness: CGFloat = 1.0 { didSet { trackLayer.setNeedsDisplay() lowerThumbLayer.setNeedsDisplay() upperThumbLayer.setNeedsDisplay() } } 

通常状况,咱们须要根据依赖的属性,调用 setNeedsDisplay 方法将对于的 layer 进行从新处理。setLayerFrames 方法会对控件的布局做出调整。

如今,找到 updateLayerFrames,而后将下面的代码添加到该方法的顶部:

CATransaction.begin()
CATransaction.setDisableActions(true) 

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

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

因为如今每当 upper 和 lower 值发生变更时, frame 会自动更新了,因此,找到 continueTrackingWithTouch 方法,并将下面的代码删除掉:

// 3. Update the UI
CATransaction.begin() CATransaction.setDisableActions(true) updateLayerFrames() CATransaction.commit() 

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

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

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

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

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

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

变为:

很容易不是吗?

上面刚刚添加到 view controller 中的代码,演示了一个很是有趣,而又常常被忽略的内容 – 对开发的自定义控件作充分的测试。当你在开发一个自定义控件时,你须要负责对全部的属性和外观作出验证。这里有一个好的方法就是建立不一样的按钮和滑块 (它们链接到控件的不一样属性) 对控件作出测试。这样,你就能够实时修改控件的属性,并实时观察到它们的结果。

何去何从?

如今咱们的 range slider 控件已经完成开发,并能够在程序中使用了!你能够在这里下载到完整的工程。

不过,建立通用性自定义控件的一个关键好处就是你能够将其用于不一样的工程 – 而且分享给别的开发者使用。

准备好了吗?

实际上尚未。在分享自定义控件以前,还有一些事情须要考虑:

文档 – 你可能认为代码写得很是的完美,具备自我陈述的能力,不在须要额外的文档了,不过别的开发者是不一样意的。最佳实践就是提供 public API 的相关文档,至少要提供全部分享的 public 代码,也就是说对全部的public 类和属性进行文档化。

例如,文档中须要说明 RangeSlider 是什么的 – slider 是这样的一个东西:定义了 4 个属性,包括minimumValue, maximumValue, lowerValue, 和 upperValue – 它是作什么的 – 容许用户经过在界面中定义数值的范围。

鲁棒性 – 若是将 upperValue 设置为比 maximumValue 还要大,会发生什么?固然,你是确定不会这样作的 – 这是愚蠢的一件事情,不是吗?可是你没法保证全部的人都不这么作!你须要确保控件的状态老是有效 – 尽管一些愚蠢的码农会尝试这样作。

API 设计 – 前面说的鲁棒性涉及到一个更普遍的主题 – API 设计。建立一个具备灵活性、直观性和鲁棒性的 API 有利于控件被普遍的使用和流行。

API 设计是很深的一个主题,超出了本文的介绍范围,若是你感兴趣,建议阅读 Matt Gemmel 关于 API 设计的 25 条规则

网络中有许多地方能够分享你的控件。下面是建议的一些地方:

  • GitHub – GitHub 已是分享开源项目首选的一个地方。在 GitHub 上有大量关于 iOS 的自定义控件。GitHub 的伟大之处在于它容许人们很容易的就能访问到你共享的代码,也可以很容易的经过 forking 你的共享的代码,与别的控件进行协做开发,另外还能很方便的对控件 faise issues。
  • CocoaPods – CocoaPods 是一个 iOS 和 OSX 工程的第三方库依赖管理工具, 容许开发者很容易的将你的控件添加到他们的工程中,因此你能够经过 CocoaPods 分享你的控件。
  • iOS Example – 这个网站位商业和开源的控件提供一个目录。许多开源控件都会提供到 上面,这是促进你进行创做的伟大方式。

但愿经过本文的学习,你已经能愉悦的建立 slider 控件了,可能你还但愿构建本身的自定义控件。若是你作了,能够在本文的评论中分享一下 – 咱们很是想看到你的创做!

相关文章
相关标签/搜索