在本課中幅疼,你將為FoodTracker應(yīng)用實(shí)現(xiàn)一個(gè)評(píng)分控件。當(dāng)你完成時(shí)昼接,應(yīng)用看上去是這樣的:
學(xué)習(xí)目標(biāo)
在結(jié)束本課時(shí),你將能夠:
- 創(chuàng)建自定義源碼文件悴晰,并將其與storyboard中的元素關(guān)聯(lián)
- 定義一個(gè)自定義類(lèi)
- 實(shí)現(xiàn)一個(gè)自定義類(lèi)的初始化器(initializer)
- 把UIStackView作為容器使用
- 理解如何創(chuàng)建可編程的視圖
- 為自定義控件添加輔助功能信息
- 使用 @IBInspectable 和 @IBDesignable在Interface Builder中顯示和控制自定義視圖
創(chuàng)建自定義視圖
為了能給菜品評(píng)分慢睡,用戶需要一個(gè)控件,這個(gè)控件可以讓他們選擇給菜品分配的星星數(shù)量铡溪。有很多方法可以實(shí)現(xiàn)這個(gè)要求漂辐,但是在本課中,關(guān)注一個(gè)簡(jiǎn)單的方法棕硫,通過(guò)已存在的視圖和控件來(lái)構(gòu)建一個(gè)自定義控件髓涯。你將創(chuàng)建一個(gè)棧視圖子類(lèi)來(lái)管理一排相當(dāng)于星的按鈕。你將在代碼中完全定義你的自定義控件哈扮,然后把它添加到storyboard中纬纪。
評(píng)分控件顯示為一排星星。
用戶能夠?yàn)椴似愤x擇一個(gè)評(píng)分滑肉。當(dāng)用戶點(diǎn)擊一顆星星時(shí)包各,這顆星和它之前的星都會(huì)被填充。如果用戶點(diǎn)擊最右邊被填充的星星(與當(dāng)前評(píng)分相關(guān)的星星)靶庙,那么評(píng)分將會(huì)被清除问畅,所有的星星都顯示為空。
開(kāi)始設(shè)計(jì)UI、交互护姆、控件行為之前矾端,需要?jiǎng)?chuàng)建一個(gè)自定義的UIStackView子視圖,
創(chuàng)建UIStackView的子類(lèi)
- 選擇File > New > File(或者按下Command-N)卵皂。
- 在彈出的對(duì)話框顯示時(shí)秩铆,選擇iOS。
- 選擇Cocoa Touch Class渐裂,點(diǎn)解Next豺旬。
- 在Class字段,輸入RatingControl柒凉。
- 在Subclass of字段族阅,選擇UIStackView。
-
確保Language選型為Swift膝捞。
- 點(diǎn)擊Next坦刀。
默認(rèn)保存位置是你的項(xiàng)目目錄。
Group選項(xiàng)默認(rèn)是你的應(yīng)用名蔬咬,F(xiàn)oodTracker鲤遥。
再Targets部分,選中你的應(yīng)用林艘,而應(yīng)用的測(cè)試不要選中盖奈。 - 對(duì)話框的其余內(nèi)容不變,點(diǎn)擊Create狐援。
Xcode創(chuàng)建一個(gè)定義RatingControl類(lèi)的文件:RatingControl.swift钢坦。RatingControl是UIView的自定義子類(lèi)。 -
如有必要啥酱,在Project navigation中爹凹,拖拽RatingControl.swift文件到其他Swift文件的下方。
- 在RatingControl.swift中镶殷,刪除所有模版自帶的注釋?zhuān)@樣你就可以使用一個(gè)空的區(qū)域開(kāi)始工作了『探矗現(xiàn)在這個(gè)實(shí)現(xiàn)文件看上去是這樣的。
import UIKit
class RatingControl: UIStackView {
}
通常創(chuàng)建一個(gè)視圖有兩種方式:通過(guò)編程方式初始化視圖绘趋,或者通過(guò)storyboard加載視圖颤陶。每一種方法都有一個(gè)相應(yīng)的初始化器:inti(farme:)用于編程初始化視圖的,init:(coder:)用于從storyboard加載的視圖埋心≈赣簦回想一下,初始化器是一個(gè)方法拷呆,這個(gè)方法為類(lèi)實(shí)例能夠使用做準(zhǔn)備闲坎。它涉及到為每個(gè)類(lèi)的實(shí)例變量設(shè)置值疫粥,以及需要的任何其他設(shè)置。
這兩種方法你都要在自定義控件中實(shí)現(xiàn)腰懂。在設(shè)計(jì)應(yīng)用時(shí)梗逮,當(dāng)你將視圖添加到畫(huà)布中時(shí)邀层,Interface Builder會(huì)實(shí)例化它量蕊。在運(yùn)行時(shí)逾冬,應(yīng)用會(huì)從storyboard中加在視圖不同。
重寫(xiě)初始化器(initializer)
- 在RatingControl.swift,在class行的下面业岁,添加一個(gè)注釋宪哩。
//MARK: Initialization
-
在注釋后面隧枫,開(kāi)始鍵入init锚沸。
代碼補(bǔ)全會(huì)顯示出來(lái)跋选。
- 從可選列表中選擇 init(frame: CGRect) ,然后按下回車(chē)鍵哗蜈。
init(frame: CGRect) {
}
-
如果有錯(cuò)誤前标,代碼旁邊會(huì)出現(xiàn)錯(cuò)誤和警告的圖標(biāo),黃色三角圖標(biāo)是警告距潘,紅色圓圈是錯(cuò)誤×读校現(xiàn)在,init(frame:)方法有一個(gè)錯(cuò)誤音比。點(diǎn)擊錯(cuò)誤圖標(biāo)俭尖,會(huì)顯示出這個(gè)錯(cuò)誤更多的信息。
- 雙擊Fix-it來(lái)插入override關(guān)鍵字
override init(frame: CGRect) {
}
Swift編譯器知道init(frame:)方法時(shí)必須被標(biāo)記的洞翩,所以提供了一個(gè)fix-it(修復(fù)選項(xiàng))目溉,以便在代碼中進(jìn)行修復(fù)。Fix-its是編譯器提供的菱农,作為代碼中錯(cuò)誤的潛在解決方案。
- 添加下面的方法來(lái)調(diào)用超類(lèi)的初始化器:
super.init(frame: frame)
- 在init(frame:)方法下面柿估,再次鍵入init循未,這次在代碼補(bǔ)全選項(xiàng)中選擇init(coder: NSCoder)。按下回車(chē)鍵秫舌。Xcode插入初始化器框架的妖。
init(coder: NSCoder) {
}
- 使用Fix-it插入required關(guān)鍵字。
注意
Swift處理初始化器不同于其他方法足陨。如果你沒(méi)有提供任何初始化器嫂粟,Swift類(lèi)自動(dòng)繼承所有超類(lèi)定義的初始化器。如果你實(shí)現(xiàn)任何初始化器墨缘,你就不能再繼承超類(lèi)的初始化器了星虹;但是零抬,超類(lèi)能標(biāo)記一個(gè)或多個(gè)它的初始化器為required。子類(lèi)必須實(shí)現(xiàn)(或自動(dòng)繼承)所有這些必須的初始化器宽涌。此外平夜,子類(lèi)必須標(biāo)記這些的初始化器為requrd,表明它們的子類(lèi)也必須實(shí)現(xiàn)這些初始化器卸亮。
- 添加下面這行來(lái)調(diào)用超類(lèi)的初始化器忽妒。
super.init(coder: coder)
初始化器看上去是這樣的:
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
super.init(coder: coder)
}
現(xiàn)在,你的初始化器僅僅是占位符兼贸,只是簡(jiǎn)單調(diào)用了超類(lèi)的實(shí)現(xiàn)而已段直。你將在本課接下來(lái)的步驟中添加額外的設(shè)置。
顯示自定義視圖
為了顯示自定義控件溶诞,你需要在storyboard中添加一個(gè)stack view鸯檬,在stack view 和你寫(xiě)的代碼之間建立連接。
顯示視圖
- 打開(kāi)storyboard很澄。
-
在storyboard中京闰,使用Object library找到Horizontal Stack View(水平棧視圖)對(duì)象,并且把它拖拽到storyboard場(chǎng)景中甩苛,讓它在image view下方蹂楣。
-
在horizontal stack view選中狀態(tài)下,打開(kāi)Identity inspector讯蒲。
回想一下痊土,Identify inspector讓你可以在storyboard中編輯一個(gè)對(duì)象的身份屬性,例如它屬于哪個(gè)類(lèi)墨林。
-
在Identity inspector中赁酝,找到Class字段并設(shè)置為RatingControl。
在視圖中添加按鈕
到這兒旭等,你已經(jīng)有了一個(gè)名為RatingControl的自定UIStackView子類(lèi)酌呆。接下來(lái),你需要添加一些按鈕到視圖中搔耕,以便讓用戶可以選擇評(píng)分隙袁。從簡(jiǎn)單開(kāi)始,在視圖中顯示一個(gè)紅色按鈕弃榨。
在視圖中創(chuàng)建按鈕
無(wú)論使用哪個(gè)初始化方法菩收,都要確保按鈕被添加。為此鲸睛,添加一個(gè)私有方法娜饵,setupButtons(),并且讓兩個(gè)初始化器都調(diào)用它官辈。
- 在RatingControl.swift中箱舞,在初始化器方法下面遍坟,添加注釋
//MARK: Private Methods
在注釋隔一點(diǎn)空間創(chuàng)建私有方法——在func前加上private來(lái)創(chuàng)建這個(gè)方法。私有方法只能在聲明它的類(lèi)中被調(diào)用褐缠。這可以讓你封裝和保護(hù)方法政鼠,確保它們不會(huì)被外部文件意外的調(diào)用。
- 在注釋下面队魏,添加下面的方法:
private func setupButtons() {
}
- 在setupButtons()方法中公般,添加下面幾行代碼來(lái)創(chuàng)建紅色的按鈕。
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
這里胡桨,你使用了UIButton類(lèi)的方便初始化器官帘。這個(gè)初始化器調(diào)用init(frame:)并傳遞給長(zhǎng)方形的尺寸為0。從0長(zhǎng)度開(kāi)始是沒(méi)有問(wèn)題的昧谊,因?yàn)槟闶褂肁uto Layout(自動(dòng)布局)刽虹。這個(gè)stack view 將自動(dòng)定義按鈕的位置,并且你將通過(guò)約束來(lái)定義按鈕的尺寸呢诬。
你使用紅色以便可以看到它的位置涌哲。如果你喜歡,可以使用其他UIColor預(yù)定義的顏色尚镰,比如blue或green阀圾。
- 在最后一行下面,添加按鈕的約束狗唉。
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
第一行代碼禁用按鈕的自動(dòng)生成的約束初烘。當(dāng)你代碼實(shí)例化視圖,它的translatesAutoresizingMaskIntoConstraints屬性默認(rèn)為true分俯。這告訴布局引擎基于視圖的frame和autoresizingmask屬性來(lái)創(chuàng)建它的約束肾筐。然而,當(dāng)你使用Auto Layout時(shí)缸剪,你是要將這些自動(dòng)生成的約束替換掉吗铐。所以,把translatesAutoresizingMaskIntoConstraints屬性值設(shè)置為false可以移除這些自動(dòng)生成的約束杏节。
注意
這一行不是絕對(duì)的抓歼。當(dāng)你添加視圖到stack view,這個(gè)stack view會(huì)自動(dòng)設(shè)置視圖的translatesAutoresizingMaskIntoConstraints屬性為false拢锹。但是,當(dāng)使用Auto Layout時(shí)萄喳,無(wú)論何時(shí)都禁用自動(dòng)生成約束是一個(gè)好習(xí)慣卒稳。這樣就不會(huì)在真的需要的時(shí)候忘了寫(xiě)了。
button.heightAnchor 和button.widthAnchor開(kāi)始的行創(chuàng)建了定義按鈕高和寬的約束他巨。每行都執(zhí)行下面的步驟:
- 按鈕的heightAnchor 和 widthAnchor屬性給訪問(wèn)布局錨(layout anchor)提供了入口充坑。你使用布局錨來(lái)創(chuàng)建約束——在本例中减江,約束分別定義視圖高和寬。
- constraint(equalToConstant:)方法返回一個(gè)約束捻爷,它為視圖的高或?qū)挾x了一個(gè)常量辈灼。
- 約束的isActive屬性是用來(lái)激活和禁用這個(gè)約束的。當(dāng)你設(shè)置這個(gè)屬性為true時(shí)也榄,系統(tǒng)添加這個(gè)約束到當(dāng)前視圖巡莹,并激活它。
這兩行代碼加在一起把按鈕定義為一個(gè)固定大小的對(duì)象(44點(diǎn)*44點(diǎn))甜紫。 - 最后降宅,添加這個(gè)按鈕到棧中:
// Add the button to the stack
addArrangedSubview(button)
這個(gè)addArrangedSubview()方法添加你創(chuàng)建的按鈕到RatingControl棧視圖管理的視圖列表中。這個(gè)操作把視圖添加為RatingControl的子視圖囚霸,并讓RatingControl創(chuàng)建必要的約束來(lái)管理按鈕在控件中的位置腰根。
你的setupButtons() 方法看上去應(yīng)該是這樣:
private func setupButtons() {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Add the button to the stack
addArrangedSubview(button)
}
現(xiàn)在在初始化器方法中調(diào)用這個(gè)方法,像下面這樣:
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupButtons()
}
檢查點(diǎn):運(yùn)行應(yīng)用拓型。你應(yīng)該能看到一個(gè)小的紅色正方形視圖额嘿。這個(gè)紅色的正方形就是你在初始化器里添加的按鈕。
你需要為這個(gè)按鈕添加動(dòng)作(稍后你還要為其他按鈕添加)劣挫。最后册养,即娘使用這個(gè)按鈕來(lái)改變菜品的評(píng)分;但是揣云,現(xiàn)在你需要檢查這個(gè)操作是否正常捕儒。
給按鈕添加一個(gè)動(dòng)作
- 在RatingControl.swift,在//MARK Initialization部分后 main邓夕,添加注釋?zhuān)?/li>
//MARK: Button Action
- 在注釋下面刘莹,添加代碼:
func ratingButtonTapped(button: UIButton) {
print("Button pressed ??")
}
使用print()函數(shù)來(lái)檢查ratingButtonTapped(_:)動(dòng)作是否按預(yù)期連接到了按鈕。這個(gè)函數(shù)會(huì)在Xcode調(diào)試控制臺(tái)打印標(biāo)準(zhǔn)輸出焚刚〉阃洌控制臺(tái)一個(gè)有用的調(diào)試機(jī)制,它出現(xiàn)在編輯器的底部矿咕。
稍后抢肛,你將把這個(gè)調(diào)試實(shí)現(xiàn)替換為更有用的實(shí)現(xiàn)。
- 找到setupButtons()方法:
private func setupButtons() {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Add the button to the stack
addArrangedSubview(button)
}
- 緊挨著 // Add the button to the stack注釋上面碳柱,添加下面這段代碼:
// Setup the button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
在之前的課程中捡絮,你使用target-action模式來(lái)連接storyboard中的元素到代碼中的action方法。這個(gè)addTarget(, action:, for:)方法在代碼中起到相同的作用莲镣。你把 ratingButtonTapped(:)action方法添加到了button對(duì)象上福稳,它將在.TouchDown事件發(fā)生的時(shí)候被觸發(fā)。
這段代碼做了很多事瑞侮。這是分析:
- 目標(biāo)是self的圆,它指向當(dāng)前的類(lèi)的實(shí)例鼓拧。在本例中,它指向的是設(shè)置這些按鈕的RatingControl對(duì)象越妈。
- 這個(gè)#selector表達(dá)式返回的是提供方法的Selector(選擇器)的值季俩。一個(gè)selector是一個(gè)識(shí)別這個(gè)方法的不透明的值。雖然很多新的API用block代替了selector梅掠,但仍然有很多方法酌住,比如performSelector(:) 和 addTarget(:action:forControlEvents:),在使用selector作為參數(shù)瓤檐。它系統(tǒng)在按鈕被點(diǎn)擊的時(shí)候調(diào)用動(dòng)作方法赂韵。
- UIControlEvents選項(xiàng)定義了一些控件能夠響應(yīng)的事件。通常按鈕響應(yīng)的是.touchUpInside事件挠蛉。當(dāng)用戶觸摸按鈕然后在按鈕的范圍內(nèi)抬起手指的時(shí)候發(fā)生祭示。這個(gè)事件比 .touchDown更好,因?yàn)橛脩裟軌虬咽种敢苿?dòng)到按鈕范圍之外再抬起谴古,這樣就可以取消事件了质涛。
- 注意因?yàn)槟銢](méi)有使用Interface Builder,所以你不需要使用IBAction屬性定義action方法掰担;你只需要像定義其他方法一樣定義動(dòng)作方法就好了汇陆。你能使用的方法可以是不帶參數(shù)的、只帶sender參數(shù)的带饱,或者帶sender和event兩個(gè)參數(shù)毡代。
func doSomething()
func doSomething(sender: UIButton)
func doSomething(sender: UIButton, forEvent event: UIEvent)
現(xiàn)在你的setupButtons()方法看上去是這樣的:
private func setupButtons() {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
}
檢查點(diǎn):運(yùn)行應(yīng)用。當(dāng)你點(diǎn)擊紅色正方形時(shí)勺疼,你能夠在控制臺(tái)看到“Button pressed”消息教寂。
現(xiàn)在是時(shí)候想一想關(guān)于RatingControl類(lèi)要用什么樣的信息來(lái)表示評(píng)分。你需要跟蹤評(píng)分值执庐,以及用戶用來(lái)點(diǎn)擊設(shè)置評(píng)分的按鈕酪耕。你可以使用Int類(lèi)型來(lái)表示評(píng)分值,按鈕可以存放在一個(gè)數(shù)組中轨淌。
添加評(píng)分屬性
- 在RattingControl.swift中迂烁,找到類(lèi)聲明行:
class RatingControl: UIView {
- 在這行下面,添加下面的代碼:
//MARK: Properties
private var ratingButtons = [UIButton]()
var rating = 0
這創(chuàng)建了兩個(gè)屬性递鹉。第一個(gè)屬性包含按鈕列表盟步。你不想讓RattingControl類(lèi)以外的的類(lèi)訪問(wèn)這些按鈕;所以躏结,你把它聲明為私有址芯。第二個(gè)屬性包含了控件的評(píng)分。你需要?jiǎng)e的類(lèi)能夠讀寫(xiě)這個(gè)值。默認(rèn)情況下時(shí)內(nèi)部訪問(wèn)谷炸,保持不變。這樣你就能從應(yīng)用內(nèi)的其他類(lèi)來(lái)訪問(wèn)這個(gè)值禀挫。
現(xiàn)在旬陡,在視圖中你有一個(gè)按鈕,但你一共需要5個(gè)按鈕语婴。使用for-in循環(huán)來(lái)創(chuàng)建5個(gè)按鈕描孟。for-in循環(huán)遍歷一個(gè)序列,例如數(shù)字范圍砰左,多次執(zhí)行一組代碼匿醒。
創(chuàng)建五個(gè)按鈕
- 在RatingControl.swift中,找到setupButtons()方法缠导,并在方法的內(nèi)容外層添加一個(gè)for-in循環(huán)廉羔,就像這樣:
for _ in 0..<5 {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
}
通過(guò)選中for-in中的所有行并按下Control-I,確保這些行都縮進(jìn)僻造。Make sure the lines in the for-in loop are indented properly by selecting all of them and pressing Control-I. 在半開(kāi)放范圍運(yùn)算符(half-open range operator憋他, ..< )不包含上限數(shù)字,所以這個(gè)范圍是0-4髓削,總共循環(huán)五次竹挡,繪制5個(gè)按鈕而不是僅僅一個(gè)。下劃線(_)表示一個(gè)通配符立膛,當(dāng)你不需要知道當(dāng)前正在執(zhí)行的迭代的次數(shù)時(shí)揪罕,可以使用它。
- 在循環(huán)的結(jié)束花括號(hào)({)上面宝泵,添加這個(gè)代碼:
// Add the new button to the rating button array
ratingButtons.append(button)
當(dāng)你創(chuàng)建一個(gè)按鈕好啰,你就把它添加到ratingButtons數(shù)組中,用來(lái)跟蹤它鲁猩。
現(xiàn)在你的setupButtons()方法看上去是這樣的:
private func setupButtons() {
for _ in 0..<5 {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
// Add the new button to the rating button array
ratingButtons.append(button)
}
}
檢查點(diǎn):運(yùn)行應(yīng)用坎怪。注意stack view是如何布局這些按鈕的。它們被水平排列的廓握,但是它們之間沒(méi)有間隔——這使它們就像一個(gè)紅色塊搅窿。
為了修復(fù)這種情況,打開(kāi)Main.storyboard并選擇RatingControl棧視圖隙券,代開(kāi)Attributes inspector男应,設(shè)置Spacing屬性為8。
檢查點(diǎn):再次運(yùn)行應(yīng)用∮樽校現(xiàn)在按鈕的布局符合期望了沐飘。注意,現(xiàn)在點(diǎn)擊任何一個(gè)按鈕仍然調(diào)用ratingButtonTapped(button:),并在控制臺(tái)上打印一條消息耐朴。
使用Debug區(qū)域開(kāi)關(guān)折疊控制臺(tái)借卧。
(未完待續(xù)......)