添加對Interface Builder的支持
如果你在Interface Builder中看rating控件,你會發現它就是個大的、空的矩形。更糟糕的是,如果你選擇rating控件,它的邊框將變成紅色,這表明rating 控件的布局有問題。事實上,還有另外兩個表明可能有問題的跡象。在右側的Activity viewer(活動查看器)有一個黃色警告三角。在View Controller場景旁邊的大綱視圖還有一個紅色的錯誤圖標。
如果你點擊這些圖標,Xcode會顯示關于這兩個錯誤和警告的更多信息。

這兩種情況,根本的原因是一樣的。Interface Builder 不知道任何關于rating控件的內容。為了修復它,你需要使用@IBDesignabel來定義控件。它讓Interface Builder實例化你的控件的一個副本,并直接將其繪制到畫布中。另外,現在Interface Builder具有一個活動的控件副本,它的布局引擎能夠正確的定位和設置控件的大小。
把控件聲明為@IBDesignable
- 在RatingControl.swift,找到類聲明:
class RatingControl: UIStackView {
- 在它前面加上 @IBDesignable。
@IBDesignable class RatingControl: UIStackView {
- 按下Command-B來構建項目(或者選擇 Product > Build)。
-
打開Main.storyboard。當構建完成,storyboard將顯示rating控件的實時視圖。
image: ../Art/ICC_designableliveview_2x.png
注意,現在畫布正確的設置了RatingControl視圖的尺寸和位置。而警告和錯誤也已經消失。
Interface Builder能夠做很多事,不僅僅是顯示你的自定義視圖。你能夠指定一些屬性可以在Attributes Inspector中被設置。添加@IBInspectable屬性到所需的屬性。Interface Builder支持基本類型(以及相應的可選項)的檢查,包括:布爾值、數字、字符串,以及CGRect、CGSize、CDPoint和UIColor。
添加可檢查屬性
- 在RatingControl.swift中,在//MARK: Properties 部分的下面添加如下屬性:
@IBInspectable var starSize: CGSize = CGSize(width: 44.0, height: 44.0)
@IBInspectable var starCount: Int = 5
這幾行代碼定義了按鈕的尺寸,并定義了你的控件有多少個按鈕。
- 現在你需要使用這些值。定位到setupButtons()方法,做如下改變:
- 在for-in聲明,把數字5改為startCount。
- 在 button.heightAnchor.constraint()方法調用,把數字44.0改為starSize.height。
- 在 button.widthAnchor.constraint()方法調用,把數字44.0改為starSize.width。現在方法應該如下所示:
private func setupButtons() {
for _ in 0..<starCount {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: starSize.width).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)
}
}
如果你切換到 Main.storyboard并選擇RatingControl,你將看到Star Size 和Star Count 已設置到了Attributes inspector中。虛線表示控件當前正在使用默認的值(44.0點和5星)。但是現在改變這些值還不會改變控件。
- 要更新控件,你需要在每次這些屬性改變的時候重新設置控件的按鈕。為了實現它,給每個屬性添加一個屬性觀察器(property observer)。屬性觀察器在屬性值每次被設置時調用,并且可以在值改變之前或之后立刻執行。
@IBInspectable var starSize: CGSize = CGSize(width: 44.0, height: 44.0) {
didSet {
setupButtons()
}
}
@IBInspectable var starCount: Int = 5 {
didSet {
setupButtons()
}
}
這里,你為starSize和starCount屬性定義了屬性觀察器。具體來說,didSet屬性觀察器會在屬性值被設置之后立刻被調用。你的實現是調用 setupButtons()方法。這個方法使用更新的尺寸和數量添加新的按鈕;但是,這個實現沒有擺脫舊的按鈕。
- 為了清除舊的按鈕,在setupButtons() 方法的開始位置添加如下代碼:
// clear any existing buttons
for button in ratingButtons {
removeArrangedSubview(button)
button.removeFromSuperview()
}
ratingButtons.removeAll()
這段代碼遍歷所有的rating控件的按鈕。首先,它從stack view管理的視圖列表中刪除按鈕。這告訴stack view它不用再計算這個按鈕的尺寸和位置——但按鈕仍然是stack view的子視圖。接下來,代碼把按鈕從stack view中完全刪除。最后,當所有的按鈕都被刪除后,代碼清空ratingButtons數組。
現在setupButtons()方法看上去是這樣的。
private func setupButtons() {
// clear any existing buttons
for button in ratingButtons {
removeArrangedSubview(button)
button.removeFromSuperview()
}
ratingButtons.removeAll()
for _ in 0..<starCount {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: starSize.width).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)
}
}
注意
從性能角度上看,刪除并替換所有按鈕并不是一個好主意。但是,didSet觀察器只能在設計的時候被Interface Builder調用。當應用運行時,setupButtons()只被調用一次,在控件第一次從storyboard被加載的時候。因此,沒有必要創建更復雜的解決方案來更新現有的按鈕。
檢查點:打開Main.storyboard并選擇RatingControl對象。嘗試改變Start Size和StarCount屬性。畫布中的控件會發生改變以匹配新的設置。運行應用,你將在模擬器中看到這些改變。
記住,當你測試完了之后,把值改回默認的。
進一步探索
更多關于使用自定義視圖的信息,見Xcode help中的Lay out user interfaces > Add objects and media > Render custom views。
添加星星圖片到按鈕
接下來,你將添加空的、填充的、以及高亮的星星圖片到按鈕。
你可以在課后的下載文件中找到Images文件,從里面找到這些圖片,或者使用你自己的圖片。(確保圖片的名字和你在稍后代碼中圖片的名字保持一致。)
添加圖片到你的項目
- 在project navigator中,選擇Assets.xcassets來查看資源目錄(asset catalog)。
回想一下,資源目錄是為應用存儲和組織圖片資源的地方。 -
在左下角,點擊加號(+)并從彈出菜單選擇New Folder。
image: ../Art/ICC_assetcatalog_addfolder_2x.png - 雙擊文件名稱,重命名為Rating Images。
- 選中這個文件,在右下角,點擊加號按鈕并在彈出菜單中選擇New Image Set。
一個圖片集合(image set)代表一個圖像資源,但是能夠包含圖像的不同版本,這些版本是用來在不同屏幕分辨率上顯示的。 - 雙擊image set的名字,重命名為emptyStar。
- 在電腦中,選擇你想添加的空心星星圖片。
-
拖拽這個圖片放到image set的2x插槽內。
image: ../Art/ICC_emptystar_drag_2x.png
2x是本課你選中的iPhone 7模擬器的顯示分辨率。
- 選中這個文件,在右下角,點擊加號按鈕并在彈出菜單中選擇New Image Set。
- 雙擊image set的名字,重命名為filledStar。
- 在電腦上,選擇你想要添加的填充星星圖片。
-
拖拽這個圖片放到image set的2x插槽內。
image: ../Art/ICC_filledstar_drag_2x.png - 選中這個文件,在右下角,點擊加號按鈕并在彈出菜單中選擇New Image Set。
- 雙擊image set的名字,重命名為highlightedStar。
- 在電腦上,選擇你想要添加的填充星星圖片。
-
拖拽這個圖片放到image set的2x插槽內。
image: ../Art/ICC_highlightedstar_drag_2x.png
你的資源目錄看上去是這樣的。
接下來,編寫代碼來在相應的時候為按鈕設置合適的圖片。
為按鈕設置星星圖片
- 在RatingControl.swift導航到setupButtons()方法,并且在創建按鈕的for-in循環的上面添加如下代碼:
// Load Button Images
let bundle = Bundle(for: type(of: self))
let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
let emptyStar = UIImage(named:"emptyStar", in: bundle, compatibleWith: self.traitCollection)
let highlightedStar = UIImage(named:"highlightedStar", in: bundle, compatibleWith: self.traitCollection)
這些行從資源目錄加載星星圖片。注意資源目錄是在應用的主束(bundle)里。這意味著應用可以使用 UIImage(named:)方法加載圖片。但是,因為控件是@IBDesignable,所以代碼也需要運行在Interface Builder中。要讓圖片在Interface Builder中正確的加載,你必須明確指定目錄的束。這樣就確保系統能找到并加載圖片。
- 找到設置背景顏色的代碼行,并用下面的代碼進行替換。
// Set the button images
button.setImage(emptyStar, for: .normal)
button.setImage(filledStar, for: .selected)
button.setImage(highlightedStar, for: .highlighted)
button.setImage(highlightedStar, for: [.highlighted, .selected])
按鈕有五種不同狀態:normal(一般)、高亮(highlighted)、聚焦(focused)、選中(selected)、和禁用(disabled)。默認時,按鈕根據它的狀態來修改自身的顯示,例如,一個禁用的按鈕呈現灰色。一個按鈕可以在同時呈現多種狀態,例如按鈕即是禁用又是高亮。
按鈕總是從normal狀態開始(不是高亮、選中、聚焦、或者禁用)。無論何時用戶點擊時,按鈕是高亮。你也能用代碼設置按鈕是選中還是禁用。聚焦狀態使用在基于焦點的界面,例如Apple TV。在上面的代碼中,你告訴按鈕normal狀態下,使用空心星星圖片。這時按鈕默認的圖片。每當一個狀態或混合狀態沒有它們自己的圖片時,系統就會使用這個圖片(可能具有附加效果)。
接下來,上面的代碼為選中狀態設置了填充圖片。如果你用編碼的方式將按鈕設置為選中,它將從空心星星變為已填充星星。最后,為高亮狀態以及高亮和選中混合狀態都設置高亮圖片。當用戶點擊按鈕的時候,無論是否選中,系統都會顯示高亮按鈕圖片。
你的setupButtons()方法看上去是這樣了:
private func setupButtons() {
// Clear any existing buttons
for button in ratingButtons {
removeArrangedSubview(button)
button.removeFromSuperview()
}
ratingButtons.removeAll()
// Load Button Images
let bundle = Bundle(for: type(of: self))
let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
let emptyStar = UIImage(named:"emptyStar", in: bundle, compatibleWith: self.traitCollection)
let highlightedStar = UIImage(named:"highlightedStar", in: bundle, compatibleWith: self.traitCollection)
for _ in 0..<starCount {
// Create the button
let button = UIButton()
// Set the button images
button.setImage(emptyStar, for: .normal)
button.setImage(filledStar, for: .selected)
button.setImage(highlightedStar, for: .highlighted)
button.setImage(highlightedStar, for: [.highlighted, .selected])
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: starSize.width).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)
}
}
檢查點:運行應用。你將看到星星替代了紅色按鈕。點擊這里的任何按鈕讓然會調用ratingButtonTapped(_:)并且在控制臺上打印消息。當你點擊按鈕的時候甚至能看到高亮星星,但是你的按鈕還沒有變為填充圖片。你要接著修改。

實現按鈕動作
用戶需要能夠通過點擊星星來選擇評分,所以你要使用真正的代碼來代替ratingButtonTapped(_:) 方法中調試用的代碼。
實現評分動作
- 在RatingControl.swift中,找到ratingButtonTapped(button:)方法。
func ratingButtonTapped(button: UIButton) {
print("Button pressed ??")
}
- 用下面的代碼替換print語句:
func ratingButtonTapped(button: UIButton) {
guard let index = ratingButtons.index(of: button) else {
fatalError("The button, \(button), is not in the ratingButtons array: \(ratingButtons)")
}
// Calculate the rating of the selected button
let selectedRating = index + 1
if selectedRating == rating {
// If the selected star represents the current rating, reset the rating to 0.
rating = 0
} else {
// Otherwise set the rating to the selected star
rating = selectedRating
}
}
在上面的代碼中,indexOf(:)方法嘗試在按鈕數組中找這個按鈕,并在找到后返回它在數組中的索引值。這個方法返回的是可選類型Int,因為你查找的對象在集合中可能不存在。但是,因為觸發該動作的唯一按鈕集是你創建的,如果indexOf(:)方法不能找到一個匹配的按鈕,那么代碼就有了嚴重的錯誤。拋出錯誤、終止應用,并在控制臺上打印有用的錯誤信息,幫助你在設計和測試應用時找到并修復錯誤。
一旦你有按鈕的索引的時候(這個值在0-4之間),你給索引加1來計算評分(就是1-5之間的值)。如果用戶點擊的星星恰好是當前的評分,你就重置控件的rating屬性為0。否則,你就設置rating值為選中的評分值。
- 一旦評分值被設置,你需要一些方法來更新按鈕的顯示。在RatingControl.swift中,在結束的花括號前(}),添加如下方法:
private func updateButtonSelectionStates() {
}
這個輔助方法可以用來更新按鈕的選擇狀態。
- 在updateButtonSelectionStates()方法中,添加如下for-in循環:
private func updateButtonSelectionStates() {
for (index, button) in ratingButtons.enumerated() {
// If the index of a button is less than the rating, that button should be selected.
button.isSelected = index < rating
}
}
這個代碼遍歷按鈕數組,并基于評分的位置對每個按鈕的選中狀態進行設置。就像你較早前看到的,選中狀態影響按鈕的呈現。如果按鈕的索引小于評分,則isSelected屬性設置為true,并且按鈕顯示填充的星星圖片。否則,isSelected屬性設置為false,按鈕顯示空心星星圖片。
- 添加一個屬性觀察器到rating屬性,當rating值改變時都會調用updateButtonSelectionStates()方法。
var rating = 0 {
didSet {
updateButtonSelectionStates()
}
}
- 每當按鈕添加到控件的時候,也需要更新按鈕的選擇狀態。在setupButtons()方法中,在方法結束的花括號(})之前添加 updateButtonSelectionStates()方法。現在setupButtons()方法看上去是這樣的:
private func setupButtons() {
// Clear any existing buttons
for button in ratingButtons {
removeArrangedSubview(button)
button.removeFromSuperview()
}
ratingButtons.removeAll()
// Load Button Images
let bundle = Bundle(for: type(of: self))
let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
let emptyStar = UIImage(named:"emptyStar", in: bundle, compatibleWith: self.traitCollection)
let highlightedStar = UIImage(named:"highlightedStar", in: bundle, compatibleWith: self.traitCollection)
for index in 0..<starCount {
// Create the button
let button = UIButton()
// Set the button images
button.setImage(emptyStar, for: .normal)
button.setImage(filledStar, for: .selected)
button.setImage(highlightedStar, for: .highlighted)
button.setImage(highlightedStar, for: [.highlighted, .selected])
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: starSize.width).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)
}
updateButtonSelectionStates()
}
檢查點:運行應用。你應該看到五顆星星,點擊一顆就改變評分。例如,點擊第三顆星星把評分改為3。第二次點擊同一顆星星,控件將評分重置為零顆星。
添加輔助信息
借助iOS內置輔助功能,您可以為每個客戶(包括有特殊需求的客戶)提供出色的移動體驗。 這些功能包括VoiceOver,開關控制,隱藏式字幕或音頻描述視頻的回放,指導訪問,文本到語音等。
在大多數情況下,用戶從這些功能中獲得好處而無需任何額外的工作。然而,VoiceOver,通常需要一些額外的工作。VoiceOver是為盲人和低視力用戶提供的革命性屏幕閱讀功能。VoiceOver把用戶界面讀給用戶聽。盡管內置控件的默認描述提供了一個很好的開端,但是你可能需要優化用戶界面的顯示;特別是自定義視圖和控件。
- 附加功能標簽(Accessibility label)。一個簡短的本地化單詞或短語,簡潔的描述這個控件或視圖,但是不能辨認元素的類型。例如“添加”或“播放”。
- 附加功能值(Accessibility value)。一個元素的當前值,當該值不由標簽表示時。例如一個滑塊(slider)的標簽可能是“速度”,但它的當前值可能是“50%”。
- 附加功能提示(Accessibility hint)。一個簡短的本地化短語,用來描述一個元素的動作的結果。例如“添加一個標題”或者“打開購物單”。
在rating控件中,每個按鈕的附加功能標簽描述了每個按鈕設置的值。例如,第一個按鈕標簽是“設置一個評分。”附加功能值包含了控件當前的評分。例如,如果你有一個4星的評分,這個值是“4星設置”。最后,你分配一個提示給當前選中的星星,“點擊重置評分為零。”所有其他星星的提示值為nil,因為它們的效果是已經被它們的標簽描述了。
添加附加功能標簽、值、和提示
- 在 RatingControl.swift中,導航到setupButtons()方法,找到for-in聲明。
for index in 0..<starCount {
- 在for-in循環內部,緊接著約束,添加如下代碼:
// Set the accessibility label
button.accessibilityLabel = "Set \(index + 1) star rating"
這段代碼使用按鈕的所以計算標簽字符串,然后把它分配到按鈕的accessibilityLabel屬性。setupButtons()方法看上去是這樣的:
private func setupButtons() {
// Clear any existing buttons
for button in ratingButtons {
removeArrangedSubview(button)
button.removeFromSuperview()
}
ratingButtons.removeAll()
// Load Button Images
let bundle = Bundle(for: type(of: self))
let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
let emptyStar = UIImage(named:"emptyStar", in: bundle, compatibleWith: self.traitCollection)
let highlightedStar = UIImage(named:"highlightedStar", in: bundle, compatibleWith: self.traitCollection)
for index in 0..<starCount {
// Create the button
let button = UIButton()
// Set the button images
button.setImage(emptyStar, for: .normal)
button.setImage(filledStar, for: .selected)
button.setImage(highlightedStar, for: .highlighted)
button.setImage(highlightedStar, for: [.highlighted, .selected])
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: starSize.width).isActive = true
// Set the accessibility label
button.accessibilityLabel = "Set \(index + 1) star rating"
// 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)
}
updateButtonSelectionStates()
}
- 導航到updateButtonSelectionStates()方法。在for-in循環內部,緊挨著設置按鈕的isSelected屬性下面,添加如下代碼:
// Set the hint string for the currently selected star
let hintString: String?
if rating == index + 1 {
hintString = "Tap to reset the rating to zero."
} else {
hintString = nil
}
// Calculate the value string
let valueString: String
switch (rating) {
case 0:
valueString = "No rating set."
case 1:
valueString = "1 star set."
default:
valueString = "\(rating) stars set."
}
// Assign the hint string and value string
button.accessibilityHint = hintString
button.accessibilityValue = valueString
這里,你通過檢查按鈕是否是當前選中的按鈕開始。如果它是,你就分配一個提示。如果不是,你就設置按鈕的hintString屬性為nil。
接下來,你基于控件的評分計算值。使用switch語句,如果評分是0或1,則分配一個自定義字符串。如果評分大于1,你就使用字符串插值來計算提示內容。最后,分配這些值給accessibilityHint和accessibilityValue屬性。
當用戶在VoiceOver可用的環境里運行應用,當用戶點擊其中一個按鈕的時候,VoicePver就會閱讀這個按鈕的標簽,跟在單詞按鈕后面。然后讀附加功能值。最后它讀附加功能提示(如果有)。這讓用戶知道控件當前的值,以及按下當前的按鈕會有什么結果。
進一步探索
更多關于附加功能的信息,參見Accessibility on iOS.
還有,因為本課的目的,你只是分配了簡單的字符串給附加功能屬性;但是,一個產品級的應用應該使用本地化字符串。更多關于國際化和本地化的信息,參見Build Apps for the World。
連接Rating控件到View Controller
作為設置rating控件的最后一步,你需要把它的一個引用給ViewController。
連接rating控件到ViewController.swift
- 打開storyboard。
-
點擊Xcode工具條上的Assistant 按鈕來打開助理編輯器。
image: ../Art/assistant_editor_toggle_2x.png -
想要更大空間,就把project navigator和utility area折疊起來。
image: ../Art/navigator_utilities_toggle_on_2x.png
也可以把大綱視圖折疊起來。
- 選擇rating 控件。
ViewController.swift顯示在右側的編輯器。(如果不是這樣,在編輯器選擇器欄里選擇 Automatic > ViewController.swift)。 -
把rating控件拖拽到photoImageView屬性的下面。
image: ../Art/ICC_ratingcontrol_dragoutlet_2x.png -
在彈出的對話框中,Name字段鍵入ratingControl。
其他選項不變。你的對話框看上去是這樣的:
image: ../Art/ICC_ratingcontrol_addoutlet_2x.png - 點擊連接。
ViewController類現在有一個引用指向storyboard中的rating控件。
清理項目
你已接近完成菜品場景的用戶界面了,但在之前你需要做一些清理工作。現在這個FoodTracker應用實現了很多比之前課程更高級的行為和不同的用戶界面,你應該移除一些不再需要的部分。你還需要把元素放到棧視圖的中心,以平衡界面。
清理UI
-
返回到標準編輯器。
image: ../Art/standard_toggle_2x.png - 打開storyboard。
-
選擇Set Default Label Text按鈕,然后按下刪除鍵刪除它。
棧視圖布置你的界面元素填充按鈕留下來的控件。
image: ../Art/ICC_deletebutton_2x.png -
如果必要,打開大綱視圖,選擇Stack View對象。
image: ../Art/ICC_outlineview_2x.png - 打開Attributes inspector
-
在Attributes inspector中,找到Alignment(對齊)字段并選擇Center。
在棧視圖中的元素都居中對齊:
image: ../Art/ICC_centerstack_2x.png
現在,移除和你刪掉的按鈕對應的action方法。
清理代碼
- 打開 ViewController.swift.
- 在ViewController.swift中,刪除setDefaultLabelText(_:) action方法。
@IBAction func setDefaultLabelText(sender: UIButton) {
mealNameLabel.text = "Default Text"
}
這就是現在你需要刪除的全部了。你將在下一課對label outlet(mealNameLabel)作出一些改變。
檢查點:運行應用。所有事都應該和之前一樣,只是沒有那個刪掉的按鈕了,并且元素都水平居中了。按鈕應該是并排的。點擊任何一個按鈕仍然調用ratingButtonTapped(_:),并且會恰當的改變按鈕的圖片。
重要
如果你運行出現構建問題,嘗試按下Command-Shift-K組合鍵來清理你的項目。
小結
在本課中,你學習了如何構建一個自定義控件,它能顯示在Interface Builder中。這個控件還會在Attributes inspector中顯示可修改的屬性。最后,你添加了附加功能信息,確保控件能很好的使用Voice Over。
下一課,你將設計和連接應用的數據模型。
注意
想看本課的完整代碼,下載這個文件并在Xcode中打開。
下載文件