馬上著手開發(fā) iOS 應(yīng)用程序 (六) - 實(shí)現(xiàn)自定義控件

重要:這是針對(duì)于正在開發(fā)中的API或技術(shù)的預(yù)備文檔(預(yù)發(fā)布版本)。蘋果提供這份文檔的目的是幫助你按照文中描述的方式對(duì)技術(shù)的選擇及界面的設(shè)計(jì)開發(fā)進(jìn)行規(guī)劃。這些信息有可能發(fā)生變化蔑匣,因此根據(jù)本文檔的軟件開發(fā)應(yīng)當(dāng)基于最終版本的操作系統(tǒng)和文檔進(jìn)行測(cè)試。該文檔的新版本或許會(huì)隨著API或相關(guān)技術(shù)未來(lái)的發(fā)展而進(jìn)行更新。

翻譯自蘋果官網(wǎng):

https://developer.apple.com/library/prerelease/ios/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson5.html#//apple_ref/doc/uid/TP40015214-CH19-SW1

在本課中潮剪,為 app 實(shí)現(xiàn)評(píng)分功能。完成的樣子如下:

[圖片上傳失敗...(image-ed9a4d-1608214851935)]

學(xué)習(xí)目標(biāo):

課程的最后分唾,你講能夠:

  • 創(chuàng)建自定義源代碼文件并與 storyboard 中控件關(guān)聯(lián)
  • 定義自定義類
  • 實(shí)現(xiàn)自定義類的構(gòu)造器
  • 使用 UIView 作為容器
  • 理解如何在程序中顯示視圖

創(chuàng)建自定義類

我們需要一個(gè)控件來(lái)讓用戶為美食評(píng)分抗碰。雖然有多種實(shí)現(xiàn)方式,但是本節(jié)重點(diǎn)是使用自定義類的方式绽乔。

這是你即將要實(shí)現(xiàn)的評(píng)分控件:

[圖片上傳失敗...(image-5a7bae-1608214851936)]

評(píng)分控件讓用戶為美食選擇 0弧蝇、1、2、3看疗、4或5分沙峻。當(dāng)用戶點(diǎn)擊星星,在所點(diǎn)星星左邊的所有星星都將被填充顏色两芳。計(jì)算填充的星星數(shù)目作為評(píng)分摔寨,而空的星星不算。

通過(guò)創(chuàng)建一個(gè) UIView 的子類來(lái)構(gòu)建控件的界面怖辆、交互和行為是复。

創(chuàng)建 UIView 的子類
  1. 選擇 File > New > File(或按 Command-N)。

  2. 選擇 Cocoa Touch Class 然后點(diǎn)擊 Next竖螃。

  3. 在 Class 區(qū)域淑廊,輸入 RatingControl。

  4. SubClass of 區(qū)域特咆,選擇 UIView季惩。

  5. 確保語(yǔ)言設(shè)置成了 Swift。

    [圖片上傳失敗...(image-781de4-1608214851936)]

  6. 點(diǎn)擊 Next腻格。
    默認(rèn)會(huì)存到你的項(xiàng)目目錄中画拾。
    Group 選項(xiàng)默認(rèn)是你的 app 名字,F(xiàn)oodTracker荒叶。
    在 Targets 區(qū)域碾阁,app 選項(xiàng)是選中的而 tests 未選中。

  7. 其他都按照默認(rèn)的來(lái)些楣,點(diǎn)擊 Create脂凶。
    Xcode 創(chuàng)建了 RatingControl.swift 文件。RatingControl 是 UIView 的子類愁茁。

  8. 刪除文件中模板自動(dòng)添加的注釋以便輕裝上陣蚕钦。

     import UIKit
    
     class RatingControl: UIView {
         
     }
    

有兩種典型的方式創(chuàng)建視圖:第一種通過(guò) frame 初始化視圖這樣可以手動(dòng)添加視圖到你的界面中,或者通過(guò) storyboard 加載視圖鹅很。它們都有對(duì)應(yīng)的構(gòu)造方法:frame 的是 init(frame:) 而 storyboard 對(duì)應(yīng) int?(coder:)方法嘶居。

由于使用 storyboard 來(lái)加載 view,所以一開始請(qǐng)覆寫父類的 init?(coder:) 構(gòu)造方法促煮。

覆寫構(gòu)造方法

  1. 在 RatingControl.swift 的 class 行下面邮屁,添加如下注釋。

     // MARK: Initialization
    
  2. 在注釋下面菠齿,輸入 init佑吝。
    彈出代碼提示框。
    [圖片上傳失敗...(image-d1c2fd-1608214851936)]

  3. 選擇列表的第三個(gè) init?(coder:) 構(gòu)造方法绳匀,回車確認(rèn)芋忿。

     init?(coder aDecoder: NSCoder) {
     }
    

    Xcode 為你生成基本的構(gòu)造方法炸客。

  4. 點(diǎn)擊紅色圓圈添加 required 關(guān)鍵字來(lái)修復(fù)錯(cuò)誤。

     required init?(coder aDecoder: NSCoder) {
     }
    

    每個(gè) UIView 子類實(shí)現(xiàn)自定義構(gòu)造方法同時(shí)必須實(shí)現(xiàn) init?(coder:)戈钢。
    Swift 編譯器知道這個(gè)規(guī)則痹仙,所以提供了 fix-it 功能為錯(cuò)誤提供潛在的解決方案。

  5. 在子類的構(gòu)造器中添加如下行殉了。

     super.init(coder: aDecoder)
    

init?(coder:) 構(gòu)造方法應(yīng)該像這樣:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

顯示自定義視圖

為了顯示自定義視圖开仰,需要向界面添加視圖并確保視圖關(guān)聯(lián)了 RatingControl 類。

顯示視圖
  1. 打開 storyboard宣渗。

  2. 在 storyboard 中抖所,使用對(duì)象庫(kù)找到 View 對(duì)象并拖到堆棧視圖中 image view 的下面。

  3. 選中視圖痕囱,在實(shí)用工具區(qū)打開尺寸檢查器。

    [圖片上傳失敗...(image-71bf43-1608214851936)]

  4. 在 Intrinsic Size 區(qū)域的彈出菜單中暴匠,選擇 Placeholder鞍恢。

  5. 在 Instrinsic Size 下面的 Height 區(qū)域輸入 44 ,Width 區(qū)域輸入 240 每窖“锏簦回車確認(rèn),界面最后應(yīng)該像這樣:

    [圖片上傳失敗...(image-c76766-1608214851936)]

  6. 選中視圖窒典,打開識(shí)別檢查器(Identity inspector)蟆炊。

    [圖片上傳失敗...(image-d3e5ed-1608214851936)]

  7. 在識(shí)別檢查器中,找到名為 Class 的區(qū)域選擇 RatingControl瀑志。

    [圖片上傳失敗...(image-50b30f-1608214851936)]

添加按鈕到視圖

下一步是添加按鈕到視圖來(lái)允許用戶選擇評(píng)分涩搓。

步驟:
  1. 在 init?(coder:) 構(gòu)造方法中,添加如下代碼行來(lái)創(chuàng)建紅色按鈕:

     let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
     button.backgroundColor = UIColor.redColor()
    

    使用 redColor() 更容易讓你看到視圖的樣子劈猪。如果你喜歡可以換成其他 UIColor 的值昧甘,如 blueColor() 和 greenColor()。

  2. 在方法最后添加如下代碼:

     addSubview(button)
    

    addSubView() 方法向 RatingControl 添加剛才創(chuàng)建的按鈕战得。

init?(coder:) 構(gòu)造方法應(yīng)該像這樣:

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
        button.backgroundColor = UIColor.redColor()
        addSubview(button)
    }

為了告訴堆棧視圖如何布局控件充边,需要提供固有內(nèi)容尺寸。如下面這樣覆寫 intrinsicContentSize 方法常侦。

override func intrinsicContentSize() -> CGSize {
    return CGSize(width: 240, height: 44)
}

檢驗(yàn):運(yùn)行 app浇冰。應(yīng)該能夠看到視圖中有個(gè)小紅色正方形。它是剛才在構(gòu)造方法中添加的按鈕聋亡。

[圖片上傳失敗...(image-4360ea-1608214851936)]

給按鈕添加動(dòng)作
  1. 在 RatingController.swift 最后 } 的前面添加這行注釋:

     // MARK: Button Action
    
  2. 在注釋下面添加如下代碼:

     func ratingButtonTapped(button: UIButton) {
         print("Button pressed ??")
     }
    

    使用 print() 方法檢查 ratingButtonTapped(_:) 動(dòng)作連接了預(yù)期的按鈕肘习。這個(gè)函數(shù)打印消息到 Xcode 調(diào)試控制臺(tái)中,而控制臺(tái)是在編輯區(qū)底部非常有用的調(diào)試工具杀捻。
    稍后會(huì)用真實(shí)實(shí)現(xiàn)來(lái)替代這行調(diào)試代碼井厌。

  3. 找到 init?(coder:) 構(gòu)造方法:

     required init?(coder aDecoder: NSCoder) {
         super.init(coder: aDecoder)
         
         let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
         button.backgroundColor = UIColor.redColor()
         addSubview(button)
     }   
    
  4. 在 addSubView(button) 行下面蚓庭,添加如下代碼:

     button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    

    你應(yīng)該很熟悉 target-action 設(shè)計(jì)模式因?yàn)橹耙呀?jīng)使用它連接過(guò)控件和方法。現(xiàn)在讓我們?cè)俅蝿?chuàng)建連接仅仆。連接 ratingButtonTapped: 動(dòng)作和按鈕對(duì)象器赞,當(dāng)用戶按下了按鈕,會(huì)觸發(fā)這個(gè)方法墓拜。
    注意因?yàn)槭褂昧私缑鏄?gòu)造器(Interface Builder)港柜,就可以像普通方法一樣定義,而不需要使用 IBAction 屬性定義方法咳榜。

最后的 init?(coder:) 構(gòu)造方法應(yīng)該像這樣:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    button.backgroundColor = UIColor.redColor()
    button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    addSubview(button)
}

檢驗(yàn):運(yùn)行 app夏醉。當(dāng)點(diǎn)擊了紅色正方形,應(yīng)該看到控制臺(tái)打印了 Button Pressed涌韩。

[圖片上傳失敗...(image-cb94a3-1608214851936)]

使用整形變量來(lái)表示評(píng)分值畔柔,它的范圍是0到5,使用數(shù)組來(lái)存放所有的按鈕臣樱。

添加評(píng)分屬性
  1. 在 RatingControl.swift 中靶擦,找到類定義行:

    class RatingControl: UIView {

  2. 在行的下面,添加如下代碼:

     // MARK: Properties
    
     var rating = 0
     var ratingButtons = [UIButton]()
    

現(xiàn)在只有一個(gè)按鈕雇毫,但我們總共需要5個(gè)玄捕。使用 for-in 循環(huán)遍歷 sequence 來(lái)創(chuàng)建五個(gè)按鈕。

創(chuàng)建五個(gè)按鈕
  1. 在 RatingControl.swift 中找到 init?(coder:) 構(gòu)造方法:

     required init?(coder aDecoder: NSCoder) {
         super.init(coder: aDecoder)
         
         let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
         button.backgroundColor = UIColor.redColor()
         button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
         addSubview(button)
     }
    
  2. 在最后四行添加 for-in 循環(huán)棚放,像這樣:

     for _ in 0..<5 {
         let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
         button.backgroundColor = UIColor.redColor()
         button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
         addSubview(button)
     }
    

    選擇所有代碼按 Control-I 確保 for-in 中的行縮進(jìn)正確枚粘。

    半閉區(qū)間運(yùn)算符(..<) 不包括最大的數(shù),所以范圍是從 0 到 4 五次循環(huán)來(lái)添加五個(gè)按鈕飘蚯。當(dāng)不需要知道當(dāng)前循環(huán)變量使用通配符 _馍迄。

  3. 在 addSubView(button) 行上面,添加如下代碼:

     ratingButtons += [button]
    

    添加每個(gè)按鈕到 ratingButtons 數(shù)組孝冒。

你的 init?(coder:) 構(gòu)造方法最后應(yīng)該像這樣:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    
    for _ in 0..<5 {
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
        button.backgroundColor = UIColor.redColor()
        button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
        ratingButtons += [button]
        addSubview(button)
    }
}

檢驗(yàn):運(yùn)行 app柬姚。還是只看到了一個(gè)按鈕。因?yàn)?for-in 循環(huán)僅僅把各個(gè)按鈕堆疊到一起庄涡。需要調(diào)整這些按鈕的布局量承。

[圖片上傳失敗...(image-7a228e-1608214851936)]

調(diào)整按鈕的布局
  1. 在 RatingControl.swift 的 // MARK: Initialization 區(qū)域添加如下方法:

     override func layoutSubviews() {
     }
    

    記得使用代碼提示快速完成方法的框架。

  2. 在方法中穴店,添加如下代碼:

     var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44)
      
     // Offset each button's origin by the length of the button plus spacing.
     for (index, button) in ratingButtons.enumerate() {
         buttonFrame.origin.x = CGFloat(index * (44 + 5))
         button.frame = buttonFrame
     }
    

    使用 for-in 循環(huán)遍歷按鈕來(lái)設(shè)置它們的 frames撕捍。

    enumerate() 方法返回 ratingButtons 中所有控件的集合。集合中每個(gè)元組包含一個(gè) index 和 button泣洞,分別代表遍歷的下標(biāo)和按鈕忧风。使用 index 計(jì)算新的 frame 并設(shè)置給對(duì)應(yīng)的按鈕。frame 的 x 值等于 44 點(diǎn)的標(biāo)準(zhǔn)按鈕大小加上 5 點(diǎn)的空隙然后乘以 index 球凰。

    最后的 layoutSubviews() 方法應(yīng)該像這樣:

     override func layoutSubviews() {
         var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44)
         
         // Offset each button's origin by the length of the button plus spacing.
         for (index, button) in ratingButtons.enumerate() {
             buttonFrame.origin.x = CGFloat(index * (44 + 5))
             button.frame = buttonFrame
         }
     }
    

檢驗(yàn):運(yùn)行 app狮腿。此刻按鈕應(yīng)該并排在一起了腿宰。注意,此時(shí)點(diǎn)擊任一按鈕應(yīng)該繼續(xù)調(diào)用 ratingButtonTapped(_:) 并打印消息到控制臺(tái)缘厢。

[圖片上傳失敗...(image-382550-1608214851936)]

使用 Debug toggle 收縮控制臺(tái)吃度。

[圖片上傳失敗...(image-e34a5f-1608214851936)]

為按鈕大小添加常量

注意在代碼中使用了 44 的值。而在代碼的各處使用硬編碼值是很不好的做法贴硫。如果需要一個(gè)大一點(diǎn)的按鈕椿每,就得在各處修改這個(gè) 44 的值。相反英遭,定義常量來(lái)表示按鈕的大小间护,這樣更方便修改因?yàn)橹抵恍栊薷囊惶帯?/p>

現(xiàn)在,根據(jù)容器視圖的高度調(diào)整按鈕來(lái)適配不同尺寸的容器視圖.

定義一個(gè)常量作為按鈕的大小
  1. 在 layoutSubviews() 方法第一行前面添加如下代碼:

     // Set the button's width and height to a square the size of the frame's height.
     let buttonSize = Int(frame.size.height)
    

    這讓布局更加靈活挖诸。

  2. 修改方法剩下部分使用 buttonSize 常量取代 44:

     var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
      
     // Offset each button's origin by the length of the button plus spacing.
     for (index, button) in ratingButtons.enumerate() {
         buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
         button.frame = buttonFrame
     }   
    
  3. 和第一次添加按鈕一樣汁尺,你需要更新控件的固有內(nèi)容尺寸來(lái)讓堆棧視圖正確的布局。使用 intrinsicContentSize 方法來(lái)計(jì)算控件的大小并返回税灌,代碼如下:

  4. 修改 init?(coder:) 方法中 for-in 循環(huán)的第一行為如下代碼 :

     let button = UIButton()
    

    因?yàn)槟阍?layoutSubviews() 中設(shè)置了按鈕的 frame均函,你不再需要在創(chuàng)建按鈕時(shí)候設(shè)置了。

    最后的 layoutSubviews() 方法應(yīng)該是這樣:

     override func layoutSubviews() {
         // Set the button's width and height to a square the size of the frame's height.
         let buttonSize = Int(frame.size.height)
         var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
         
         // Offset each button's origin by the length of the button plus some spacing.
         for (index, button) in ratingButtons.enumerate() {
             buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
             button.frame = buttonFrame
         }
     }
    

intrinsicContentSize 方法應(yīng)該像這樣:

override func intrinsicContentSize() -> CGSize {
    let buttonSize = Int(frame.size.height)
    let width = (buttonSize + spacing) * stars
    
    return CGSize(width: width, height: buttonSize)
}

init?(coder:) 構(gòu)造方法應(yīng)該像這樣:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    
    for _ in 0..<5 {
        let button = UIButton()
        button.backgroundColor = UIColor.redColor()
        button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
        ratingButtons += [button]
        addSubview(button)
    }
}

檢驗(yàn):運(yùn)行 app菱涤。一切和之前一樣。按鈕應(yīng)該并排顯示洛勉。此刻點(diǎn)擊任何一個(gè)按鈕都會(huì)調(diào)用 ratingButtonTapped(_:) 并打印信息到控制臺(tái)粘秆。

[圖片上傳失敗...(image-24503b-1608214851936)]

向按鈕中添加星星的圖片

下一步,添加空的和填充的星星圖片到按鈕中收毫。

[圖片上傳失敗...(image-27ef0d-1608214851936)]

可以在課程最終項(xiàng)目的 Images/ 文件夾中找到這兩張圖片攻走,或者用你自己的。

添加圖片到項(xiàng)目中
  1. 選擇項(xiàng)目導(dǎo)航的 Assets.xcassets 查看 asset catalog此再。
    回憶一下 asset catalog 是存放和管理 app 圖片資源的地方昔搂。

  2. 點(diǎn)擊左下角的 + 按鈕,從彈出的菜單中選擇 New Folder输拇。

    [圖片上傳失敗...(image-ca95d8-1608214851936)]

  3. 雙擊文件夾名字并重命名為 Rating Images摘符。

  4. 選中文件夾,點(diǎn)擊左下角的 + 按鈕策吠,在彈出菜單中選擇 New Image Set逛裤。
    一個(gè)圖片集合雖然只能代表單張圖片,但是包含在不同屏幕分辨率上顯示的不同版本圖片猴抹。

  5. 雙擊圖片集合的名字并重命名為 emptyStar带族。

  6. 選擇電腦中空的星星圖片。

  7. 拖動(dòng)圖片到圖片集合的 2x 槽中蟀给。

    [圖片上傳失敗...(image-54cea1-1608214851936)]

    2x 是 iPhone 6 模擬器顯示的分辨率蝙砌,圖片在這個(gè)分辨率顯示最好阳堕。

  8. 點(diǎn)擊左下角的 + 按鈕,在彈出的菜單中選擇 New Image Set择克。

  9. 雙擊圖片集合名字并重命名為 filledStar恬总。

  10. 在電腦上,選擇想要添加的填充狀態(tài)的星星圖片祠饺。

  11. 拖動(dòng)圖片到圖片集合的 2x 槽中越驻。

    [圖片上傳失敗...(image-8ab7f7-1608214851936)]

    asset catalog 應(yīng)該像這樣:

    [圖片上傳失敗...(image-3c4c98-1608214851936)]

    下一步,在適當(dāng)時(shí)候?qū)懘a為按鈕設(shè)置正確的圖片道偷。

為按鈕設(shè)置星星的圖片
  1. 打開 RatingControl.swift缀旁。

  2. 在 init?(coder:) 構(gòu)造方法的 for-in 循環(huán)前面添加這兩行:

     let filledStarImage = UIImage(named: "filledStar")
     let emptyStarImage = UIImage(named: "emptyStar")
    
  3. 在 for-in 循環(huán)按鈕初始化那一行的后面,添加如下代碼:

     button.setImage(emptyStarImage, forState: .Normal)
     button.setImage(filledStarImage, forState: .Selected)
     button.setImage(filledStarImage, forState: [.Highlighted, .Selected])
    

    為按鈕的不同狀態(tài)設(shè)置兩張不同的圖片這樣當(dāng)按鈕選中的時(shí)候就能看到狀態(tài)變化了勺鸦。按鈕未選中(.Normal 狀態(tài))顯示空的星星圖片并巍。按鈕被選中(.Selected 狀態(tài))顯示填充的星星圖片。在用戶點(diǎn)擊按鈕不松手時(shí)候按鈕同時(shí)處于選中和高亮狀態(tài)换途。

  4. 刪除設(shè)置背景顏色為紅色的那行代碼:

     button.backgroundColor = UIColor.redColor()
    

    因?yàn)榘粹o已經(jīng)有圖片懊渡,所以把背景顏色去掉。

  5. 添加如下代碼:

     button.adjustsImageWhenHighlighted = false
    

    確保圖片在狀態(tài)切換時(shí)不顯示額外的高亮狀態(tài)军拟。

init?(coder:) 構(gòu)造方法應(yīng)該像這樣:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    
    let emptyStarImage = UIImage(named: "emptyStar")
    let filledStarImage = UIImage(named: "filledStar")
    
    for _ in 0..<5 {
        let button = UIButton()
        
        button.setImage(emptyStarImage, forState: .Normal)
        button.setImage(filledStarImage, forState: .Selected)
        button.setImage(filledStarImage, forState: [.Highlighted, .Selected])
        
        button.adjustsImageWhenHighlighted = false
        
        button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
        ratingButtons += [button]
        addSubview(button)
    }
}

檢驗(yàn):運(yùn)行 app剃执。應(yīng)該看到星星已經(jīng)取代紅色按鈕了。此刻點(diǎn)擊任意一個(gè)按鈕仍然會(huì)調(diào)用 ratingButtonTapped(_:) 并打印信息到控制臺(tái)上懈息,但是按鈕圖片并沒(méi)有改變肾档。下一步會(huì)修復(fù)這個(gè)問(wèn)題的。

[圖片上傳失敗...(image-2e0a7f-1608214851936)]

實(shí)現(xiàn)按鈕動(dòng)作

用戶需要點(diǎn)擊星星的時(shí)候選擇評(píng)分辫继,所以使用真實(shí)的 ratingButtonTapped(_:) 方法替代調(diào)試實(shí)現(xiàn)怒见。

實(shí)現(xiàn)評(píng)分動(dòng)作
  1. 找到 RaingControl.swift 文件的 ratingButtonTapped(_:) 方法:

  2. 使用下面這行替代 print 語(yǔ)句:

     rating = ratingButtons.indexOf(button)! + 1
    

    indexOf(_:) 方法嘗試找出數(shù)組中選中的按鈕并返回它的下標(biāo)。
    方法返回可選的 Int 因?yàn)樗阉鞯膶?duì)象或許在集合中不存在姑宽。然而遣耍,因?yàn)橛|發(fā)動(dòng)作的按鈕是你自己添加和創(chuàng)建到數(shù)組中的,所以確定會(huì)返回正確的下標(biāo)炮车。這時(shí)舵变,使用 ! 來(lái)訪問(wèn)內(nèi)在的下標(biāo)值示血。因?yàn)閿?shù)組下標(biāo)是從0開始所以要加1才是合適的評(píng)分.

  3. 在 RatingControl.swift 的最后一個(gè) } 前面棋傍,添加如下代碼:

     func updateButtonSelectionStates() {
     }
    

    使用幫助方法來(lái)更新按鈕的選中狀態(tài)。

  4. 在 updateButtonSelectionStates() 方法中难审,添加 for-in 循環(huán):

     for (index, button) in ratingButtons.enumerate() {
         // If the index of a button is less than the rating, that button should be selected.
         button.selected = index < rating
     }
    

    代碼遍歷按鈕數(shù)組根據(jù)按鈕的下標(biāo)是否小于評(píng)分來(lái)設(shè)置每個(gè)按鈕的狀態(tài)瘫拣。如果 index < rating 設(shè)置按鈕的狀態(tài)為選中并讓它顯示填充的星星圖片。否則告喊,按鈕不選中顯示空的星星圖片麸拄。

  5. 在 ratingButtonTapped(_:) 方法中派昧,添加 updateButtonSelectionStates() 的調(diào)用作為最后一行的實(shí)現(xiàn):

     func ratingButtonTapped(button: UIButton) {
         rating = ratingButtons.indexOf(button)! + 1
         
         updateButtonSelectionStates()
     }
    
  6. 在 layoutSubviews() 方法中,添加 updateButtonSelectionStates() 的調(diào)用作為最后一行的實(shí)現(xiàn):

     override func layoutSubviews() {
         // Set the button's width and height to a square the size of the frame's height.
         let buttonSize = Int(frame.size.height)
         var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
         
         // Offset each button's origin by the length of the button plus some spacing.
         for (index, button) in ratingButtons.enumerate() {
             buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
             button.frame = buttonFrame
         }
         updateButtonSelectionStates()
     }
    

不僅僅當(dāng)評(píng)分改動(dòng)的時(shí)候拢切,視圖加載的時(shí)候也要更新按鈕的選中狀態(tài)蒂萎,這是非常重要的。

  1. // MARK: Properties 區(qū)域淮椰,找到 rating 屬性:

     var rating = 0
    
  2. 更新 rating 屬性讓它包含屬性觀察器:

     var rating = 0 {
     didSet {
         setNeedsLayout()
     }
     }
    

    屬性觀察器觀察和響應(yīng)屬性值的修改五慈。在設(shè)置屬性值的前后它會(huì)被立即調(diào)用。具體來(lái)講主穗,當(dāng)設(shè)完值后 didSet 屬性觀察器立刻執(zhí)行泻拦。在這里調(diào)用 setNeedsLayout() 讓每次評(píng)分修改后觸發(fā)布局更新。確保 UI 顯示正確的 rating 屬性值忽媒。

updateButtonSelectionStates() 方法應(yīng)該像這樣:

func updateButtonSelectionStates() {
    for (index, button) in ratingButtons.enumerate() {
        // If the index of a button is less than the rating, that button shouldn't be selected.
        button.selected = index < rating
    }
}

檢驗(yàn):運(yùn)行 app争拐。應(yīng)該能看到五顆星,請(qǐng)嘗試點(diǎn)擊一個(gè)來(lái)修改評(píng)分晦雨。點(diǎn)擊第三顆星來(lái)修改評(píng)分為3架曹,例如。

[圖片上傳失敗...(image-273d8b-1608214851936)]

為間距和星星數(shù)量添加屬性

確保在代碼中沒(méi)有任何硬編碼的值闹瞧,為評(píng)分的數(shù)量和評(píng)分之間的間距添加屬性绑雄。這樣,你只需修改代碼的一處奥邮。

用屬性代替硬編碼的值
  1. 找到 RatingControl.swift 的 // MARK: Properties 區(qū)域绳慎。

     // MARK: Properties
      
     var rating = 0 {
     didSet {
         setNeedsLayout()
     }
     }
     var ratingButtons = [UIButton]()
    

    點(diǎn)擊編輯區(qū)頂部文件名字使用 functions menu 快速跳過(guò)去。

  2. 在已存在的屬性下面漠烧,添加如下代碼:

     var spacing = 5
    

    使用這個(gè)屬性給你的按鈕增加一些額外間距。

  3. 在 layoutSubviews 中靡砌,使用 spacing 屬性替代那個(gè)作為間距的常量已脓。

     buttonFrame.origin.x = CGFloat(index * (buttonSize + spacing))
    
  4. 在 spacing 屬性下面,添加另外一個(gè)屬性:

     var stars = 5
    

    可以使用這個(gè)屬性來(lái)控制控件的星星數(shù)量通殃。

  5. init?(coder:) 中,使用 stars 屬性替代之前你為星星數(shù)量設(shè)置的常量度液。

     for _ in 0..<stars {
    

檢驗(yàn):運(yùn)行 app。一起都應(yīng)該和之前一樣画舌。

連接評(píng)分控件和視圖控制器

最后一件要做的事情就是給 ViewController 類添加一個(gè)評(píng)分控件的引用堕担。

連接評(píng)分控件 outlet 到 ViewController.swift 中
  1. 打開 storyboard。

  2. 點(diǎn)擊 Xcode 工具欄中的 Assistant 按鈕打開輔助編輯器曲聂。

    [圖片上傳失敗...(image-bca606-1608214851936)]

  3. 如果想要更多的空間來(lái)工作霹购,點(diǎn)擊 Xcode 工具欄中的 Navigator 和 Utilties 按鈕來(lái)收縮項(xiàng)目導(dǎo)航和實(shí)用工具區(qū)。

    [圖片上傳失敗...(image-4a797d-1608214851936)]

    同樣可以收縮大綱視圖朋腋。

  4. 選擇評(píng)分控件齐疙。

    ViewConroller.swift 會(huì)顯示在右邊的編輯器膜楷。(如果沒(méi)有,在右邊的編輯器選擇欄中選擇 Automatic > ViewController.swift)

  5. 按住 Control 從畫板中的評(píng)分控件拖動(dòng)到右邊的編輯器中贞奋,在 photoImageView 屬性的下面停止拖動(dòng)赌厅。

    [圖片上傳失敗...(image-8d53cb-1608214851936)]

  6. 在出現(xiàn)的對(duì)話框中,輸入名字 ratingControl轿塔。
    忽略剩下的選項(xiàng)特愿,對(duì)話框最后應(yīng)該像這樣:

    [圖片上傳失敗...(image-80bfa3-1608214851936)]

  7. 點(diǎn)擊 Connect。

此刻 ViewController 類應(yīng)該有評(píng)分控件的引用了勾缭。

清理項(xiàng)目

現(xiàn)在已經(jīng)很接近最后的食物場(chǎng)景界面了揍障,但是還是需要做一些清理工作。app 正在逐漸實(shí)現(xiàn)更多牛逼的功能且擁有與前面課程完全不同的界面漫拭。需要?jiǎng)h掉一些不需要的代碼亚兄,同時(shí)把控件集中到堆棧視圖中來(lái)平衡界面。

清理界面
  1. 點(diǎn)擊 Standard 按鈕返回標(biāo)準(zhǔn)編輯器采驻。

    [圖片上傳失敗...(image-48437d-1608214851936)]

    點(diǎn)擊 Xcode 工具欄的 Navigator 和 Utiliites 按鈕展開項(xiàng)目導(dǎo)航和實(shí)用工具區(qū)审胚。

  2. 打開 storyboard。

  3. 選擇 Set Default Label Text 按鈕礼旅,點(diǎn)擊 Delete 鍵刪除它膳叨。
    堆棧視圖重新排放 UI 控件來(lái)填充按鈕留下的空間。

    [圖片上傳失敗...(image-80ff66-1608214851936)]

  4. 如果需要痘系,打開大綱視圖菲嘴。選擇 Stack View 對(duì)象。

    [圖片上傳失敗...(image-7b62c4-1608214851936)]

  5. 打開屬性檢查器汰翠。

  6. 在屬性檢查器中龄坪,找到 Alignment 區(qū)域并選擇 Center。
    堆棧視圖中所有控件應(yīng)該水平居中了:

    [圖片上傳失敗...(image-15509a-1608214851936)]

現(xiàn)在复唤,清理剛才刪除按鈕的對(duì)應(yīng)動(dòng)作方法健田。

清理代碼
  1. 打開 ViewController.swift。

  2. 刪除 ViewController.swift 中的 setDefaultLabelText(_:) 動(dòng)作方法佛纫。

     @IBAction func setDefaultLabelText(sender: UIButton) {
         mealNameLabel.text = "Default Text"
     }
    

檢驗(yàn):運(yùn)行 app妓局。一切都和之前一樣,但是 Set Default Label Text 按鈕消失了呈宇,控件也都水平居中了好爬。按鈕并排貼在一起。此刻點(diǎn)擊任何一個(gè)按鈕仍然會(huì)調(diào)用 ratingButtonTapped(_:) 并相應(yīng)修改按鈕圖片甥啄。

重要提示

如果運(yùn)行出現(xiàn)問(wèn)題存炮,嘗試按 Command-Shift-K 來(lái) clear 項(xiàng)目。

[圖片上傳失敗...(image-91ad7a-1608214851936)]

注意:

為了查看本課的完整實(shí)例項(xiàng)目, 下載文件并在 Xcode 中查看它僵蛛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末尚蝌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子充尉,更是在濱河造成了極大的恐慌飘言,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驼侠,死亡現(xiàn)場(chǎng)離奇詭異姿鸿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)倒源,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門苛预,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人笋熬,你說(shuō)我怎么就攤上這事热某。” “怎么了胳螟?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵昔馋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我糖耸,道長(zhǎng)秘遏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任嘉竟,我火速辦了婚禮邦危,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舍扰。我一直安慰自己倦蚪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布边苹。 她就那樣靜靜地躺著审丘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勾给。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天锅知,我揣著相機(jī)與錄音播急,去河邊找鬼。 笑死售睹,一個(gè)胖子當(dāng)著我的面吹牛桩警,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播昌妹,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼捶枢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼握截!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起烂叔,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤谨胞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蒜鸡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胯努,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年逢防,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叶沛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忘朝,死狀恐怖灰署,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情局嘁,我是刑警寧澤溉箕,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站导狡,受9級(jí)特大地震影響约巷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜旱捧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一独郎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧枚赡,春花似錦氓癌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至卢肃,卻和暖如春疲迂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背莫湘。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工尤蒿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人幅垮。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓腰池,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子示弓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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