UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.11.19 星期一

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面,用戶交互也是通過UIKit進(jìn)行的羔味。感興趣的參考上面幾篇文章燕垃。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)

開始

首先看一下寫作環(huán)境

Swift 4.2, iOS 12, Xcode 10

用戶界面控件是任何應(yīng)用程序中最重要的構(gòu)建塊之一。它們充當(dāng)圖形組件,允許用戶查看應(yīng)用程序并與之交互卵牍。 Apple提供了一組控件,例如UITextField沦泌,UIButtonUISwitch糊昙。有了這個(gè)預(yù)先存在的控件工具箱,您可以創(chuàng)建各種各樣的用戶界面谢谦。

iOS自定義控件只不過是您自己創(chuàng)建的控件释牺。自定義控件,如標(biāo)準(zhǔn)控件回挽,應(yīng)該是通用的没咙。你會(huì)發(fā)現(xiàn)有一個(gè)活躍且充滿活力的開發(fā)者社區(qū),他們喜歡分享他們的iOS自定義控件創(chuàng)作千劈,前提是他們都是這些東西祭刚。

在本教程中,您將實(shí)現(xiàn)RangeSlider iOS自定義控件墙牌。此控件類似于雙端滑塊涡驮,可讓您選擇最小值和最大值。您將涉及擴(kuò)展現(xiàn)有控件喜滨,設(shè)計(jì)和實(shí)現(xiàn)控件API以及如何與開發(fā)社區(qū)共享新控件等概念捉捅。

是時(shí)候開始定制了!

假設(shè)您正在開發(fā)一個(gè)搜索待售物業(yè)列表的應(yīng)用程序虽风。 這個(gè)虛構(gòu)的應(yīng)用程序允許用戶過濾搜索結(jié)果棒口,使其落在特定的價(jià)格范圍內(nèi)。

您可以提供一個(gè)界面焰情,向用戶顯示一對(duì)UISlider控件陌凳,一個(gè)用于設(shè)置最高價(jià)格剥懒,另一個(gè)用于設(shè)置最低價(jià)格内舟。 但是,此界面無法幫助用戶查看價(jià)格范圍初橘。 使用兩個(gè)指示器呈現(xiàn)單個(gè)滑塊以指示其搜索條件的高價(jià)和低價(jià)范圍會(huì)更好验游。

您可以通過繼承UIView并創(chuàng)建一個(gè)定制視圖來可視化價(jià)格范圍來構(gòu)建此范圍滑塊。 對(duì)于您的應(yīng)用程序的上下文來說這沒什么問題 - 但將它移植到其他應(yīng)用程序會(huì)很困難保檐。

將這個(gè)新組件盡可能地設(shè)為通用是一個(gè)更好的主意耕蝉,以便您以后可以重用它。 這是自定義控件的本質(zhì)夜只。

創(chuàng)建iOS自定義控件時(shí)需要做出的第一個(gè)決定是將現(xiàn)有類子類化或擴(kuò)展以制作新控件垒在。

您的類必須是UIView子類,才能在應(yīng)用程序的UI中使用它扔亥。

如果你檢查Apple的UIKit引用场躯,你會(huì)看到許多框架控件谈为,比如UILabelUIWebView,直接子類UIView踢关。 但是伞鲫,有一些,如UIButtonUISwitch签舞,它們是UIControl的子類秕脓,如下面的層次結(jié)構(gòu)所示:

注意:有關(guān)UI組件的完整類層次結(jié)構(gòu),請(qǐng)查看 UIKit Framework Reference儒搭。

在本教程中吠架,您將繼承UIControl的子類。

在Xcode中打開啟動(dòng)項(xiàng)目搂鲫。 您將滑塊控制代碼放在RangeSlider.swift中诵肛。 但是,在為控件編寫任何代碼之前默穴,請(qǐng)將其添加到視圖控制器中怔檩,以便您可以觀察其演變。

打開ViewController.swift并使用以下內(nèi)容替換其內(nèi)容:

import UIKit

class ViewController: UIViewController {
  let rangeSlider = RangeSlider(frame: .zero)
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    rangeSlider.backgroundColor = .red
    view.addSubview(rangeSlider)
  }
  
  override func viewDidLayoutSubviews() {
    let margin: CGFloat = 20
    let width = view.bounds.width - 2 * margin
    let height: CGFloat = 30
    
    rangeSlider.frame = CGRect(x: 0, y: 0,
                               width: width, height: height)
    rangeSlider.center = view.center
  }
}

此代碼在給定框架中創(chuàng)建控件的實(shí)例蓄诽,并將其添加到視圖中薛训。 它還將背景顏色設(shè)置為紅色,以便控件在屏幕上可見仑氛。

構(gòu)建并運(yùn)行您的應(yīng)用程序乙埃。 您應(yīng)該看到類似于以下內(nèi)容的內(nèi)容:

在將可視元素添加到控件之前,您需要一些屬性來跟蹤控件的狀態(tài)锯岖。 這將構(gòu)成控件的應(yīng)用程序編程接口(簡(jiǎn)稱API)的開始介袜。

注意:您的控件的API定義了您決定向?qū)⑹褂媚目丶钠渌_發(fā)人員公開的方法和屬性。


Adding Default Control Properties - 添加默認(rèn)控件屬性

打開RangeSlider.swift并用以下代碼替換代碼:

import UIKit

class RangeSlider: UIControl {
  var minimumValue: CGFloat = 0
  var maximumValue: CGFloat = 1
  var lowerValue: CGFloat = 0.2
  var upperValue: CGFloat = 0.8
}

您只需要這四個(gè)屬性來描述此控件的狀態(tài)出吹。 您可以提供范圍的最大值和最小值遇伞,以及用戶設(shè)置的上限和下限值。

精心設(shè)計(jì)的控件應(yīng)該定義一些默認(rèn)的屬性值捶牢,否則當(dāng)它在屏幕上繪制時(shí)你的控件看起來很奇怪鸠珠。

現(xiàn)在是時(shí)候處理控件的交互元素了:即用指示器表示高值和低值以及指示器將滑動(dòng)的軌道。


CoreGraphics vs. Images

您可以在屏幕上呈現(xiàn)控件的兩種主要方法:

  • 1) CoreGraphics:使用圖層和CoreGraphics的組合渲染您的控件秋麸。
  • 2) Images:創(chuàng)建代表控件各種元素的圖像渐排。

每種技術(shù)都有利弊,如下所述:

  • Core Graphics:使用Core Graphics構(gòu)建控件意味著您必須自己編寫渲染代碼灸蟆,這需要更多的努力驯耻。但是,此技術(shù)允許您創(chuàng)建更靈活的API。

使用Core Graphics可缚,您可以參數(shù)化控件的每個(gè)功能孽水,例如顏色,邊框粗細(xì)和曲率 - 幾乎所有用于繪制控件的視覺元素城看!

  • Images:使用圖像構(gòu)建控件可能是創(chuàng)建控件的最簡(jiǎn)單選項(xiàng)女气。如果您希望其他開發(fā)人員能夠更改控件的外觀,則需要將這些圖像公開為UIImage屬性测柠。

使用images為使用您的控件的開發(fā)人員提供了最大的靈活性炼鞠。開發(fā)人員可以更改控件外觀的每個(gè)像素和每個(gè)細(xì)節(jié),但這需要出色的圖形設(shè)計(jì)技能 - 并且很難從代碼中修改控件轰胁。

在本教程中谒主,您將使用兩者。您將使用images渲染指示器和CoreGraphics來渲染軌道圖層赃阀。

注意:有趣的是霎肯,Apple也傾向于選擇在其控件中使用images。這很可能是因?yàn)樗麄冎烂總€(gè)控件的大小榛斯,并且不希望允許過多的自定義观游。畢竟,他們希望所有應(yīng)用程序最終都具有類似的外觀驮俗。


Adding thumbs to your custom control - 將指示器添加到自定義控件

打開RangeSlider.swift并添加以下屬性懂缕,就在上面定義的屬性之后:

var thumbImage = #imageLiteral(resourceName: "Oval")

private let trackLayer = CALayer()
private let lowerThumbImageView = UIImageView()
private let upperThumbImageView = UIImageView()

trackLayerlowerThumbImageViewupperThumbImageView用于渲染滑塊控件的各種組件王凑。

仍然在RangeSlider中鞍时,添加一個(gè)初始化器:

override init(frame: CGRect) {
  super.init(frame: frame)
  
  trackLayer.backgroundColor = UIColor.blue.cgColor
  layer.addSublayer(trackLayer)
  
  lowerThumbImageView.image = thumbImage
  addSubview(lowerThumbImageView)
  
  upperThumbImageView.image = thumbImage
  addSubview(upperThumbImageView)
}

required init?(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

此初始化程序?qū)D層和視圖添加到控件臭墨。

要查看添加的元素,您需要設(shè)置其frame桑孩。 在初始化器之后添加以下代碼:

// 1
private func updateLayerFrames() {
  trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height / 3)
  trackLayer.setNeedsDisplay()
  lowerThumbImageView.frame = CGRect(origin: thumbOriginForValue(lowerValue),
                                     size: thumbImage.size)
  upperThumbImageView.frame = CGRect(origin: thumbOriginForValue(upperValue),
                                     size: thumbImage.size)
}
// 2
func positionForValue(_ value: CGFloat) -> CGFloat {
  return bounds.width * value
}
// 3
private func thumbOriginForValue(_ value: CGFloat) -> CGPoint {
  let x = positionForValue(value) - thumbImage.size.width / 2.0
  return CGPoint(x: x, y: (bounds.height - thumbImage.size.height) / 2.0)
}

以下是這些方法的內(nèi)容:

  • 1) 在第一種方法中肋层,您將trackLayer居中并使用thumbOriginForValue(_ :)計(jì)算指示器的位置原叮。
  • 2) 此方法將給定值縮放到邊界的上下文楣颠。
  • 3) 最后瓢阴,thumbOriginForValue(_ :)返回位置,以便指示器在給定縮放值的情況下居中瓣戚。

將以下代碼添加到init(frame :)的末尾以調(diào)用更新方法:

updateLayerFrames()

接下來端圈,通過將以下內(nèi)容添加到類的頂部來重寫frame并實(shí)現(xiàn)屬性觀察器:

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

屬性觀察者在幀更改時(shí)更新圖層幀焦读。 當(dāng)使用不像ViewController.swift中的最終frame那樣初始化控件時(shí)子库,這是必需的。

構(gòu)建并運(yùn)行您的應(yīng)用程序矗晃。 你的滑塊開始成型仑嗅!

紅色是整個(gè)控件的背景色;藍(lán)色是滑塊的軌道顏色;藍(lán)色圓圈是上下兩個(gè)值的兩個(gè)指示器仓技。

您的控件開始在視覺上形成鸵贬,但您無法與它進(jìn)行交互!

為了您的控制脖捻,用戶必須能夠拖動(dòng)每個(gè)指示器以設(shè)置所需的控件范圍阔逼。 您將處理這些交互并更新UI和控件公開的屬性。


Adding Touch Handlers - 添加Touch處理

打開RangeSlider.swift并添加以下屬性以及其他屬性:

private var previousLocation = CGPoint()

您可以使用此屬性來跟蹤觸摸位置地沮。

您如何跟蹤控件的各種觸摸和釋放事件嗜浮?

UIControl提供了幾種跟蹤觸摸的方法。 UIControl的子類可以重寫這些方法以添加自己的交互邏輯摩疑。

在你的自定義控件中危融,你將重寫UIControl的三個(gè)關(guān)鍵方法:beginTracking(_:with :)continueTracking(_:with :)endTracking(_:with :)雷袋。

將以下代碼添加到RangeSlider.swift文件的末尾:

extension RangeSlider {
  override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    // 1
    previousLocation = touch.location(in: self)
    
    // 2
    if lowerThumbImageView.frame.contains(previousLocation) {
      lowerThumbImageView.isHighlighted = true
    } else if upperThumbImageView.frame.contains(previousLocation) {
      upperThumbImageView.isHighlighted = true
    }
    
    // 3
    return lowerThumbImageView.isHighlighted || upperThumbImageView.isHighlighted
  }
}

當(dāng)用戶第一次觸摸控件時(shí)吉殃,iOS會(huì)調(diào)用此方法。 以下是它的工作原理:

  • 1) 首先楷怒,它將觸摸事件轉(zhuǎn)換為控件的坐標(biāo)空間蛋勺。
  • 2) 接下來,它會(huì)檢查每個(gè)指示器視圖以查看觸摸是否在其frame內(nèi)鸠删。
  • 3) 返回值通知UIControl超類是否應(yīng)跟蹤后續(xù)觸摸迫卢。 如果突出顯示任一指示器,則繼續(xù)跟蹤觸摸事件

現(xiàn)在您已經(jīng)有了初始觸摸事件冶共,當(dāng)用戶的手指在屏幕上移動(dòng)時(shí)乾蛤,您需要處理事件。

beginTracking(_:with :)之后添加以下方法:

override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
  let location = touch.location(in: self)
  
  // 1
  let deltaLocation = location.x - previousLocation.x
  let deltaValue = (maximumValue - minimumValue) * deltaLocation / bounds.width
  
  previousLocation = location
  
  // 2
  if lowerThumbImageView.isHighlighted {
    lowerValue += deltaValue
    lowerValue = boundValue(lowerValue, toLowerValue: minimumValue,
                            upperValue: upperValue)
  } else if upperThumbImageView.isHighlighted {
    upperValue += deltaValue
    upperValue = boundValue(upperValue, toLowerValue: lowerValue,
                            upperValue: maximumValue)
  }
  
  // 3
  CATransaction.begin()
  CATransaction.setDisableActions(true)
  
  updateLayerFrames()
  
  CATransaction.commit()
  
  return true
}

// 4
private func boundValue(_ value: CGFloat, toLowerValue lowerValue: CGFloat, 
                        upperValue: CGFloat) -> CGFloat {
  return min(max(value, lowerValue), upperValue)
}

這是代碼細(xì)分:

  • 1) 首先捅僵,計(jì)算增量位置家卖,該位置確定用戶手指行進(jìn)的點(diǎn)數(shù)。 然后庙楚,您可以根據(jù)控件的最小值和最大值將其轉(zhuǎn)換為縮放的delta值上荡。
  • 2) 在這里,您可以根據(jù)用戶拖動(dòng)滑塊的位置調(diào)整上限或下限值馒闷。
  • 3) 此部分在CATransaction中設(shè)置disabledActions標(biāo)志酪捡。 這可確保立即應(yīng)用每個(gè)圖層的frame更改,而沒有動(dòng)畫纳账。 最后逛薇,調(diào)用updateLayerFrames將指示器移動(dòng)到正確的位置。
  • 4) boundValue(_:toLowerValue:upperValue :)限制傳入的值疏虫,使其在指定范圍內(nèi)永罚。 使用此輔助函數(shù)比嵌套的min/max調(diào)用更容易閱讀啤呼。

您已經(jīng)編碼了滑塊的拖動(dòng),但您仍需要處理觸摸和拖動(dòng)事件的結(jié)束呢袱。

continueTracking(_:with :)之后添加以下方法:

override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
  lowerThumbImageView.isHighlighted = false
  upperThumbImageView.isHighlighted = false
}

此代碼將兩個(gè)指示器重置為非突出顯示狀態(tài)官扣。

構(gòu)建并運(yùn)行您的項(xiàng)目,并使用閃亮的新滑塊羞福! 你應(yīng)該可以拖動(dòng)指示器惕蹄。

您會(huì)注意到,當(dāng)滑塊跟蹤觸摸時(shí)治专,您可以將手指拖動(dòng)到控件的邊界之外焊唬,然后返回控件內(nèi),而不會(huì)丟失跟蹤操作看靠。 對(duì)于具有低精度指示設(shè)備的小屏幕設(shè)備而言赶促,這是一個(gè)重要的可用性功能 - 或者因?yàn)樗鼈兏R姟?/p>


Notifying Changes - 通知改變

您現(xiàn)在擁有一個(gè)交互式控件,用戶可以操作它來設(shè)置上限和下限挟炬。但是鸥滨,您如何將這些更改通知傳達(dá)給調(diào)用應(yīng)用程序,以便應(yīng)用程序知道控件具有新值谤祖?

您可以實(shí)現(xiàn)許多不同的模式來提供更改通知:NSNotification婿滓,鍵值觀察(KVO),代理模式粥喜,target-action模式以及許多其他模式凸主。這么多選擇!

那么該怎么辦额湘?

如果您查看UIKit控件卿吐,您會(huì)發(fā)現(xiàn)他們不使用NSNotification或鼓勵(lì)使用KVO,因此為了與UIKit保持一致锋华,您可以排除這兩個(gè)選項(xiàng)嗡官。另外兩種模式 - 代理模式,target-action模式 - 在UIKit中廣泛使用毯焕。

以下是對(duì)代理和target-action模式的詳細(xì)分析:

  • Delegate pattern - 委托模式:使用委托模式衍腥,您提供的協(xié)議包含用于一系列通知的方法。該控件有一個(gè)屬性纳猫,通常名為delegate婆咸,它接受任何實(shí)現(xiàn)此協(xié)議的類。一個(gè)典型的例子是UITableView芜辕,它提供了UITableViewDelegate協(xié)議尚骄。請(qǐng)注意,這些控件只接受單個(gè)委托實(shí)例物遇。委托方法可以使用任意數(shù)量的參數(shù)乖仇,因此您可以根據(jù)需要向這些方法傳遞盡可能多的信息憾儒。

  • Target-action pattern - 目標(biāo)操作模式:UIControl基類提供目標(biāo)操作模式询兴。當(dāng)發(fā)生控制狀態(tài)的改變時(shí)乃沙,目標(biāo)被通知由一個(gè)UIControlEvents枚舉值描述的事件。您可以提供多個(gè)目標(biāo)來控制操作诗舰,雖然可以創(chuàng)建自定義事件(請(qǐng)參閱UIControlEventApplicationReserved)警儒,但您最多只能有四個(gè)自定義事件】舾控制操作無法通過事件發(fā)送任何信息蜀铲,因此在觸發(fā)事件時(shí),它們不能用于傳遞額外信息属百。

這兩種模式的主要區(qū)別如下:

  • Multicast:目標(biāo)操作模式multicasts其更改通知记劝,而委托模式綁定到單個(gè)委托實(shí)例。
  • Flexibility:您可以在委托模式中自己定義協(xié)議族扰,這意味著您可以精確控制傳遞的信息量厌丑。目標(biāo)操作無法傳遞額外信息,客戶端在收到事件后必須自行查找渔呵。

您的范圍滑塊控件沒有為您提供通知所需的大量狀態(tài)更改或交互怒竿。唯一改變的是控件的上限和下限值。

在這種情況下扩氢,target-action模式非常有意義耕驰。這是您在iOS自定義控件教程開始時(shí)將UIControl子類化的原因之一。

它現(xiàn)在有意義录豺!

滑塊值在continueTracking(_:with :)內(nèi)更新朦肘,因此您需要添加通知代碼。

打開RangeSlider.swift双饥,找到continueTracking(_:with :)并在return true語句之前添加以下內(nèi)容:

sendActions(for: .valueChanged)

這就是通知任何訂閱目標(biāo)的變化所需要做的全部工作厚骗。

現(xiàn)在您已經(jīng)完成了通知處理,您應(yīng)該將其連接到您的應(yīng)用程序兢哭。

打開ViewController.swift并將以下方法添加到類的底部:

@objc func rangeSliderValueChanged(_ rangeSlider: RangeSlider) {
  let values = "(\(rangeSlider.lowerValue) \(rangeSlider.upperValue))"
  print("Range slider value changed: \(values)")
}

此方法將范圍滑塊值記錄到控制臺(tái)领舰,以證明控件正在按計(jì)劃發(fā)送通知。

現(xiàn)在迟螺,將以下代碼添加到viewDidLoad()的末尾:

rangeSlider.addTarget(self, action: #selector(rangeSliderValueChanged(_:)),
                      for: .valueChanged)

每次范圍滑塊發(fā)送valueChanged事件時(shí)冲秽,都會(huì)調(diào)用rangeSliderValueChanged(_ :)

構(gòu)建并運(yùn)行您的應(yīng)用程序矩父,并來回移動(dòng)滑塊锉桑。 您將在控制臺(tái)中看到控件的值,類似于:

Range slider value changed: (0.117670682730924 0.390361445783134)
Range slider value changed: (0.117670682730924 0.38835341365462)
Range slider value changed: (0.117670682730924 0.382329317269078)

您現(xiàn)在可能已經(jīng)厭倦了查看多色范圍滑塊UI窍株。 它看起來像一個(gè)憤怒的水果沙拉民轴! 是時(shí)候給控件一個(gè)急需的換裝攻柠。


Modifying Your Control With Core Graphics - 使用核心圖形修改您的控件

首先,您將更新滑塊指示器移動(dòng)的軌道圖形后裸。

RangeSliderTrackLayer.swift中瑰钮,使用以下代碼替換代碼:

import UIKit

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

此代碼將引用添加回RangeSlider。 由于滑塊擁有軌道微驶,因此引用是一個(gè)weak變量浪谴,以避免循環(huán)引用。

打開RangeSlider.swift因苹,找到trackLayer屬性并將其修改為新圖層類的實(shí)例:

private let trackLayer = RangeSliderTrackLayer()

現(xiàn)在苟耻,找到init(frame :)并將其替換為以下內(nèi)容:

override init(frame: CGRect) {
  super.init(frame: frame)
  
  trackLayer.rangeSlider = self
  trackLayer.contentsScale = UIScreen.main.scale
  layer.addSublayer(trackLayer)
  
  lowerThumbImageView.image = thumbImage
  addSubview(lowerThumbImageView)
  
  upperThumbImageView.image = thumbImage
  addSubview(upperThumbImageView)    
}

上面的代碼確保新的軌道圖層具有對(duì)范圍滑塊的引用,并刪除默認(rèn)的背景顏色扶檐。 設(shè)置contentsScale因子以匹配設(shè)備屏幕的因子可確保視網(wǎng)膜顯示屏上的一切都清晰凶杖。

還有一點(diǎn):刪除控件的紅色背景。

打開ViewController.swift款筑,在viewDidLoad()中找到以下行并將其刪除:

rangeSlider.backgroundColor = .red

立即構(gòu)建并運(yùn)行智蝠。 你看到了什么?

指示器處于懸浮狀態(tài)醋虏?

不要擔(dān)心 - 你剛剛刪除了華而不實(shí)的測(cè)試顏色寻咒。 你的控件仍在那里,但現(xiàn)在你有一個(gè)空白的畫布來裝扮它颈嚼。

由于大多數(shù)開發(fā)人員喜歡它時(shí)控件可以配置為模擬他們正在編碼的特定應(yīng)用程序的外觀和感覺毛秘,因此您將向滑塊添加一些屬性以允許自定義控件的外觀。

打開RangeSlider.swift并在upperValue屬性下添加以下屬性:

var trackTintColor = UIColor(white: 0.9, alpha: 1)
var trackHighlightTintColor = UIColor(red: 0, green: 0.45, blue: 0.94, alpha: 1)

接下來阻课,打開RangeSliderTrackLayer.swift叫挟。

此圖層渲染兩個(gè)指示器滑動(dòng)的軌道。 它目前繼承自CALayer限煞,它只呈現(xiàn)純色抹恳。

要繪制軌道,您需要實(shí)現(xiàn)draw(in :)并使用Core Graphics API來執(zhí)行渲染署驻。

將以下方法添加到RangeSliderTrackLayer

override func draw(in ctx: CGContext) {
  guard let slider = rangeSlider else {
    return
  }
  
  let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
  ctx.addPath(path.cgPath)
  
  ctx.setFillColor(slider.trackTintColor.cgColor)
  ctx.fillPath()
  
  ctx.setFillColor(slider.trackHighlightTintColor.cgColor)
  let lowerValuePosition = slider.positionForValue(slider.lowerValue)
  let upperValuePosition = slider.positionForValue(slider.upperValue)
  let rect = CGRect(x: lowerValuePosition, y: 0,
                    width: upperValuePosition - lowerValuePosition,
                    height: bounds.height)
  ctx.fill(rect)
}

剪裁軌道形狀后奋献,您將填充背景。 之后旺上,您將填寫突出顯示的范圍瓶蚂。

構(gòu)建并運(yùn)行以查看您的新軌道圖層! 它看起來像這樣:


Handling Changes to Control Properties - 處理對(duì)控件屬性的更改

控件現(xiàn)在看起來很時(shí)髦宣吱;視覺樣式是多功能的窃这,它支持target-action通知。

聽起來你已經(jīng)完成了征候?

想一想如果在渲染后在代碼中設(shè)置了一個(gè)范圍滑塊屬性會(huì)發(fā)生什么杭攻。 例如祟敛,您可能希望將滑塊范圍更改為某個(gè)預(yù)設(shè)值,或更改軌道突出顯示以指示有效范圍兆解。

目前馆铁,沒有什么觀察屬性setter。 您需要將該功能添加到控件中痪宰。 您需要實(shí)現(xiàn)更新控件frame或繪圖的屬性觀察器叼架。

打開RangeSlider.swift并更改以下屬性的屬性聲明畔裕,如下所示:

var minimumValue: CGFloat = 0 {
  didSet {
    updateLayerFrames()
  }
}

var maximumValue: CGFloat = 1 {
  didSet {
    updateLayerFrames()
  }
}

var lowerValue: CGFloat = 0.2 {
  didSet {
    updateLayerFrames()
  }
}

var upperValue: CGFloat = 0.8 {
  didSet {
    updateLayerFrames()
  }
}

var trackTintColor = UIColor(white: 0.9, alpha: 1) {
  didSet {
    trackLayer.setNeedsDisplay()
  }
}

var trackHighlightTintColor = UIColor(red: 0, green: 0.45, blue: 0.94, alpha: 1) {
  didSet {
    trackLayer.setNeedsDisplay()
  }
}

var thumbImage = #imageLiteral(resourceName: "Oval") {
  didSet {
    upperThumbImageView.image = thumbImage
    lowerThumbImageView.image = thumbImage
    updateLayerFrames()
  }
}

var highlightedThumbImage = #imageLiteral(resourceName: "HighlightedOval") {
  didSet {
    upperThumbImageView.highlightedImage = highlightedThumbImage
    lowerThumbImageView.highlightedImage = highlightedThumbImage
    updateLayerFrames()
  }
}

您可以調(diào)用setNeedsDisplay()來更改軌道圖層衣撬,并為每個(gè)其他更改調(diào)用updateLayerFrames()。 更改thumbImagehighlightedThumbImage時(shí)扮饶,還會(huì)更改其各自圖像視圖的屬性具练。

您還添加了一個(gè)新屬性!highlightThumbImage顯示指示器圖像視圖高亮顯示甜无。 它有助于為用戶提供有關(guān)他們?nèi)绾闻c控件交互的更多反饋扛点。

現(xiàn)在,找到updateLayerFrames()并將以下內(nèi)容添加到方法的頂部:

CATransaction.begin()
CATransaction.setDisableActions(true)

將以下內(nèi)容添加到方法的最底部:

CATransaction.commit()

此代碼將整個(gè)幀更新包裝到一個(gè)事務(wù)中岂丘,以使重新流渲染變得平滑陵究。 它還會(huì)禁用圖層上的隱式動(dòng)畫,就像之前一樣奥帘,因此圖層frame會(huì)立即更新铜邮。

由于您現(xiàn)在每次更改上限值和下限值時(shí)自動(dòng)更新frame,請(qǐng)?jiān)?code>continueTracking(_:with :)中找到以下代碼并將其刪除:

// 3
CATransaction.begin()
CATransaction.setDisableActions(true)

updateLayerFrames()

CATransaction.commit()

這就是讓范圍滑塊對(duì)屬性更改做出反應(yīng)所需要做的全部工作寨蹋。

但是松蒜,您現(xiàn)在需要更多代碼來測(cè)試新的屬性觀察器,并確保所有內(nèi)容都已連接并按預(yù)期工作已旧。

打開ViewController.swift并將以下代碼添加到viewDidLoad()的末尾:

let time = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: time) {
    self.rangeSlider.trackHighlightTintColor = .red
    self.rangeSlider.thumbImage = #imageLiteral(resourceName: "RectThumb")
    self.rangeSlider.highlightedThumbImage = 
      #imageLiteral(resourceName: "HighlightedRect")
}

這會(huì)在一秒延遲后更新某些控件的屬性秸苗。 它還將軌道高亮顏色更改為紅色,將指示器圖像更改為矩形运褪。

構(gòu)建并運(yùn)行您的項(xiàng)目惊楼。 一秒鐘之后,您將看到范圍滑塊從此更改:

更改為:

已經(jīng)很好了秸讹!

您的范圍滑塊現(xiàn)在功能齊全檀咙,可以在您自己的應(yīng)用程序中使用!

不僅如此嗦枢。在共享自定義控件之前攀芯,請(qǐng)考慮以下幾點(diǎn):

有很多地方可以開始與全世界共享自定義控件。以下是一些開始的建議:

  • GitHub - 分享開源項(xiàng)目最受歡迎的地方之一文虏。 GitHub上已有許多iOS自定義控件侣诺。它允許人們通過分配代碼進(jìn)行其他控制或在現(xiàn)有控件上引發(fā)問題來輕松訪問代碼并進(jìn)行協(xié)作殖演。
  • CocoaPods - 為了讓人們可以輕松地將控件添加到他們的項(xiàng)目中,您可以通過CocoaPods共享它年鸳,CocoaPods是iOS和macOS項(xiàng)目的依賴管理器趴久。
  • Cocoa Controls - 此站點(diǎn)提供商業(yè)和開源控件的目錄。 Cocoa Controls涵蓋的許多開源控件都托管在GitHub上搔确,這是推廣創(chuàng)作的好方法彼棍。

后記

本篇主要講述了自定義控件:可重復(fù)使用的滑塊,感興趣的給個(gè)贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膳算,一起剝皮案震驚了整個(gè)濱河市座硕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涕蜂,老刑警劉巖华匾,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異机隙,居然都是意外死亡蜘拉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門有鹿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旭旭,“玉大人,你說我怎么就攤上這事葱跋〕旨模” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵年局,是天一觀的道長(zhǎng)际看。 經(jīng)常有香客問我,道長(zhǎng)矢否,這世上最難降的妖魔是什么仲闽? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮僵朗,結(jié)果婚禮上赖欣,老公的妹妹穿的比我還像新娘。我一直安慰自己验庙,他們只是感情好顶吮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著粪薛,像睡著了一般悴了。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天湃交,我揣著相機(jī)與錄音熟空,去河邊找鬼。 笑死搞莺,一個(gè)胖子當(dāng)著我的面吹牛息罗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播才沧,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼迈喉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了温圆?” 一聲冷哼從身側(cè)響起挨摸,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捌木,沒想到半個(gè)月后油坝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫉戚,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刨裆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了彬檀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帆啃。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖窍帝,靈堂內(nèi)的尸體忽然破棺而出努潘,到底是詐尸還是另有隱情,我是刑警寧澤坤学,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布疯坤,位于F島的核電站,受9級(jí)特大地震影響深浮,放射性物質(zhì)發(fā)生泄漏压怠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一飞苇、第九天 我趴在偏房一處隱蔽的房頂上張望菌瘫。 院中可真熱鬧,春花似錦布卡、人聲如沸雨让。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栖忠。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庵寞,已是汗流浹背虚汛。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留皇帮,地道東北人卷哩。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像属拾,于是被迫代替她去往敵國(guó)和親将谊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容