如何用 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 控件了,可能你還但願構建本身的自定義控件。若是你作了,能夠在本文的評論中分享一下 – 咱們很是想看到你的創做!

相關文章
相關標籤/搜索