歡迎回到 macOS 開發(fā)教程初學者系列 3 部分中的第 3 部分,也是最后一個部分!
在第 1 部分中,學習了如何安裝 Xcode 以及創(chuàng)建簡單的 app。在 第 2 部分 中,為更復雜的 app 創(chuàng)建了用戶界面,但還不能正常工作,因為沒有寫任何代碼。在這部分,會添加 Swift 代碼,以使 app 正常工作!
開始
如果尚未完成第 2 部分或希望用干凈的模板開始,可 下載項目文件,帶有布局好的 UI,就和第 2 部分結(jié)尾的時候一樣。打開此項目或你自己在第 2 部分里的項目,運行一下確定 UI 已全部就位。同樣也把 Preferences 打開檢查一下。
沙盒
在你深入代碼之前,花一點時間學習沙盒(sandboxing)。如果你是一個 iOS 程序員,你已經(jīng)熟悉這個概念——否則就請繼續(xù)閱讀。
沙盒 app 有自己的空間,可以使用單獨的文件存儲區(qū)域,無法訪問其他 app 創(chuàng)建的文件,具有有限的訪問權(quán)限。對于 iOS app,這是唯一的選擇。對于 macOS app,這是可選的;但是,如果要通過 Mac App Store 分發(fā) app,則必須將其沙盒化。一般情況下,都應將 app 沙盒化,因為這使 app 減少潛在問題。
要為 Egg Timer app 啟用沙盒,請在 Project Navigator 中選擇項目——頂部帶有藍色圖標的那個。在 Targets 中選擇 EggTimer(只列出了一個 target),然后單擊頂部選項卡中的 Capabilities。單擊開關(guān)以啟用 App Sandbox。屏幕會展開,以顯示現(xiàn)在 app 可以請求的各種權(quán)限。這個 app 什么都不需要,所以不要勾選它們。
組織文件
看看 Project Navigator。列出了所有文件,但毫無紀律。這個 app 不會有很多文件,但把類似的文件分組在一起是好的做法,可以更有效的導航,特別是對于較大的項目來說。
選擇兩個視圖控制器文件,方法是單擊一個,然后按住 Shift 鍵單擊下一個。右鍵單擊并從彈出菜單中選擇 New Group from Selection。將新組命名為 View Controllers。
該項目馬上會有一些模型文件,因此選擇頂部 EggTimer 組,右鍵單擊并選擇 New Group。取名為 Model。
最后,選擇 Info.plist 和 EggTimer.entitlements,并將它們放入名為 Supporting Files 的組。
拖動組和文件,直到 Project Navigator 看起來像這樣:
MVC
這個 app 使用 MVC 模式:Model View Controller。
app 的主要模型將是一個名為 EggTimer 的類。這個類將具有定時器的開始時間、所請求的持續(xù)時間和已經(jīng)過去的時間等屬性。它還會有一個 Timer 對象,每秒觸發(fā)、自我更新。EggTimer 對象還會有 start,stop,resume 和 reset 方法。
EggTimer 模型類保存數(shù)據(jù)并執(zhí)行操作,但不了解如何顯示它們。 Controller(在這種情況下是 ViewController)了解 EggTimer 類(Model),并且有一個 View 可以用來顯示數(shù)據(jù)。
為了與 ViewController 通信,EggTimer 使用委托協(xié)議。當某事發(fā)生變化時,EggTimer 向其 delegate 發(fā)送一條消息。ViewController 將自身分配為 EggTimer 的 delegate,所以由它來接收消息,然后它可以在自己的 View 中顯示新的數(shù)據(jù)。
編寫 EggTimer
在 Project Navigator 里選擇 Model 組,然后選擇 File/New/File…,選擇 macOS/Swift File 然后點擊 Next。將文件命名為 EggTimer.swift,然后點擊 Create 以保存它。
添加如下代碼:
class EggTimer {
var timer: Timer? = nil
var startTime: Date?
var duration: TimeInterval = 360 // default = 6 minutes
var elapsedTime: TimeInterval = 0
}
這樣就設(shè)置了 EggTimer 類及其屬性。 TimeInterval 實際上是 Double,意思為秒數(shù)。
接下來要在類中添加兩個計算屬性,就在前面那些屬性之后:
var isStopped: Bool {
return timer == nil && elapsedTime == 0
}
var isPaused: Bool {
return timer == nil && elapsedTime > 0
}
這是用于快速確定 EggTimer 狀態(tài)的方式。
將 delegate 協(xié)議的定義插入 EggTimer.swift 文件,但在 EggTimer 類的外面——我喜歡將協(xié)議定義放在文件的頂部,import 的后面。
protocol EggTimerProtocol {
func timeRemainingOnTimer(_ timer: EggTimer, timeRemaining: TimeInterval)
func timerHasFinished(_ timer: EggTimer)
}
協(xié)議規(guī)定了一個契約,任何符合 EggTimerProtocol 的對象必須提供這兩個函數(shù)。
現(xiàn)在你已經(jīng)定義了一個協(xié)議,EggTimer 需要一個可選的 delegate 屬性,該屬性設(shè)置為符合此協(xié)議的任何對象。EggTimer 不知道或不關(guān)心 delegate 是什么類型的對象,因為它只要確定 delegate 有這兩個函數(shù)就行了。
將此行添加到 EggTimer 類中的現(xiàn)有屬性中:
var delegate: EggTimerProtocol?
啟動 EggTimer 的 timer 對象將每秒觸發(fā)一次函數(shù)調(diào)用。插入此代碼,定義了將由定時器調(diào)用的函數(shù)。必須要有關(guān)鍵字 dynamic,以便 Timer 能夠找到它。
dynamic func timerAction() {
// 1
guard let startTime = startTime else {
return
}
// 2
elapsedTime = -startTime.timeIntervalSinceNow
// 3
let secondsRemaining = (duration - elapsedTime).rounded()
// 4
if secondsRemaining <= 0 {
resetTimer()
delegate?.timerHasFinished(self)
} else {
delegate?.timeRemainingOnTimer(self, timeRemaining: secondsRemaining)
}
}
會發(fā)生什么?
-
startTime是一個Optional Date——如果是nil,timer 就無法運行,所以什么都不會發(fā)生。 - 重新計算
elapsedTime屬性。startTime早于當前,因此timeIntervalSinceNow會生成負數(shù)。用減號使得 elapsedTime 是正數(shù)。 - 計算 timer 的剩余秒數(shù),四舍五入以給出整數(shù)秒。
- 如果 timer 已經(jīng)完成,重置它并告訴 delegate 它已經(jīng)完成。否則,告訴 delegate 剩余的秒數(shù)。由于
delegate是可選屬性,? 號用于執(zhí)行可選鏈。如果 delegate 沒有設(shè)置,這些方法將不會被調(diào)用,也就不會出現(xiàn)意外情況了。
添加 EggTimer 類所需的最后一點代碼的時候,你會看到一個錯誤:timer 的 starting, stopping, resuming 和 resetting 方法。
// 1
func startTimer() {
startTime = Date()
elapsedTime = 0
timer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(timerAction),
userInfo: nil,
repeats: true)
timerAction()
}
// 2
func resumeTimer() {
startTime = Date(timeIntervalSinceNow: -elapsedTime)
timer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(timerAction),
userInfo: nil,
repeats: true)
timerAction()
}
// 3
func stopTimer() {
// really just pauses the timer
timer?.invalidate()
timer = nil
timerAction()
}
// 4
func resetTimer() {
// stop the timer & reset back to start
timer?.invalidate()
timer = nil
startTime = nil
duration = 360
elapsedTime = 0
timerAction()
}
這些函數(shù)做了什么?
-
startTimer使用Date()將啟動時間設(shè)置為現(xiàn)在、設(shè)置了重復的Timer。 -
resumeTimer是 timer 已暫停并正在重新啟動時調(diào)用的內(nèi)容?;谝堰^去的時間重新計算開始時間。 -
stopTimer停止了重復的 timer。 -
resetTimer停止了重復的 timer 并將屬性恢復為默認值。
這些函數(shù)還全部調(diào)用了 timerAction,以便屏幕可以立即刷新。
ViewController
現(xiàn)在 EggTimer 對象已經(jīng)正常工作了,現(xiàn)在回到 ViewController.swift 讓屏幕改變以反映這一點。
ViewController 已經(jīng)有 @IBOutlet 屬性了,現(xiàn)在給它一個 EggTimer 屬性:
var eggTimer = EggTimer()
將下面這行駕到 viewDidLoad 中,替換掉注視行:
eggTimer.delegate = self
這將導致一個錯誤,因為 ViewController 不符合 EggTimerProtocol。當符合協(xié)議時,為協(xié)議創(chuàng)建單獨的擴展,會使代碼更干凈。在 ViewController 類定義下面添加這段代碼:
extension ViewController: EggTimerProtocol {
func timeRemainingOnTimer(_ timer: EggTimer, timeRemaining: TimeInterval) {
updateDisplay(for: timeRemaining)
}
func timerHasFinished(_ timer: EggTimer) {
updateDisplay(for: 0)
}
}
錯誤消失了,因為 ViewController 現(xiàn)在有 EggTimerProtocol 所需的兩個函數(shù)。但是這兩個函數(shù)都調(diào)用了還不存在的 updateDisplay。
這是 ViewController 的另一個擴展,包含了用于顯示的函數(shù):
extension ViewController {
// MARK: - Display
func updateDisplay(for timeRemaining: TimeInterval) {
timeLeftField.stringValue = textToDisplay(for: timeRemaining)
eggImageView.image = imageToDisplay(for: timeRemaining)
}
private func textToDisplay(for timeRemaining: TimeInterval) -> String {
if timeRemaining == 0 {
return "Done!"
}
let minutesRemaining = floor(timeRemaining / 60)
let secondsRemaining = timeRemaining - (minutesRemaining * 60)
let secondsDisplay = String(format: "%02d", Int(secondsRemaining))
let timeRemainingDisplay = "\(Int(minutesRemaining)):\(secondsDisplay)"
return timeRemainingDisplay
}
private func imageToDisplay(for timeRemaining: TimeInterval) -> NSImage? {
let percentageComplete = 100 - (timeRemaining / 360 * 100)
if eggTimer.isStopped {
let stoppedImageName = (timeRemaining == 0) ? "100" : "stopped"
return NSImage(named: stoppedImageName)
}
let imageName: String
switch percentageComplete {
case 0 ..< 25:
imageName = "0"
case 25 ..< 50:
imageName = "25"
case 50 ..< 75:
imageName = "50"
case 75 ..< 100:
imageName = "75"
default:
imageName = "100"
}
return NSImage(named: imageName)
}
}
updateDisplay 使用私有函數(shù)獲取剩余時間的文本和圖像,并在 text field 和 image view 中顯示它們。
textToDisplay 將剩余秒數(shù)轉(zhuǎn)換為 M:SS 格式。 imageToDisplay 計算煮蛋程度的百分比,并選擇匹配的圖像。
所以 ViewController 有了一個 EggTimer 對象,它也有從 EggTimer 接收數(shù)據(jù)并顯示結(jié)果的函數(shù),但按鈕還沒有編碼。在第 2 部分中,已經(jīng)為按鈕設(shè)置了 @IBActions。
這里是這些 action 函數(shù)的代碼,把它們替換掉:
@IBAction func startButtonClicked(_ sender: Any) {
if eggTimer.isPaused {
eggTimer.resumeTimer()
} else {
eggTimer.duration = 360
eggTimer.startTimer()
}
}
@IBAction func stopButtonClicked(_ sender: Any) {
eggTimer.stopTimer()
}
@IBAction func resetButtonClicked(_ sender: Any) {
eggTimer.resetTimer()
updateDisplay(for: 360)
}
這3個 action 調(diào)用之前添加的 EggTimer 方法。
現(xiàn)在構(gòu)建并運行 app,然后單擊.Start 按鈕。
還少幾個功能:Stop 和 Reset 按鈕總是在禁用狀態(tài),以及只能煮一個 6 分鐘的蛋??梢允褂?Timer 菜單來控制 app; 嘗試使用菜單和鍵盤快捷鍵來停止,啟動和重置。
如果足夠有耐心,你會看到煮的時候雞蛋變了顏色,最后在煮好時顯示了 “DONE!”。
根據(jù) timer 狀態(tài),按鈕應該啟用或禁用,并且 Timer 菜單項應該與之匹配。
將這個函數(shù)添加到 ViewController,放在用與顯示的 extension 里面:
func configureButtonsAndMenus() {
let enableStart: Bool
let enableStop: Bool
let enableReset: Bool
if eggTimer.isStopped {
enableStart = true
enableStop = false
enableReset = false
} else if eggTimer.isPaused {
enableStart = true
enableStop = false
enableReset = true
} else {
enableStart = false
enableStop = true
enableReset = false
}
startButton.isEnabled = enableStart
stopButton.isEnabled = enableStop
resetButton.isEnabled = enableReset
if let appDel = NSApplication.shared().delegate as? AppDelegate {
appDel.enableMenus(start: enableStart, stop: enableStop, reset: enableReset)
}
}
此函數(shù)使用 EggTimer 狀態(tài)(還記得添加到 EggTimer 的那幾個計算變量嗎)來確定應啟用哪些按鈕。
在第 2 部分中,你把 Timer 菜單項設(shè)置為 AppDelegate 的屬性,因此AppDelegate 是配置它們的地方。
切換到 AppDelegate.swift 添加如下函數(shù):
func enableMenus(start: Bool, stop: Bool, reset: Bool) {
startTimerMenuItem.isEnabled = start
stopTimerMenuItem.isEnabled = stop
resetTimerMenuItem.isEnabled = reset
}
為了在首次啟動 app 時正確配置菜單,請將此行添加到 applicationDidFinishLaunching 方法中:
enableMenus(start: true, stop: false, reset: false)
每當按鈕或菜單項動作改變 EggTimer 的狀態(tài)時,就需要改變按鈕和菜單。切換回 ViewController.swift 并將此行添加到 3 個按鈕 action 函數(shù)中每一個的末尾:
configureButtonsAndMenus()
再次構(gòu)建并運行 app,可以看到按鈕按預期啟用和禁用。檢查一下菜單項;它們應該會反映按鈕的狀態(tài)。
偏好設(shè)置
這個 app 還有一個大問題——如果你不想把雞蛋煮 6 分鐘怎么辦?
在第 2 部分中,我們設(shè)計了 Preferences 窗口以允許選擇不同的時間。此窗口由 PrefsViewController 控制,但它需要一個模型對象來處理數(shù)據(jù)存儲以及檢索。
將使用 UserDefaults 存儲 Preferences,UserDefaults 是在 app 容器中用鍵值對存儲小數(shù)據(jù)到 Preferences 文件夾中的方式。
右擊 Project Navigator 中的 Model 組,然后選擇 New File… 選擇 macOS/Swift File ,然后單擊 Next。將文件命名為 Preferences.swift ,然后單擊 Create。將此代碼添加到 Preferences.swift 文件:
struct Preferences {
// 1
var selectedTime: TimeInterval {
get {
// 2
let savedTime = UserDefaults.standard.double(forKey: "selectedTime")
if savedTime > 0 {
return savedTime
}
// 3
return 360
}
set {
// 4
UserDefaults.standard.set(newValue, forKey: "selectedTime")
}
}
}
這段代碼可以做什么?
- 叫做
selectedTime的計算變量定義為TimeInterval。 - 請求變量的值時,
UserDefaults單例取出分配給鍵 “selectedTime” 的Double值。如果值未定義,UserDefaults將返回零,但如果值大于 0,則將其作為selectedTime的值返回。 - 如果
selectedTime沒有被定義,使用默認值 360(6 分鐘)。 -
selectedTime被改變的時候,將新值寫入UserDefaults的鍵 “selectedTime”。
因此,通過使用帶有 getter 和 setter 的計算變量,UserDefaults 的數(shù)據(jù)存儲將被自動處理。
現(xiàn)在切換到 PrefsViewController.swift,第一件事是更新顯示以反映現(xiàn)有偏好設(shè)置或默認值。
首先,在 outlets 下面添加此屬性:
var prefs = Preferences()
在這里,你創(chuàng)建了一個 Preferences 實例,以便訪問 selectedTime 計算變量。
然后,添加這些方法:
func showExistingPrefs() {
// 1
let selectedTimeInMinutes = Int(prefs.selectedTime) / 60
// 2
presetsPopup.selectItem(withTitle: "Custom")
customSlider.isEnabled = true
// 3
for item in presetsPopup.itemArray {
if item.tag == selectedTimeInMinutes {
presetsPopup.select(item)
customSlider.isEnabled = false
break
}
}
// 4
customSlider.integerValue = selectedTimeInMinutes
showSliderValueAsText()
}
// 5
func showSliderValueAsText() {
let newTimerDuration = customSlider.integerValue
let minutesDescription = (newTimerDuration == 1) ? "minute" : "minutes"
customTextField.stringValue = "\(newTimerDuration) \(minutesDescription)"
}
看上去有很多代碼,一步步看一遍:
- 請求 prefs 對象的
selectedTime,并將其從秒數(shù)轉(zhuǎn)換為整數(shù)分鐘。 - 如果找不到匹配的預設(shè)值,請將默認值設(shè)置為 “Custom”。
- 遍歷循環(huán)
presetsPopup中的菜單項檢查他們的 tag。還記得在第 2 部分中如何將 tag 設(shè)置為每個選項的分鐘數(shù)嗎?如果找到匹配,啟用該項目并退出循環(huán)。 - 設(shè)置滑塊的值并調(diào)用
showSliderValueAsText。 - showSliderValueAsText 為數(shù)字添加 “minute” 或 “minutes”,并在 text field中顯示。
現(xiàn)在,把這個添加到 viewDidLoad 中:
showExistingPrefs()
當視圖加載后,調(diào)用顯示偏好設(shè)置的方法。記住,使用 MVC 模式,Preferences 模型對象不知道如何或何時被顯示——這由 PrefsViewController 管理。
所以現(xiàn)在有顯示設(shè)置的時間的能力了,但改變彈出窗口中的時間并不做任何事情。我們需要一個保存新數(shù)據(jù)的方法,并告知有興趣的對象數(shù)據(jù)已更改。
在 EggTimer 對象中,使用.delegate 模式傳遞需要的數(shù)據(jù)。這一次(只是為了有點區(qū)別),你要在數(shù)據(jù)變化時廣播一個 Notification??梢赃x擇任何對象來接收此通知,并在收到通知時進行操作。
把下面的方法添加到 PrefsViewController 中:
func saveNewPrefs() {
prefs.selectedTime = customSlider.doubleValue * 60
NotificationCenter.default.post(name: Notification.Name(rawValue: "PrefsChanged"),
object: nil)
}
它會從自定義滑塊獲取數(shù)據(jù)(稍后以內(nèi)你會看到任何更改都反映在那里)。設(shè)置 selectedTime 屬性后將自動將新數(shù)據(jù)保存到 UserDefaults。然后,名為 “PrefsChanged” 的通知將發(fā)布到 NotificationCenter。
稍后,你會看到如何將 ViewController 設(shè)置為監(jiān)聽此通知并對其作出反應。
編寫 PrefsViewController 的最后一步是設(shè)置在第2部分中添加的 @IBActions 的代碼:
// 1
@IBAction func popupValueChanged(_ sender: NSPopUpButton) {
if sender.selectedItem?.title == "Custom" {
customSlider.isEnabled = true
return
}
let newTimerDuration = sender.selectedTag()
customSlider.integerValue = newTimerDuration
showSliderValueAsText()
customSlider.isEnabled = false
}
// 2
@IBAction func sliderValueChanged(_ sender: NSSlider) {
showSliderValueAsText()
}
// 3
@IBAction func cancelButtonClicked(_ sender: Any) {
view.window?.close()
}
// 4
@IBAction func okButtonClicked(_ sender: Any) {
saveNewPrefs()
view.window?.close()
}
- 從彈出窗口中選擇一個新項目時,檢查它是否是自定義菜單項。如果是,啟用滑塊并退出。如果沒有,使用 tag 獲取分鐘數(shù),使用它們來設(shè)置滑塊值和文本,并禁用滑塊。
- 滑塊變動時更新文字。
- 點擊 Cancel 會關(guān)閉窗口,且不保存改變。
- 點擊 OK 會先調(diào)用
saveNewPrefs然后關(guān)閉窗口。
現(xiàn)在構(gòu)建并運行 app,然后轉(zhuǎn)到 Preferences。嘗試在彈出窗口中選擇不同的選項——注意滑塊和文本如何更改以匹配。選擇 Custom 并選擇自己的時間。單擊確定,然后返回 Preferences 并確認仍然顯示你選擇的時間。
現(xiàn)在嘗試退出 app 并重新啟動。返回 Preferences,可以看到它已儲存你的設(shè)定。
實現(xiàn)已選擇的偏好設(shè)置
Preferences 窗口看起來不錯——按預期保存和還原了所選時間。但是當你回到主窗口,仍然顯示一個6分鐘的蛋! :[
因此,需要編輯 ViewController.swift 以使用存儲的值進行計時,并監(jiān)聽更改通知,以便可以更改或重置計時器。
將此擴展添加到 ViewController.swift,添加在任何現(xiàn)有類定義或擴展之外——它將所有 preferences 相關(guān)功能分組到一個單獨的包中以使代碼更加整潔:
extension ViewController {
// MARK: - Preferences
func setupPrefs() {
updateDisplay(for: prefs.selectedTime)
let notificationName = Notification.Name(rawValue: "PrefsChanged")
NotificationCenter.default.addObserver(forName: notificationName,
object: nil, queue: nil) {
(notification) in
self.updateFromPrefs()
}
}
func updateFromPrefs() {
self.eggTimer.duration = self.prefs.selectedTime
self.resetButtonClicked(self)
}
}
這會導致錯誤,因為 ViewController 沒有叫做 prefs 的對象。在 ViewController 類的主定義中,添加這行來定義 eggTimer 屬性:
var prefs = Preferences()
現(xiàn)在 PrefsViewController 有一個 prefs 對象,ViewController也有一個——這是一個錯誤嗎?不,有幾個原因。
-
Preferences是一個結(jié)構(gòu)體,因此它是基于值的,不是基于引用的。每個View Controller都有自己的副本。 -
Preferences結(jié)構(gòu)體通過單例與UserDefaults交互,因此兩個副本都使用相同的UserDefaults并獲取相同的數(shù)據(jù)。
在 ViewController viewDidLoad 函數(shù)的末尾,添加此調(diào)用用語設(shè)置Preferences 連接:
setupPrefs()
還最后一組編輯。之前是使用硬編碼值進行計時——360 秒或 6 分鐘?,F(xiàn)在ViewController 有權(quán)訪問 Preferences,要把這些硬編碼的 360 秒的更改為 prefs.selectedTime。
在 ViewController.swift 里搜索 360 然后把給一個都改成 prefs.selectedTime——應該能找到 3 個。
構(gòu)建并運行 app。如果你之前更改了偏好的煮雞蛋時間,剩余時間將顯示你選擇的那個時間。打開 Preferences,選擇另一個時間,然后單擊確定——你的新時間將立即顯示出來,因為 ViewController 接收了通知。
啟動計時器,然后打開 Preferences。倒計時在后面那個窗口繼續(xù)。更改雞蛋計時,然后單擊確定。定時器應用了新的時間,但停止并復位了計數(shù)器。其實這樣也可以,但如果 app 警告一下就會更好了。如何添加一個對話框,詢問這是否真的是你想做的嗎?
在ViewController 處理 Preferences 的 extension 中,添加此函數(shù):
func checkForResetAfterPrefsChange() {
if eggTimer.isStopped || eggTimer.isPaused {
// 1
updateFromPrefs()
} else {
// 2
let alert = NSAlert()
alert.messageText = "Reset timer with the new settings?"
alert.informativeText = "This will stop your current timer!"
alert.alertStyle = .warning
// 3
alert.addButton(withTitle: "Reset")
alert.addButton(withTitle: "Cancel")
// 4
let response = alert.runModal()
if response == NSAlertFirstButtonReturn {
self.updateFromPrefs()
}
}
}
上面發(fā)生了什么?
- 如果 timer 停止或暫停了,不用詢問直接弄。
- 創(chuàng)建一個
NSAlert,它是顯示對話框的類。配置其文本和樣式。 - 添加2個按鈕:Reset 和 Cancel。它們將按照從右到左的順序顯示,第一個將是默認選項。
- 將 alert 顯示為模態(tài)對話框,然后等待答復。檢查用戶是否點擊第一個按鈕(復位),如果是這樣,重置定時器。
在 setupPrefs 方法中,將 self.updateFromPrefs() 行更改為:
self.checkForResetAfterPrefsChange()
構(gòu)建并運行 app,啟動計時器,打開 Preferences,更改時間,然后單擊確定。你會看到對話框,詢問是否重置。
聲音
這個 app 目前為止唯一尚未涉及的就是聲音了。煮蛋器如果不能叮叮叮叮叮叮就不是煮蛋器了!
在第 2 部分中,已下載了 app 的資源文件夾。大多數(shù)是圖像,已經(jīng)用上了,但也有一個聲音文件:ding.mp3。如果你需要再次下載,這里是一個只有 聲音文件 的鏈接。
將 ding.mp3 文件拖動到 Project Navigator 中 EggTimer 組內(nèi)——就在 Main.storyboard 下面,這似乎是一個合乎邏輯的地方。確保勾選 Copy items if needed ,并選中了 EggTimer target。然后單擊完成。
要播放聲音,需要使用 AVFoundation 庫。當 EggTimer 告訴它的 delegate 計時器已經(jīng)完成時,ViewController 將播放聲音,所以打開 ViewController.swift。你會看到 Cocoa 庫在頂部被 import 了。
就在那行下面,添加這行:
import AVFoundation
ViewController 需要一個播放器來播放聲音文件,所以將它添加到屬性重:
var soundPlayer: AVAudioPlayer?
使 ViewController 有單獨的擴展來保存聲音相關(guān)的功能好像是個好主意,所以添加如下代碼添加到 ViewController.swift,在任何現(xiàn)有的定義或 extension 之外:
extension ViewController {
// MARK: - Sound
func prepareSound() {
guard let audioFileUrl = Bundle.main.url(forResource: "ding",
withExtension: "mp3") else {
return
}
do {
soundPlayer = try AVAudioPlayer(contentsOf: audioFileUrl)
soundPlayer?.prepareToPlay()
} catch {
print("Sound player not available: \(error)")
}
}
func playSound() {
soundPlayer?.play()
}
}
prepareSound 做了這里的大部分工作——它首先檢查 ding.mp3 文件是否在 app bundle 中。如果文件存在,它嘗試用聲音文件 URL 初始化 AVAudioPlayer 并準備播放。會預先緩沖聲音文件,以便在需要時立即播放。
playSound 只是發(fā)送一個播放消息給可能存在的播放器,但如果prepareSound 失敗了,soundPlayer 將是 nil 所以不會有任何事發(fā)生。
聲音只需要在點擊開始按鈕后準備就緒就可以了,因此在 startButtonClicked 末尾插入此行:
prepareSound()
并在 eggTimerProtocol 擴展中的 timerHasFinished 中添加:
playSound()
構(gòu)建和運行 app,為你的蛋選擇一個短一點的時間,啟動計時器。當定時器結(jié)束時,你聽到叮了嗎?
下一步?
你可以在這里下載 完整項目 。
本 macOS 系列開發(fā)教程為你介紹了基本的知識以開始開發(fā) macOS app,但還有很多要學習!
蘋果有一些特別棒的 文檔 ,涵蓋了 macOS 開發(fā)的所有方面。
我還強烈建議看看其他在 raywenderlich.com 的 macOS 教程。
如果您有任何問題或意見,請在下面評論!