日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時(shí)間:2023/12/9 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何用 Swift 语言构建一个自定控件 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

?

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

用戶界面控件是所有應(yīng)用程序重要的組成部分之一。它們以圖形組件的方式呈現(xiàn)給用戶,用戶可以通過(guò)它們與應(yīng)用程序進(jìn)行交互。蘋果提供了一套控件,例如 UITextField,UIButton,UISwitch。通過(guò)工具箱中的這些已有控件,我們可以創(chuàng)建各式各樣的用戶界面。

然而,有時(shí)候你希望界面做得稍微的與眾不同,那么此時(shí)蘋果提供的這些控件就無(wú)法滿足你的需求。

自定義控件,除了是自己構(gòu)建二外,與蘋果提供的,沒(méi)什么差別。也就是說(shuō),自定義控件不存在于 UIKit 框架。自定義控件跟蘋果提供的標(biāo)準(zhǔn)控件一樣,應(yīng)該是通用,并且多功能的。你也會(huì)發(fā)現(xiàn),互聯(lián)網(wǎng)上有一些積極的開(kāi)發(fā)者樂(lè)意分享他們自定義的控件。

本文中,你將實(shí)現(xiàn)一個(gè)自己的 RangeSlider 自定義控件。這個(gè)控件是一個(gè)兩端都可以滑動(dòng)的,也就是說(shuō),你可以通過(guò)該控件獲得最小值和最大值。你將會(huì)接觸到這樣一些概念:對(duì)現(xiàn)有控件的擴(kuò)展,設(shè)計(jì)和實(shí)現(xiàn) 自定義控件的 API,甚至還能學(xué)到如何分享你的自定義控件到開(kāi)發(fā)社區(qū)中。

注意:本文截稿時(shí),我們還不會(huì)貼出關(guān)于 iOS 8 beta 版本的截圖。所有文中涉及到的截圖都是在iOS 8之前的版本中得到的,不過(guò)結(jié)果非常類似。

目錄:

  • 開(kāi)始
  • Images vs. CoreGraphics
  • 添加默認(rèn)的控件屬性
  • 添加交互邏輯
  • 添加觸摸處理
  • 值改變的通知
  • 結(jié)合 Core Graphics 對(duì)控件進(jìn)行修改
  • 處理控件屬性的改變
  • 何去何從?

開(kāi)始

假設(shè)你在開(kāi)發(fā)一個(gè)應(yīng)用程序,該程序提供搜索商品價(jià)格列表。通過(guò)這個(gè)假象的應(yīng)用程序允許用戶對(duì)搜索結(jié)果進(jìn)行過(guò)濾,以獲得一定價(jià)格范圍的商品。你可能會(huì)提供這樣一個(gè)用戶界面:兩個(gè) UISlider 控件,一個(gè)用于設(shè)置最低價(jià)格,另外一個(gè)設(shè)置最高價(jià)格。然而,這樣的設(shè)計(jì),不能夠讓用戶很好的感知價(jià)格的范圍。要是能夠提供一個(gè) slider,兩端可以分別設(shè)置用于搜索的最高和最低的價(jià)格范圍,就更好了。

你可以通過(guò)創(chuàng)建一個(gè) UIView 的子類,然后為可視的價(jià)格范圍定做一個(gè) view。這對(duì)于應(yīng)用程序內(nèi)部來(lái)說(shuō),是 ok的,但是要想移植到別的程序中,就需要花更多的精力了。

最好的辦法是將構(gòu)建一個(gè)新的盡可能通用的 UI 控件,這樣就能在任意的合適場(chǎng)合中重用。這也是自定義控件的本質(zhì)。

啟動(dòng) Xcode,File/New/Project,選中 iOS/Application/Single View Application 模板,然后點(diǎn)擊 Next。在接下來(lái)的界面中,輸入 CustomSliderExample 當(dāng)做工程名,然后是 Organization Name 和 Organization Identifier,然后,一定要確保選中 Swift 語(yǔ)言,iPhone 選中,Use Core Data 不要選。

最后,選擇一個(gè)保存工程的地方并單擊 Create。

首先,我們需要做出決定的就是創(chuàng)建自定義控件需要繼承自哪個(gè)類,或者對(duì)哪個(gè)類進(jìn)行擴(kuò)展。

位了使自定義控件能夠在應(yīng)用程序中使用,你的類必須是 UIView 的一個(gè)子類。

如果你注意觀察蘋果的 UIKit 參考,會(huì)發(fā)現(xiàn)框架中的許多控件,例如 UILabel 和 UIWebView 都是直接繼承自 UIView 的。然而,也有極少數(shù),例如 UIButton 和 UISwitch 是繼承自 UIControl 的,如下繼承圖所示:

注意:iOS 中 UI 組件的完整類繼承圖,請(qǐng)看 UIKit Framework 參考。

UIControl 實(shí)現(xiàn)了 target-action 模式,這是一種將變化通知訂閱者的機(jī)制。UIControl 同樣還有一些與控件狀態(tài)相關(guān)的屬性。在本文中的自定義空間中,將使用到 target-action 模式,所以從 UIControl 開(kāi)始繼承使用將是一個(gè)非常好的切入點(diǎn)。

在 Project Navigator 中右鍵單擊 CustomSliderExample,選擇 New File…,然后選擇 iOS/Source/Cocoa Touch Class 模板,并單擊 Next。將類命名位 RangeSlider,在 Subclass of 字段中輸入 UIControl,并確保語(yǔ)言是 Swift。然后單擊 Next,并在默認(rèn)存儲(chǔ)位置中 Create 出新的類。

雖然編碼非常讓人愉悅,不過(guò)你可能也希望盡快看到自定義控件在屏幕中熏染出來(lái)的模樣!在寫自定義控件相關(guān)的任何代碼之前,你應(yīng)該先把這個(gè)控件添加到 view controller中,這樣就可以實(shí)時(shí)觀察控件的演進(jìn)程度。

打開(kāi) ViewController.swift,用下面的內(nèi)容替換之:

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 }

?

上面的代碼根據(jù)指定的 frame 實(shí)例化了一個(gè)全新的控件,然后將其添加到 view 中。為了在應(yīng)用程序背景中凸顯出控件,我們將控件的背景色被設(shè)置位了紅色。如果不把控件的背景色設(shè)置為紅色,那么控件中什么都沒(méi)有,可能會(huì)想,控件去哪里了!:]

編譯并運(yùn)行程序,將看到如下類似界面:

在開(kāi)始給控件添加可視元素之前,應(yīng)該先定義幾個(gè)屬性,用以在控件中記錄下各種信息。這也是開(kāi)始應(yīng)用程序編程接口 (API) 的開(kāi)始。

注意:控件中定義的方法和屬性是你決定用來(lái)暴露給別的開(kāi)發(fā)者使用的。稍后你將看到 API 設(shè)計(jì)相關(guān)的內(nèi)容,現(xiàn)在只需要緊跟就行!

添加默認(rèn)的控件屬性

打開(kāi) 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 }

上面定義的四個(gè)屬性用來(lái)描述控件的狀態(tài),提供最大值和最小值,以及有用戶設(shè)置的 upper 和 lower 兩個(gè)值。

好的控件設(shè)計(jì),應(yīng)該提供一些默認(rèn)的屬性值,否則將你的控件繪制到屏幕中時(shí),看起來(lái)會(huì)有點(diǎn)奇怪。

現(xiàn)在是時(shí)候開(kāi)始做控件的交互元素了,我們分別用兩個(gè) thumbs 表示高和低兩個(gè)值,并且讓這兩個(gè) thumbs 能夠滑動(dòng)。

Images vs. CoreGraphics

在屏幕中渲染控件有兩種方法:

1、Images - 為控件構(gòu)建不同的圖片,這些圖片代表控件的各種元素。
2、Core Graphics - 利用 layers 和 Core Graphics 組合起來(lái)熏染控件。

這兩種方法都有利有弊,下面來(lái)看看:

Images - 利用圖片來(lái)構(gòu)建控件是最簡(jiǎn)單的一種方法 - 只要你知道如何繪制圖片!:] 如果你想要讓開(kāi)發(fā)者能夠修改控件的外觀,那么你應(yīng)該將這些圖片以 UIImage 屬性的方式暴露出去。

通過(guò)圖片的方式來(lái)構(gòu)建的控件,給使用控件的人提供了非常大的靈活度。開(kāi)發(fā)者可以改變每一個(gè)像素,以及控件的詳細(xì)外觀,不過(guò)這需要非常熟練的圖形設(shè)計(jì)技能 - 并且通過(guò)代碼非常難以對(duì)控件做出修改。

Core Graphics - 利用 Core Graphics 構(gòu)建控件意味著你必須自己編寫渲染控件的代碼,這就需要付出更多的代價(jià)。不過(guò),這種方法可以創(chuàng)建更加靈活的 API。

使用 Core Graphics,可以把控件的所有特征都參數(shù)化,例如顏色、邊框厚度和弧度 - 幾乎每一個(gè)可視元素都通過(guò)繪制完成!這種方法運(yùn)行開(kāi)發(fā)者對(duì)控件做出任意調(diào)整,以適配相應(yīng)的需求。

本文中,你將學(xué)到第二種技術(shù) - 利用 Core Graphics 來(lái)熏染控件。

主要:有趣的時(shí),蘋果建議在他們提供的控件中使用圖片。這可能是蘋果知道每個(gè)控件的大小,他們不希望程序中出現(xiàn)太多的定制。也就是說(shuō),他們希望所有的應(yīng)用程序,都具有相似的外觀和體驗(yàn)。

打開(kāi) 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 個(gè) layer - trackLayer, lowerThumbLayer, 和 upperThumbLayer - 用來(lái)熏染滑塊控件的不同組件。thumbWidth 用來(lái)布局使用。

接下來(lái)就是控件默認(rèn)的一些圖形屬性。

在 RangeSlider 類中,添加一個(gè) 初始化方法,以及一個(gè) 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 }

?

初始化方法簡(jiǎn)單的創(chuàng)建了 3 個(gè) layer,并將它們以 children 的身份添加到控件的 root layer 中,然后通過(guò) updateLayerFrames 對(duì)這些 layer 的位置進(jìn)行更新定位! :]

最后,positionForValue 方法利用一個(gè)簡(jiǎn)單的比例,對(duì)控件的最小和最大值的范圍做了一個(gè)縮放,將值映射到屏幕中確定的一個(gè)位置。

接下來(lái),override一下 frame,通過(guò)將下面的代碼添加到 RangeSlider.swift 中,實(shí)現(xiàn)對(duì)屬性的觀察:

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

?

當(dāng) frame 發(fā)生變化時(shí),屬性觀察者會(huì)更新 layer frame。這一步是必須的,因?yàn)楫?dāng)控件初始化時(shí),傳入的 frame 并不是最終的 frame,就像 ViewController.swift 中的。

編譯并運(yùn)行程序,可以看到滑塊初具形狀!看起來(lái),如下圖所示:

還記得嗎,紅色是整個(gè)控件的背景色。藍(lán)色是滑塊的軌跡,綠色 thumb 是兩個(gè)代表兩端的值。

現(xiàn)在控件看起來(lái)有形狀了,不過(guò)幾乎所有的控件都提供了相關(guān)方法,讓用戶與之交互。

針對(duì)本文中的控件,用戶必須能夠通過(guò)拖拽 2 個(gè) thumb 來(lái)設(shè)置控件的范圍。你將處理這些交互,并通過(guò)控件更新 UI 和暴露的屬性。

添加交互邏輯

本文的交互邏輯需要存儲(chǔ)那個(gè) thumb 被拖拽了,并將效果反應(yīng)到 UI 中。控件的 layer 是放置該邏輯的最佳位置。

跟之前一樣,在 Xcode 中創(chuàng)建一個(gè)新的 Cocoa Touch Class,命名為 RangeSliderThumbLayer,繼承自 CALayer。

用下面的代碼替換掉 RangeSliderThumbLayer.swift 文件中的內(nèi)容:

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

?

上面的代碼中簡(jiǎn)單的添加了兩個(gè)屬性:一個(gè)表示這個(gè) thumb 是否 高亮 (highlighted),另外一個(gè)引用回父 range slider。由于 RangeSlider 有兩個(gè) thumb layer,所以將這里的引用設(shè)置位 weak,避免循環(huán)引用。

打開(kāi) RangeSlider.swift,修改一下 lowerThumbLayer 和 upperThumbLayer 兩個(gè)屬性的類型,用下面的代碼替換掉它們的定義:

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

還是在 RangeSlider.swift 中,找到 init,將下面的代碼添加進(jìn)去:

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

?

上面的代碼簡(jiǎn)單的將 layer 的 rangeSlider 屬性設(shè)置為 self。

編譯并運(yùn)行程序,界面看起來(lái)沒(méi)有什么變化。

現(xiàn)在你已經(jīng)有了 slider 的thumb layer - RangeSliderThumbLayer,然后需要給控件添加拖拽 thumb 的功能。

添加觸摸處理

打開(kāi) RangeSlider.swift,將下面這個(gè)屬性添加進(jìn)去:

?1 var previousLocation = CGPoint()?

這個(gè)屬性用來(lái)跟蹤記錄用戶的觸摸位置。

那么你該如何來(lái)跟蹤控件的各種觸摸和 release 時(shí)間呢?

UIControl 提供了一些方法來(lái)跟蹤觸摸。UIControl 的子類可以 override 這些方法,以實(shí)現(xiàn)自己的交互邏輯。

在自定義控件中,我們將 override 3 個(gè) UIControl 關(guān)鍵的方法:beginTrackingWithTouch, continueTrackingWithTouch 和 endTrackingWithTouch。

將下面的方法添加到 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 }

?

當(dāng)首次觸摸控件時(shí),會(huì)調(diào)用上面的方法。

代碼中,首先將觸摸事件的坐標(biāo)轉(zhuǎn)換到控件的坐標(biāo)空間。然后檢查每個(gè) thumb,是否觸摸位置在其上面。方法中返回的值將決定 UIControl 是否繼續(xù)跟蹤觸摸事件。

如果任意一個(gè) thumb 被 highlighted 了,就繼續(xù)跟蹤觸摸事件。

現(xiàn)在,有了初始的觸摸事件,我們需要處理用戶在屏幕上移動(dòng)的事件了。

將下面的方法添加到 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 會(huì)將傳入的值控制在某個(gè)確定的范圍。通過(guò)這個(gè)方法比嵌套調(diào)用 min/max 更容易理解。

下面我們根據(jù)注釋,來(lái)分析一下 continueTrackingWithTouch 方法都做了些什么:

  • 首先計(jì)算出位置增量,這個(gè)值決定著用戶手指移動(dòng)的數(shù)值。然后根據(jù)控件的最大值和最小值,對(duì)這個(gè)增量做轉(zhuǎn)換。
  • 根據(jù)用戶滑動(dòng)滑塊的距離,修正一下 upper 或 lower 值。
  • 設(shè)置 CATransaction 中的 disabledActions。這樣可以確保每個(gè) layer 的frame 立即得到更新,并且不會(huì)有動(dòng)畫(huà)效果。最后,調(diào)用 updateLayerFrames 方法將 thumb 移動(dòng)到正確的位置。
  • 至此,已經(jīng)編寫了移動(dòng)滑塊的代碼 - 不過(guò)我們還要處理觸摸和拖拽事件的結(jié)束。

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

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

    ?

    上面的代碼簡(jiǎn)單的將兩個(gè) thumb 還原位 non-highlighted 狀態(tài)。

    編譯并運(yùn)行程序,嘗試移動(dòng)滑塊!現(xiàn)在你應(yīng)該可以移動(dòng) thumb 了。

    你可能注意到當(dāng)在移動(dòng)滑塊時(shí),可以在控件之外的范圍對(duì)其拖拽,然后手指回到控件內(nèi),也不會(huì)丟失跟蹤。其實(shí)這在小屏幕的設(shè)備上,是非常重要的一個(gè)功能。

    值改變的通知

    現(xiàn)在你已經(jīng)有一個(gè)可以交互的控件了 - 用戶可以對(duì)其進(jìn)行操作,以設(shè)置范圍的大小值。但是如何才能把這些值的改變通知調(diào)用者:控件有新的值了呢?

    這里有多種模式可以實(shí)現(xiàn)值改變的通知: NSNotification,Key-Value-Observing (KVO), delegate 模式,target-action 模式等。有許多選擇!

    面對(duì)這么多的通知方式,那么我們?cè)撛趺催x擇呢?

    如果你研究過(guò) UIKit 控件,會(huì)發(fā)現(xiàn)它們并沒(méi)有使用 NSNotification,也不鼓勵(lì)使用 KVO。所以為了保持與 UIKit 的一致性,我們可以先排除這兩種方法。另外的兩種模式:delegate 和 target-action 被廣泛用于 UIKit 中。

    Delegate 模式 - delegate 模式需要提供一個(gè) protocol,里面有一些用于通知的方法。控件中有一個(gè)屬性,一般命名位 delegate,它可以是任意實(shí)現(xiàn)該協(xié)議的類。經(jīng)典的一個(gè)示例就是 UITableView 提供了 UITableViewDelegate protocol。注意,控件只接受單個(gè) delegate 實(shí)例。一個(gè) delegate 方法可以使用任意的參數(shù),所以可以給這樣的方法傳遞盡可能多的信息。

    Target-action 模式 - UIControl 基類已經(jīng)提供了 target-action 模式。當(dāng)控件狀態(tài)發(fā)生了改變,target 會(huì)獲得相應(yīng) action 的通知,該 action 是在 UIControlEvents 枚舉值做定義的。我們可以給控件的 action 提供多個(gè) target,另外還可以創(chuàng)建自定義事件 (查閱 UIControlEventApplicationReserved),自定義事件的數(shù)量不得超過(guò) 4 個(gè)。控件 action 針對(duì)某個(gè)事件,無(wú)法傳送任意的信息,所以當(dāng)事件觸發(fā)時(shí),不能用它來(lái)傳遞額外的信息。

    這兩種模式關(guān)鍵不同點(diǎn)如下:

    • 多播 (Multicast) - target-action 模式可以對(duì)改變事件進(jìn)行多播通知,而 delegate 模式只能綁定到單個(gè) delegate 實(shí)例上。
    • 靈活 (Flexibility) - 在 delegate 模式中,你可以定義自己的 protocol,這就意味著你可以控制信息的傳遞量。而 target-action 是無(wú)法傳遞額外信息的,客戶端只能在收到事件后,自行查詢信息。

    我們的 slider 控件不會(huì)有大量的狀態(tài)變化,也不需要提供大量的通知。唯一真正改變的就是控件的 upper 和 lower 值。

    基于這樣的情況,使用 target-action 模式是最好的。這也是為什么在本文開(kāi)頭的時(shí)候告訴你為什么這個(gè)控件要繼承自 UIControl。

    slider 的值是在 continueTrackingWithTouch:withEvent: 方法中進(jìn)行更新的,所以這個(gè)方法也是添加通知代碼的地方。

    打開(kāi) RangeSlider.swift,定位到 continueTrackingWithTouch 方法,然后將下面的代碼添加到 return true 語(yǔ)句前面:

    ?1 sendActionsForControlEvents(.ValueChanged)?

    上面的這行代碼就能將值改變事件通知給任意的訂閱者 target。

    現(xiàn)在我們應(yīng)該對(duì)這個(gè)事件進(jìn)行訂閱,并當(dāng)事件來(lái)了以后,作出相應(yīng)的處理。

    打開(kāi) ViewController.swift,將下面這行代碼添加到 viewDidLoad 尾部:

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

    通過(guò)上面的代碼,每次 slider 發(fā)送 UIControlEventValueChanged action 時(shí),都會(huì)調(diào)用 rangeSliderValueChanged 方法。

    將下面的代碼添加到 ViewController.swift 中:

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

    ?

    當(dāng) slider 值發(fā)生變化是,上面這個(gè)方法簡(jiǎn)單的將 slider 的值打印出來(lái)。

    編譯并運(yùn)行程序,并移動(dòng)一下 slider,可以在控制臺(tái)中看到控件的值,如下所示:

    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)

    ?

    看到 控件五顏六色的,你可能不高心,它開(kāi)起來(lái)就像水果沙拉一樣!

    現(xiàn)在是時(shí)候給控件換換面目了!

    結(jié)合 Core Graphics 對(duì)控件進(jìn)行修改

    首先,首選更新一下slider thumb 移動(dòng)的軌跡圖形。

    跟之前一樣,給工程添加另外一個(gè)繼承自 CALayer 的子類,命名為 RangeSliderTrackLayer。

    打開(kāi)剛剛添加的文件 RangeSliderTrackLayer.swift,然后用下面的內(nèi)容替換之:

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

    ?

    上面的代碼添加了一個(gè)到 slider 控件的引用,跟之前 thumb layer 做的一樣。

    打開(kāi) RangeSlider.swift 文件,找到 trackLayer 屬性,用剛剛創(chuàng)建的這個(gè)類對(duì)其實(shí)例化,如下所示:

    ?1 let trackLayer = RangeSliderTrackLayer()?

    接下來(lái),找到 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 - 并沒(méi)有再用那可怕的顏色了!然后將 contentsScale 因子設(shè)置位與設(shè)備的屏幕一樣,這樣可以確保所有的內(nèi)容在 retina 顯示屏中沒(méi)有問(wèn)題。

    下面還有一個(gè)事情需要做,就是將 viewDidLoad 中的如下代碼移除掉:

    ?1 rangeSlider.backgroundColor = UIColor.redColor()?

    編譯并運(yùn)行程序,看到什么了呢?

    什么東西都沒(méi)有?這是正確的!

    不要煩惱 - 我們只不過(guò)移除掉了在 layer 中花哨的測(cè)試顏色。控件依舊存在 - 只不過(guò)現(xiàn)在是白色的!

    由于許多開(kāi)發(fā)者希望能夠通過(guò)編碼對(duì)控件做各種配置,以使其外觀能夠效仿一些流行的程序,所以我們給 slider 添加一些屬性,運(yùn)行開(kāi)發(fā)者對(duì)其外觀做出一些定制。

    打開(kāi) 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?這個(gè)屬性在這里有點(diǎn)趣味 - 稍后你將發(fā)現(xiàn)其用途!

    接下來(lái),打來(lái) RangeSliderTrackLayer.swift。

    這個(gè) layer 用來(lái)渲染兩個(gè) thumb 滑動(dòng)的軌跡。目前它繼承自 CALayer,僅僅是繪制一個(gè)單一顏色。

    為了繪制軌跡,需要實(shí)現(xiàn)方法 drawInContext:,并利用 Core Pgraphics APIs 來(lái)進(jìn)行渲染。

    注意:要想深入學(xué)習(xí) Core Graphics,建議閱讀 Core Graphics 101 教程。

    將下面這個(gè)方法添加到 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 形狀確定,控件的背景色就會(huì)被填充,另外高亮范圍也會(huì)被填充。

    編譯并運(yùn)行程序,會(huì)看到新的 track layer 被完美的渲染出來(lái)!如下圖所示:

    給暴露出來(lái)的屬性設(shè)置不同的值,觀察一下它們是如何反應(yīng)到控件渲染中的。

    如果你對(duì) curvaceousness 做什么的還存在疑惑,那么試著修改一下它看看!

    接下來(lái)我們使用相同的方法來(lái)繪制 thumb layer。

    打開(kāi) 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 的形狀路徑,就會(huì)將其形狀填充好。注意繪制微弱的陰影看起來(lái)的效果就是 thumb 上方的軌跡。接下來(lái)是繪制邊框。最后,如果 thumb 是高亮的 - 也就是被移動(dòng)狀態(tài) - 那么就繪制微弱的灰色陰影效果。

    在運(yùn)行之前,還有最后一件事情要做。按照下面的代碼對(duì) highlighted 屬性的定義做出修改:

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

    這里,定義了一個(gè)屬性觀察者,這樣當(dāng)每次 highlighted 屬性修改時(shí),相應(yīng)的 layer 都會(huì)得到重繪。這會(huì)使得觸摸事件發(fā)生時(shí),填充色發(fā)生輕微的變動(dòng)。

    再次編譯并運(yùn)行程序,這下看起來(lái)會(huì)非常的有形狀,如下圖所示:

    不難發(fā)現(xiàn),用 Core Graphics 來(lái)繪制控件是非常值得做的。使用 Core Graphics 可以做出比通過(guò)圖片渲染方法更通用的控件。

    處理控件屬性的改變

    那么到現(xiàn)在,還有什么事情要做呢?控件現(xiàn)在看起來(lái)已經(jīng)非常的華麗了,它的外觀是通用的,并且也支持 target-action 通知。

    貌似已經(jīng)做完了?

    思考一下,如果當(dāng)控件熏染之后,如果通過(guò)代碼對(duì) slider 的屬性做了修改,會(huì)發(fā)生什么?例如,你希望修改一下 slider 的默認(rèn)值,或者修改一下 track highlight,表示出一個(gè)有效范圍。

    目前,還沒(méi)有任何代碼來(lái)觀察屬性的設(shè)置情況。我們需要將其添加到控件中。我們需要實(shí)現(xiàn)屬性觀察者,來(lái)更新控件的 frame 或者重繪控件。打開(kāi) RangeSlider.swift,按照下面的代碼對(duì)屬性的聲明作出修改:

    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 }

    ?

    一般情況,我們需要根據(jù)依賴的屬性,調(diào)用 setNeedsDisplay 方法將對(duì)于的 layer 進(jìn)行重新處理。setLayerFrames 方法會(huì)對(duì)控件的布局作出調(diào)整。

    現(xiàn)在,找到 updateLayerFrames,然后將下面的代碼添加到該方法的頂部:

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

    并將下面的代碼添加到方法的尾部:

    ?1 CATransaction.commit()?

    上面的代碼將整個(gè) frame 的更新封裝到一個(gè)事物處理中,這樣可以讓界面重繪變得流暢。同樣還明確的把 layer 中的動(dòng)畫(huà)禁用掉,跟之前一樣,這樣 layer frame 的更新會(huì)變得即時(shí)。

    由于現(xiàn)在每當(dāng) upper 和 lower 值發(fā)生變動(dòng)時(shí), frame 會(huì)自動(dòng)更新了,所以,找到 continueTrackingWithTouch 方法,并將下面的代碼刪除掉:

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

    ?

    上面的這些代碼就能夠確保屬性變化時(shí),能夠反應(yīng)到 slider 控件中。

    為了確保代碼無(wú)誤,我們需要寫點(diǎn)測(cè)試 case 進(jìn)行測(cè)試。

    打開(kāi) 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 }

    ?

    上面的代碼會(huì)在暫停 1 秒鐘之后,對(duì)控件的一些屬性做出更新。其中將 track highlight 的顏色修改為紅色,并修改了 slider 和 thumb 的形狀。

    編譯并運(yùn)行程序,一秒鐘之后,你看到 slider 由:

    變?yōu)?#xff1a;

    很容易不是嗎?

    上面剛剛添加到 view controller 中的代碼,演示了一個(gè)非常有趣,而又經(jīng)常被忽略的內(nèi)容 - 對(duì)開(kāi)發(fā)的自定義控件做充分的測(cè)試。當(dāng)你在開(kāi)發(fā)一個(gè)自定義控件時(shí),你需要負(fù)責(zé)對(duì)所有的屬性和外觀做出驗(yàn)證。這里有一個(gè)好的方法就是創(chuàng)建不同的按鈕和滑塊 (它們連接到控件的不同屬性) 對(duì)控件做出測(cè)試。這樣,你就可以實(shí)時(shí)修改控件的屬性,并實(shí)時(shí)觀察到它們的結(jié)果。

    何去何從?

    現(xiàn)在我們的 range slider 控件已經(jīng)完成開(kāi)發(fā),并可以在程序中使用了!你可以在這里下載到完整的工程(方便的話給個(gè)小小的star...)。

    不過(guò),創(chuàng)建通用性自定義控件的一個(gè)關(guān)鍵好處就是你可以將其用于不同的工程 - 并且分享給別的開(kāi)發(fā)者使用。

    準(zhǔn)備好了嗎?

    實(shí)際上還沒(méi)有。在分享自定義控件之前,還有一些事情需要考慮:

    希望通過(guò)本文的學(xué)習(xí),你已經(jīng)能愉悅的創(chuàng)建 slider 控件了,可能你還希望構(gòu)建自己的自定義控件。如果你做了,可以在本文的評(píng)論中分享一下 - 我們非常想看到你的創(chuàng)作!(分享了)

    本文轉(zhuǎn)載自(破船之家)

    轉(zhuǎn)載于:https://www.cnblogs.com/chenyihang/p/5640079.html

    總結(jié)

    以上是生活随笔為你收集整理的如何用 Swift 语言构建一个自定控件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。