如何在Swift中创建自定义控件


原文地址:http://www.raywenderlich.com/76433/how-to-make-a-custom-control-swift
泰然翻译组:阳光新鲜。校对:glory。

Mou icon

更新通知:这篇引导教程由Mikael Konutgan使用iOS 8和Swift语言重新制作,在Xcode6和7上测试通过。原始教程是由Colin Eberhardt团队制作的。

用户界面控件是许多应用的重要组成部分。使用这些控件,可以让用户查看应用的内容或与他们的应用进行交互。苹果提供了一个控件集,像UITextField, UIButtonUISwitch。灵活使用这些工具箱中已经存在的控件,可以让你创建各种各样的用户界面。

但是,有的时候你可能需要做一些与众不同的事情;库中的控件已经不够用了。

自定义控件只不过是你自己创建的控件;也就是一个不来自于UIKit框架的控件。自定义控件,就像标准控件一样,应该具有通用和可定制的特性。而且,你会找到一个积极且充满活力的开发者社区喜欢分享他们的自定义控件。

在这个教程里,你将实现你自己的RangeSlider控件。这个控件就像一个双头滑块,可以用来选择最小和最大值。你会接触到关于如何扩展已经存在控件的思想,然后设计和实现你的控件API,甚至在开发者社区中共享你的控件。

开始我们的自定义之旅吧!

注意:在我们制作教程时,iOS 8仍处於测试阶段,所以我们不能给他截图。所有的截图都取自之前的iOS版本,但你的实际运行结果是没有差别的。

开始的工作

话说你开发了一个可以搜索房地产销售信息的应用。这个应用允许用户过滤搜索结果,来将搜索结果控制在一定的价格范围内。

你可以提供一个呈现出一对UISlider控件的交互界面,一个设置最低价格,另一个设置最高价格。尽管如此,这个交互界面不能真正帮助使用者形象的了解价格范围。如果设计成一个滑动条和连个滑动钮,这样可以更好的表达出他们想要搜索的价格高低范围。

你可以子类化UIView来创建一个范围滑动器,并且创建一个定制的视图来显示价格区间。这对于你的app是好用的,但如果将他用在其他类型的app上那就是一种煎熬。

让这个新的自定义组件变的更通用将会是个好的想法,这样他就可以在任何需要他的环境内使用。这是自定义控件的精髓所在。

启动Xcode。选择File/New/Project,然后选择iOS/Application/Single View Application模板,点击Next。在接下来的屏幕里,在product name中输入CustomSliderExample,选择你想使用的Organization NameOrganization Identifier,确定Swift语言被选中,iPhone被选为DeviceUse Core Data没有被选中。

最后选择一个保存路径,然后点击Create

我们要做的第一个决策就是通过对一个已经存在的控件进行继承或扩展来实现新的控件。

为了在现有的UI上使用,你的控件必须继承于UIView

如果你查看苹果的UIKit参考手册,你会看到许多的控件像 UILabelUIWebView 是直接继承于UIView的。尽管如此,还是有一些棘手的事情,像 UIButtonUISwitch 是继承于 UIControl ,像如下的层级关系所示:

Mou icon

注意:如果你想查看一个完整的UI组件类层级示意图,请阅读UIKit Framework Reference

UIControl 使用的是target-action pattern机制,这是一种用于通知用户信息改变的机制。 UIControl 也具有很多的属性来表示当前的控制状态。在这个自定义控件中将使用target-action pattern,所以 UIControl 将担当重要的起始点。

在项目导航中右击CustomSliderExample组然后选择New File…,选择iOS/Source/Cocoa Touch Class模板点击Next。类取名为RangeSlider,在Subclass of中输入 UIControl 并且确保语言选择为Swift。点击下一步然后选择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)

    }

}

上面的代码简单的创建了一个指定过大小的全新控件的实例并且将实例添加到了视图内。控件的背景颜色已经被设置成红色这样可以使其和应用的背景形成鲜明的对比。如果你不将背景设置成红色,你的控件将不容易被找到,你将怀疑你的控件去哪了!:]

运行app;你将看到和下图一样的画面:

Mou icon

在你向控件中添加可视元素之前,你需要几个属性,以便跟踪控件当前所设置的信息。这将形成你的控件的应用程序接口,简称API。

注意:你的控件的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

}

这四个属性足够你描述控件的状态了,最大最小值用于表示范围,这些值由上限下限值进行调整。

设计好的控件需要一些默认的属性值,否则你的控件会看起来有一些怪!如果你照做的话会好很多。

现在我们可以设计控件上的交互元素了;也就是说,滑块以表示最大最小值,并且可以在滑条上滑动。

Images vs. CoreGraphics

你可以使用以下两种方式来在屏幕上绘制你的控件:

  1. Images – 为控件创建图片来表示多种多样的形式

  2. CoreGraphics – 使用层和CoreGraphics混合使用来渲染控件

每个技术都有优缺点,如下所示:

Images — 使用图片来创建控件可能是最容易做到的事 - 只要你知道如何绘制!:]如果你需要让你的开发队友能够修改控件的外观,你只需要将图片设为 UIImage 的开放属性就可以了。

使用图片可以使开发人员更灵活的使用控件。开发这可以修改每一个像素和没一个你想要表现的细节,但这需要很好的平面设计技能 - 因为在代码中修改图片很困难。

Core Graphics — 使用Core Graphics构建以为这你需要自己编写渲染代码,这需要花费更多的努力。但是,这种技术允许你创建更加灵活的API。

使用Core Graphics,你可以用参数表示控件的每一个特征,不如颜色,边框宽度,和弯曲度 - 几乎所有的视觉元素都可以直接绘制!这种方法允许开发人员对控件进行完整的定制以适应他们的需要。

这个教程将使用第二种技术 - 使用Core Graphics进行渲染。

注意:有趣的是,苹果公司倾向于在控件中直接使用图片。这可能是因为他们知道控件的实际大小,且不需要做太多的自定义修改。毕竟,他们希望的结果是所有的应用看起来都差不多。

打开RangeSlider.swiftimport UIKit 的下面加入以下代码:

import QuartzCore

在我们刚才添加代码的下部添加如下代码:

let trackLayer = CALayer()

let lowerThumbLayer = CALayer()

let upperThumbLayer = CALayer()



var thumbWidth: CGFloat {

    return CGFloat(bounds.height)

}

trackLayer, lowerThumbLayer, 和 upperThumbLayer 这三个层将用于滑条控件的渲染。 thumbWidth 将在布局属性中被使用。

接下来是添加一些控件自身的默认属性。

在RangeSlider类中,添加一个初始化方法和其他的辅助方法:

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)

}

初始化方法简单的创建了三个层,然后作为子层添加到控件层中,然后 updateLayerFrames ,这样,层的布局就能自动选择合适的大小了!:]

最后,positionForValue 根据一个值计算出控件在屏幕中的位置,并且使用一个简单的比例缩放最大最小值,计算控件中的位置。

接下来,重写frame方法,实现一个观察者属性,添加到RangeSlider.swift后面:

override var frame: CGRect {

    didSet {

        updateLayerFrames()

    }

}

这个观察者会在布局发生改变时更新层的布局位置。这是必须的,但就像在ViewController.swift中一样,在初始化中被执行,但这并不是最终的布局。

运行应用;你的滑动条已经成形了!他看起来应该如下所示:

Mou icon

记住,红色的是整个控件的背景。蓝色是轨道,绿色是两个滑动的钮,对应最大最小值。

你的控件已经可以显示了,但几乎苹果上所有的控件都可以和用户进行交互。

对于你的控件,用户应该可以拖拽两个滑块来调解范围。你将控制他们进行交互,并且开放更新UI和更新属性的接口。

添加交互逻辑

交互必须储存滑块拖动的位置,并在UI中显示出来。控件层是最适合放这些逻辑的地方。

像以前一样,在Xcode中创建一个新Cocoa Touch Class,将他命名为 RangeSliderThumbLayer ,继承于 CALayer

将以下代码添加到RangeSliderThumbLayer.swift中:

import UIKit

import QuartzCore



class RangeSliderThumbLayer: CALayer {

    var highlighted = false

    weak var rangeSlider: RangeSlider?

}

这里添加了两个属性:一个用于表明滑块是否高亮显示,另一个是父控件的引用。自从RangeSlider有两个滑块层,他们需要持有两个背景控件的弱引用,以避免被内存管理系统回收。

打开RangeSlider.swift修改 lowerThumbLayerupperThumbLayer 属性的类型,替换成如下代码。

let lowerThumbLayer = RangeSliderThumbLayer()

let upperThumbLayer = RangeSliderThumbLayer()

还是在RangeSlider.swift,找到 init 添加如下代码:

lowerThumbLayer.rangeSlider = self

upperThumbLayer.rangeSlider = self

上面的代码设置了自己的父节点弱引用 rangeSlider 属性。

运行项目;检查应用是否按预想的方式执行。

现在你已经使用 RangeSliderThumbLayer 创建了滑块层,你需要添加让用户可以拖动滑块移动的功能。

添加触摸控制

打开RangeSlider.swift在属性区域添加如下属性:

var previousLocation = CGPoint()

这个属性将被用于追踪触摸位置。

如何跟踪变化的触摸点和释放事件?

UIControl 提供了跟踪触摸点的多种方法。继承于 UIControl 的类可以重写这些方法并在里面增加自己的交互代码。

在你的自定义控件中,你将重写三个方法 UIControl: beginTrackingWithTouch, continueTrackingWithTouchendTrackingWithTouch

添加如下方法在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

}

这个方法将在用户第一次触摸屏幕时被调用。

首先,他将触摸事件转换为控件的坐标系。接下来,他将检查两个滑块已查看时候触摸到了滑块上。上面方法的返回值通知 UIControl 父类哪个后来的触点需要被跟踪。

如果滑块是高亮的,则跟踪轨迹事件。

现在你有一个初始的触摸事件,你将需要在手指在屏幕上移动是做一些处理。

添加如下方法到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. 这里通过手指一动哪个滑块确定改变最大值还是最小值。

  3. 这个部分设置了 CATransaction 类的 disabledActions 标志。这将确保改变立刻生效,而不使用动画。最后调用 updateLayerFrames 设置滑钮到当前位置。

你已经编写了移动滑钮的代码 - 但你还需要编写触摸和拖拽的处理事件。

添加如下方法到RangeSlider.swift

override func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) {

    lowerThumbLayer.highlighted = false

    upperThumbLayer.highlighted = false

}

上面的代码将恢复滑钮到非高亮显示的状态。

运行项目,然后试用你闪亮的滑动条!你应该可以随意的拽动绿色的按钮。

Mou icon

请注意,在你拖动滑条时,你可以将手指拖动到空间范围以外的地方,然后返回控件位置依然不会失去对按钮的控制。这是一个对小屏低分辨率设备尤为重要的特性 - 或者说对于广大手指们来说!:]

改变通知消息

现在你已经有了一个用户可以控制上界和下界的用户交互控件。但你如何将这些更改通知给应用,以让应用知道新设置的值?

有很多模式可以实现发出修改消息:NSNotification, Key-Value-Observing (KVO),委托模式,目标操作模式和其他。有很多的选择!

怎么做?

如果你看过UIKit的控件,你将发现他们都不使用 NSNotification 也不鼓励使用KVO,所以为了能和UIKit达到一致性你应该避免使用这两个方法。另外两种方法委托和目标则在UIKit中广泛使用。

以下是对委托和目标的详细描述:

委托模式 - 委托者模式中你将提供一个包含多个方法的协议用于一定范围内的消息通知。控件有一个属性,通常命名为 delegate ,用于接受使用这种协议的类。一个经典的例子是UITableView使用了 UITableViewDelegate 协议。注意这些控件都值接受一种协议实例。一个委托方法有许多参数,所以你可以使用方法传递尽可能多的信息。

目标操作模式 - 目标操作模式是 UIControl 的基类。当一个控件的状态发生改变时,将用 UIControlEvents 的枚举值提醒目标发生了什么动作。你可以给一个控件提供多个目,可以创建自定义事件(具体请查看 UIControlEventApplicationReserved ),自定义事件的数量限定为4个。控件多做不能在事件中传递更多的信息。所以他不能处理包含巨量信息的事件。

下面是关于两个模式的不同点:

  • 多路广播 - 目标操作模式可以实现多路通知,而委托模式只限制在一个委托实例上。

  • 灵活性 - 你在委托模式中定义了自己的协议,以为着你可以通过它控制巨量的信息。目标操作不能提供额外的信息,客户端只能在接受到事件后自查。

你的范围滑条中提供的消息中是没有大量的状态信息的。消息中其实仅有最大和最小值。

在这种情况下,目标操作模式完美的做到了这一点。这就是为什么在教程的一开始要求你继承UIControl的原因之一!

啊哈!现在感觉不错!:]

滑条值的更新在 continueTrackingWithTouch:withEvent: ,所以这里也是你添加通知代码的地方。

打开RangeSlider.swift,定位到 continueTrackingWithTouch ,在“ return true ”前面添加:

sendActionsForControlEvents(.ValueChanged)

这样做你就完全可以响应定制目标的改变!

现在你已经有你的消息处理了,你现在应该将他挂载到你的应用中。

打开ViewController.swiftviewDidLoad 尾部添加如下代码:

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

上面的代码将在滑条每次发送 UIControlEventValueChanged 动作时调用 rangeSliderValueChanged 方法。

现在在ViewController.swift中添加如下代码:

func rangeSliderValueChanged(rangeSlider: RangeSlider) {

    println("Range slider value changed: (\(rangeSlider.lowerValue) \(rangeSlider.upperValue))")

}

这段代码简单的将滑块数值的改变发送到控制台输出上。

运行应用,左右移动滑条。你将在控制台上看到空间输出的值,截图如下:

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)

估计你已经看够了由多种颜色拼成的滑条的UI。他看起来就像一个愤怒的水果沙拉!

是时候给控件整整容了!

使用Core Graphics修改你的控件

首先,首先你需要更新滑钮滑动的轨道。

增加另一个以CALayer为父类的类像之前一样,这次起名为 RangeSliderTrackLayer

打开这个类,将内容替换为如下内容:

import UIKit

import QuartzCore



class RangeSliderTrackLayer: CALayer {

    weak var rangeSlider: RangeSlider?

}

代码添加了一个指向背景的弱引用,就像之前的滑钮层一样。

打开 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)

}

代码确保了滑钮从滑道的引用添加正确 - 之前可怕的背景颜色不再被使用了。:]设置 contentsScale 工厂匹配设备将确保在视网膜屏幕上也能按正确的比例显示。

这项的内容有点多一些 - 在控件中删除红色的背景。

打开ViewController.swift,定位到viewDidLoad方法中的如下位置然后删除它:

rangeSlider.backgroundColor = UIColor.redColor()

运行。。。你会看到什么呢?

Mou icon

你什么都没看到?那就对了!

很好?这有什么好的?之前所做的一切都白费了?!?!

别着急 - 你刚才删除了层上用于测试的颜色。你的控件一直都在这里 - 但是你的控件使用的是一个空白的画布!

因为大多数开发者喜欢可以定制控件的显示风格,所以你需要在滑条里添加一些属性以实现可以改变视觉效果的控件。

打开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

使用可变颜色的目的是非常简单的。还有曲线?好的,这一点就有趣了 - 你很快就会了解他!:]

下一步,打开RangeSliderTrackLayer.swift

这个层是用于渲染两个滑钮下面的滑道的。他继承于 CALayer ,他只是渲染一个固定的颜色。

为了绘制这条轨迹,你需要实现 drawInContext :并且使用Core Graphics APIs来执行渲染。

注意:想深入学习Core Graphics的只是,强烈推荐学习Core Graphics 101系列引导教程的内容,这里如果讨论关于Core Graphics的话题就超出了本教程的范围了。

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)

    }

}

一旦轨道的轮廓被裁减,背景就会填充上。之后高亮的部分也被填充。

运行一下,看看你的新的轨道,很自豪吧!他看起来应该如下所示:

Mou icon

尝试着改变可以变化的值看看会对渲染产生什么影响。

如果你一直怀疑 curvaceousness 是做什么的,尝试改变他试一下!

你将使用相同的方式绘制按钮层。

打开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)

        }

    }

}

一旦路径定义成按钮的轮廓,轮廓将被填充。注意看那细微的影子,这给人一种按钮在轨道上徘徊的立体感。边界之后被渲染。最后,如果按钮高亮 - 正打算移动 - 在他的附近就会出现一个微妙的阴影。

最后的事情是在运行前。将 highlighted 属性改为如下所示:

var highlighted: Bool = false {

    didSet {

        setNeedsDisplay()

    }

}

这里,你定义了一个属性观察者,这样在高亮这个属性改变时层会被重绘。这样在触摸事件激活时颜色会发生些微的改变。

再次运行;你将会看到漂亮的轮廓,和下面的截图类似:

Mou icon

你可以轻易的发现使用Core Graphics渲染你的控件产生了很多额外的效果,这是非常值得的。使用Core Graphics可以比用图片渲染更轻易的进行控制。

操控控件属性的改变

现在还剩什么? 现在的控件看起来华丽而俗气,视觉效果可以改变,并且支持目标操作模式的消息通知。

看起来已经完成了 - 或者没完成?

想一下在渲染完成后,滑条属性会被设置成什么。举例来说,如果你想通过设置属性来改变滑条的显示范围,或者通过设置轨迹高亮来确定一个可用的范围。

目前没有进行属性的观察者设置。你需要在控件中添加如下功能。这里你需要实现观察属性,以实现更新控件的轮廓或重新绘制。打开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 在属性值被改变时。 setLayerFrames 调用时可以改变控件的布局。

现在,寻找 updateLayerFrames 并且将如下内容添加到这个方法的顶端:

CATransaction.begin()

CATransaction.setDisableActions(true)

在最下面添加如下方法:

CATransaction.commit()

代码会使用一些动画效果,这样可以使整个过程更平滑。这种效果会在层上被禁用,像之前我们做的一样,以使层内容立即更新。

自从你现在可以自动更新画面,每次最大最小值改变,在 continueTrackingWithTouch 中寻找如下代码,并删除它:

// 3. Update the UI

CATransaction.begin()

CATransaction.setDisableActions(true)



updateLayerFrames()



CATransaction.commit()

这是你需要做的全部事情,确保滑条改变属性。

尽管如此,你还是需要一些代码,来测试你的大量代码,并且确保所有的元素都关联起来,如期运行。

打开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

}

这断代码会更新控件的属性,实际使用中会有一些延迟。你改变轨迹高亮的颜色为红色,改变滑条的轮廓和滑钮的外观。

运行项目。几秒后,你应该看到滑条从这样:

Mou icon

变成这样:

Mou icon

多简单,不是吗

刚才添加的代码演示了一个有趣的部分,而且是经常被忽视的部分,同时也是开发自定义控件最关键的部分 - 测试。当你开发你的自定义控件时,你就有责任测试他的所有属性并验证所有视觉效果。一个好的办法是创建一个视觉测试框架,有各种不同的按钮和滑条,每个控件都和控件的不同属性关联。这样你就可以同时改变你控件的各种属性 - 并查看会有什么效果。

接下来要做的事情?

你的数值范围滑条已经完成了所有功能,并且可以在你自己的应用中使用!你可以在这里下载完整版本。

尽管如此,创建通用性控件的好处之一是你可以将他用在其他工程上 - 并和其他开发者分享。

在这个苹果开发的黄金时期,你的控件准备好了吗?

现在还不是时候。在共享你的自定义控件之前这里还有其他的几点需要考虑:

文档 - 所有的开发者都喜欢编码!:]你肯定认为你的代码是漂亮的精美的,自文档的,可其他的开发者可不这么想。一个好的练习就是提供一个公共的API文档,最精简的,包括所有的开放共享代码。这意味着你将写出所有的开放类和属性。

举例说明,你的 RangeSlider 需要文档来解释 - 一个滑条有四个成员属性: minimumValue, maximumValue, lowerValue, 和 upperValue - 这可以做什么 - 可以让用户直接设置范围值。

健壮性 - 如果你设置最小值比最大值大会发生什么?你自己当然不会这么干 - 那会很愚蠢,不是吗?但你不能保证别人不这么做!你需要保证控制状态始终是可用的 - 尽管有些人会做傻事。

API设计 - 前面的健壮性延伸出了其他话题 - API设计。创建一个灵活,直观,健壮的API将是你的控件使用的更广泛,也会更流行。

API设计是一个更深层次的主题,超出了我们本次教程的讨论范围。如果有兴趣,强烈推荐Matt Gemmell’s 25 rules of API design

共享你的控件有如下几种方式。这里有如下几条建议:

  • GitHub - GitHub已经成为最流行的开源项目社区。这里已经有大量的自定义控件。GitHub的好处就在于他允许人们非常容易的使用你的代码并且通过为你的控件创建分支方法来实现潜在的合作开发,或者你也可以为已有的控件提交错误报告。

  • CocoaPods – 允许人们方便的添加控件到自己的工程,你也可以在CocoaPods发布,这里主要是支持iOS和OSX工程。

  • Cocoa Controls – 这个站点提供了商业和开源控件。这里的许多开源控件引用于GitHub上。这里也是一个推广你新鲜创意的好地方。

希望你能在创建滑条控件时得到乐趣,也希望你已经用自己的灵感创造了一个自定义控件。如果是这样,那么请在评论中发布链接 - 让我们看到您惊人的创造力!

标签: Swift

?>