macOS 新手开发:第 3 部分
![](https://ww1.sinaimg.cn/large/006tKfTcgy1fd08vanmc6j30dw0dwmxo.jpg)
欢迎回到 macOS 开发教程初学者系列 3 部分中的第 3 部分,也是最后一个部分!
在第 1 部分中,学习了如何安装 Xcode 以及创建简单的 app。在 第 2 部分 中,为更复杂的 app 创建了用户界面,但还不能正常工作,因为没有写任何代码。在这部分,会添加 Swift 代码,以使 app 正常工作!
开始
如果尚未完成第 2 部分或希望用干净的模板开始,可 下载项目文件,带有布局好的 UI,就和第 2 部分结尾的时候一样。打开此项目或你自己在第 2 部分里的项目,运行一下确定 UI 已全部就位。同样也把 Preferences 打开检查一下。
![](https://ww4.sinaimg.cn/large/006tKfTcgy1fd1kg9w5gcj30jg0gzdhb.jpg)
沙盒
在你深入代码之前,花一点时间学习沙盒(sandboxing)。如果你是一个 iOS 程序员,你已经熟悉这个概念——否则就请继续阅读。
沙盒 app 有自己的空间,可以使用单独的文件存储区域,无法访问其他 app 创建的文件,具有有限的访问权限。对于 iOS app,这是唯一的选择。对于 macOS app,这是可选的;但是,如果要通过 Mac App Store 分发 app,则必须将其沙盒化。一般情况下,都应将 app 沙盒化,因为这使 app 减少潜在问题。
要为 Egg Timer app 启用沙盒,请在 Project Navigator 中选择项目——顶部带有蓝色图标的那个。在 Targets 中选择 EggTimer(只列出了一个 target),然后单击顶部选项卡中的 Capabilities。单击开关以启用 App Sandbox。屏幕会展开,以显示现在 app 可以请求的各种权限。这个 app 什么都不需要,所以不要勾选它们。
![](https://ww2.sinaimg.cn/large/006tKfTcgy1fd1kn0xsogj30jg0bvq3z.jpg)
组织文件
看看 Project Navigator。列出了所有文件,但毫无纪律。这个 app 不会有很多文件,但把类似的文件分组在一起是好的做法,可以更有效的导航,特别是对于较大的项目来说。
![](https://ww3.sinaimg.cn/large/006tKfTcgy1fd1kosr610j308c08st95.jpg)
选择两个视图控制器文件,方法是单击一个,然后按住 Shift 键单击下一个。右键单击并从弹出菜单中选择 New Group from Selection。将新组命名为 View Controllers。
该项目马上会有一些模型文件,因此选择顶部 EggTimer 组,右键单击并选择 New Group。取名为 Model。
最后,选择 Info.plist 和 EggTimer.entitlements,并将它们放入名为 Supporting Files 的组。
拖动组和文件,直到 Project Navigator 看起来像这样:
![](https://ww4.sinaimg.cn/large/006tKfTcgy1fd1krkuggpj30df0f1q40.jpg)
MVC
这个 app 使用 MVC 模式:Model View Controller。
app 的主要模型将是一个名为 EggTimer
的类。这个类将具有定时器的开始时间、所请求的持续时间和已经过去的时间等属性。它还会有一个 Timer
对象,每秒触发、自我更新。EggTimer
对象还会有 start,stop,resume 和 reset 方法。
EggTimer
模型类保存数据并执行操作,但不了解如何显示它们。 Controller(在这种情况下是 ViewController
)了解 EggTimer
类(Model),并且有一个 View
可以用来显示数据。
为了与 ViewController
通信,EggTimer
使用委托协议。当某事发生变化时,EggTimer
向其 delegate
发送一条消息。ViewController
将自身分配为 EggTimer
的 delegate
,所以由它来接收消息,然后它可以在自己的 View 中显示新的数据。
编写 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
}
这样就设置了 EggTimer
类及其属性。 TimeInterval
实际上是 Double
,意思为秒数。
接下来要在类中添加两个计算属性,就在前面那些属性之后:
var isStopped: Bool {
return timer == nil && elapsedTime == 0
}
var isPaused: Bool {
return timer == nil && elapsedTime > 0
}
这是用于快速确定 EggTimer
状态的方式。
将 delegate 协议的定义插入 EggTimer.swift 文件,但在 EggTimer
类的外面——我喜欢将协议定义放在文件的顶部,import 的后面。
protocol EggTimerProtocol {
func timeRemainingOnTimer(_ timer: EggTimer, timeRemaining: TimeInterval)
func timerHasFinished(_ timer: EggTimer)
}
协议规定了一个契约,任何符合 EggTimerProtocol
的对象必须提供这两个函数。
现在你已经定义了一个协议,EggTimer
需要一个可选的 delegate 属性,该属性设置为符合此协议的任何对象。EggTimer
不知道或不关心 delegate 是什么类型的对象,因为它只要确定 delegate 有这两个函数就行了。
将此行添加到 EggTimer
类中的现有属性中:
var delegate: EggTimerProtocol?
启动 EggTimer
的 timer 对象将每秒触发一次函数调用。插入此代码,定义了将由定时器调用的函数。必须要有关键字 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)
}
}
会发生什么?
-
startTime
是一个Optional Date
——如果是nil
,timer 就无法运行,所以什么都不会发生。 - 重新计算
elapsedTime
属性。startTime
早于当前,因此timeIntervalSinceNow
会生成负数。用减号使得 elapsedTime 是正数。 - 计算 timer 的剩余秒数,四舍五入以给出整数秒。
- 如果 timer 已经完成,重置它并告诉 delegate 它已经完成。否则,告诉 delegate 剩余的秒数。由于
delegate
是可选属性,? 号用于执行可选链。如果 delegate 没有设置,这些方法将不会被调用,也就不会出现意外情况了。
添加 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()
}
这些函数做了什么?
-
startTimer
使用Date()
将启动时间设置为现在、设置了重复的Timer
。 -
resumeTimer
是 timer 已暂停并正在重新启动时调用的内容。基于已过去的时间重新计算开始时间。 -
stopTimer
停止了重复的 timer。 -
resetTimer
停止了重复的 timer 并将属性恢复为默认值。
这些函数还全部调用了 timerAction
,以便屏幕可以立即刷新。
ViewController
现在 EggTimer
对象已经正常工作了,现在回到 ViewController.swift 让屏幕改变以反映这一点。
ViewController
已经有 @IBOutlet
属性了,现在给它一个 EggTimer
属性:
var eggTimer = EggTimer()
将下面这行驾到 viewDidLoad
中,替换掉注视行:
eggTimer.delegate = self
这将导致一个错误,因为 ViewController
不符合 EggTimerProtocol
。当符合协议时,为协议创建单独的扩展,会使代码更干净。在 ViewController
类定义下面添加这段代码:
extension ViewController: EggTimerProtocol {
func timeRemainingOnTimer(_ timer: EggTimer, timeRemaining: TimeInterval) {
updateDisplay(for: timeRemaining)
}
func timerHasFinished(_ timer: EggTimer) {
updateDisplay(for: 0)
}
}
错误消失了,因为 ViewController
现在有 EggTimerProtocol
所需的两个函数。但是这两个函数都调用了还不存在的 updateDisplay
。
这是 ViewController
的另一个扩展,包含了用于显示的函数:
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
使用私有函数获取剩余时间的文本和图像,并在 text field 和 image view 中显示它们。
textToDisplay
将剩余秒数转换为 M:SS 格式。 imageToDisplay
计算煮蛋程度的百分比,并选择匹配的图像。
所以 ViewController
有了一个 EggTimer
对象,它也有从 EggTimer
接收数据并显示结果的函数,但按钮还没有编码。在第 2 部分中,已经为按钮设置了 @IBActions
。
这里是这些 action 函数的代码,把它们替换掉:
@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 调用之前添加的 EggTimer
方法。
现在构建并运行 app,然后单击.Start 按钮。
还少几个功能:Stop 和 Reset 按钮总是在禁用状态,以及只能煮一个 6 分钟的蛋。可以使用 Timer 菜单来控制 app; 尝试使用菜单和键盘快捷键来停止,启动和重置。
如果足够有耐心,你会看到煮的时候鸡蛋变了颜色,最后在煮好时显示了 “DONE!”。
![](https://ww3.sinaimg.cn/large/006tKfTcgy1fd1lynyeonj30b40fuaaj.jpg)
根据 timer 状态,按钮应该启用或禁用,并且 Timer 菜单项应该与之匹配。
将这个函数添加到 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)
}
}
此函数使用 EggTimer
状态(还记得添加到 EggTimer
的那几个计算变量吗)来确定应启用哪些按钮。
在第 2 部分中,你把 Timer 菜单项设置为 AppDelegate
的属性,因此AppDelegate
是配置它们的地方。
切换到 AppDelegate.swift 添加如下函数:
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
的状态时,就需要改变按钮和菜单。切换回 ViewController.swift 并将此行添加到 3 个按钮 action 函数中每一个的末尾:
configureButtonsAndMenus()
再次构建并运行 app,可以看到按钮按预期启用和禁用。检查一下菜单项;它们应该会反映按钮的状态。
![](https://ww4.sinaimg.cn/large/006tKfTcgy1fd1maqt4koj30go0ovq47.jpg)
偏好设置
这个 app 还有一个大问题——如果你不想把鸡蛋煮 6 分钟怎么办?
在第 2 部分中,我们设计了 Preferences 窗口以允许选择不同的时间。此窗口由 PrefsViewController
控制,但它需要一个模型对象来处理数据存储以及检索。
将使用 UserDefaults
存储 Preferences,UserDefaults
是在 app 容器中用键值对存储小数据到 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
的数据存储将被自动处理。
现在切换到 PrefsViewController.swift,第一件事是更新显示以反映现有偏好设置或默认值。
首先,在 outlets 下面添加此属性:
var prefs = Preferences()
在这里,你创建了一个 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
,并将其从秒数转换为整数分钟。 - 如果找不到匹配的预设值,请将默认值设置为 “Custom”。
- 遍历循环
presetsPopup
中的菜单项检查他们的 tag。还记得在第 2 部分中如何将 tag 设置为每个选项的分钟数吗?如果找到匹配,启用该项目并退出循环。 - 设置滑块的值并调用
showSliderValueAsText
。 - showSliderValueAsText 为数字添加 “minute” 或 “minutes”,并在 text field中显示。
现在,把这个添加到 viewDidLoad
中:
showExistingPrefs()
当视图加载后,调用显示偏好设置的方法。记住,使用 MVC 模式,Preferences
模型对象不知道如何或何时被显示——这由 PrefsViewController
管理。
所以现在有显示设置的时间的能力了,但改变弹出窗口中的时间并不做任何事情。我们需要一个保存新数据的方法,并告知有兴趣的对象数据已更改。
在 EggTimer
对象中,使用.delegate 模式传递需要的数据。这一次(只是为了有点区别),你要在数据变化时广播一个 Notification
。可以选择任何对象来接收此通知,并在收到通知时进行操作。
把下面的方法添加到 PrefsViewController
中:
func saveNewPrefs() {
prefs.selectedTime = customSlider.doubleValue * 60
NotificationCenter.default.post(name: Notification.Name(rawValue: "PrefsChanged"),
object: nil)
}
它会从自定义滑块获取数据(稍后以内你会看到任何更改都反映在那里)。设置 selectedTime
属性后将自动将新数据保存到 UserDefaults
。然后,名为 “PrefsChanged” 的通知将发布到 NotificationCenter
。
稍后,你会看到如何将 ViewController
设置为监听此通知并对其作出反应。
编写 PrefsViewController
的最后一步是设置在第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 获取分钟数,使用它们来设置滑块值和文本,并禁用滑块。
- 滑块变动时更新文字。
- 点击 Cancel 会关闭窗口,且不保存改变。
- 点击 OK 会先调用
saveNewPrefs
然后关闭窗口。
现在构建并运行 app,然后转到 Preferences。尝试在弹出窗口中选择不同的选项——注意滑块和文本如何更改以匹配。选择 Custom 并选择自己的时间。单击确定,然后返回 Preferences 并确认仍然显示你选择的时间。
现在尝试退出 app 并重新启动。返回 Preferences,可以看到它已储存你的设定。
![](https://ww1.sinaimg.cn/large/006tKfTcgy1fd1n4n77usj30go09jgm8.jpg)
实现已选择的偏好设置
Preferences 窗口看起来不错——按预期保存和还原了所选时间。但是当你回到主窗口,仍然显示一个6分钟的蛋! :[
因此,需要编辑 ViewController.swift 以使用存储的值进行计时,并监听更改通知,以便可以更改或重置计时器。
将此扩展添加到 ViewController.swift,添加在任何现有类定义或扩展之外——它将所有 preferences 相关功能分组到一个单独的包中以使代码更加整洁:
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()
现在 PrefsViewController
有一个 prefs
对象,ViewController
也有一个——这是一个错误吗?不,有几个原因。
-
Preferences
是一个结构体,因此它是基于值的,不是基于引用的。每个View Controller
都有自己的副本。 -
Preferences
结构体通过单例与UserDefaults
交互,因此两个副本都使用相同的UserDefaults
并获取相同的数据。
在 ViewController viewDidLoad
函数的末尾,添加此调用用语设置Preferences
连接:
setupPrefs()
还最后一组编辑。之前是使用硬编码值进行计时——360 秒或 6 分钟。现在ViewController
有权访问 Preferences
,要把这些硬编码的 360 秒的更改为 prefs.selectedTime
。
在 ViewController.swift 里搜索 360 然后把给一个都改成 prefs.selectedTime
——应该能找到 3 个。
构建并运行 app。如果你之前更改了偏好的煮鸡蛋时间,剩余时间将显示你选择的那个时间。打开 Preferences,选择另一个时间,然后单击确定——你的新时间将立即显示出来,因为 ViewController
接收了通知。
![](https://ww2.sinaimg.cn/large/006tKfTcgy1fd1nkyzeo0j30jg0ec0u2.jpg)
启动计时器,然后打开 Preferences。倒计时在后面那个窗口继续。更改鸡蛋计时,然后单击确定。定时器应用了新的时间,但停止并复位了计数器。其实这样也可以,但如果 app 警告一下就会更好了。如何添加一个对话框,询问这是否真的是你想做的吗?
在ViewController 处理 Preferences 的 extension 中,添加此函数:
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()
}
}
}
上面发生了什么?
- 如果 timer 停止或暂停了,不用询问直接弄。
- 创建一个
NSAlert
,它是显示对话框的类。配置其文本和样式。 - 添加2个按钮:Reset 和 Cancel。它们将按照从右到左的顺序显示,第一个将是默认选项。
- 将 alert 显示为模态对话框,然后等待答复。检查用户是否点击第一个按钮(复位),如果是这样,重置定时器。
在 setupPrefs
方法中,将 self.updateFromPrefs()
行更改为:
self.checkForResetAfterPrefsChange()
构建并运行 app,启动计时器,打开 Preferences,更改时间,然后单击确定。你会看到对话框,询问是否重置。
声音
这个 app 目前为止唯一尚未涉及的就是声音了。煮蛋器如果不能叮叮叮叮叮叮就不是煮蛋器了!
在第 2 部分中,已下载了 app 的资源文件夹。大多数是图像,已经用上了,但也有一个声音文件:ding.mp3。如果你需要再次下载,这里是一个只有 声音文件 的链接。
将 ding.mp3 文件拖动到 Project Navigator 中 EggTimer 组内——就在 Main.storyboard 下面,这似乎是一个合乎逻辑的地方。确保勾选 Copy items if needed ,并选中了 EggTimer target。然后单击完成。
![](https://ww1.sinaimg.cn/large/006tKfTcgy1fd1ny4imf2j30go09tt94.jpg)
要播放声音,需要使用 AVFoundation
库。当 EggTimer
告诉它的 delegate 计时器已经完成时,ViewController
将播放声音,所以打开 ViewController.swift。你会看到 Cocoa
库在顶部被 import 了。
就在那行下面,添加这行:
import AVFoundation
ViewController 需要一个播放器来播放声音文件,所以将它添加到属性重:
var soundPlayer: AVAudioPlayer?
使 ViewController
有单独的扩展来保存声音相关的功能好像是个好主意,所以添加如下代码添加到 ViewController.swift
,在任何现有的定义或 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
只是发送一个播放消息给可能存在的播放器,但如果prepareSound
失败了,soundPlayer
将是 nil 所以不会有任何事发生。
声音只需要在点击开始按钮后准备就绪就可以了,因此在 startButtonClicked
末尾插入此行:
prepareSound()
并在 eggTimerProtocol 扩展中的 timerHasFinished 中添加:
playSound()
构建和运行 app,为你的蛋选择一个短一点的时间,启动计时器。当定时器结束时,你听到叮了吗?
![](https://ww1.sinaimg.cn/large/006tKfTcgy1fd1o82xutfj30b40fu74t.jpg)
下一步?
你可以在这里下载 完整项目 。
本 macOS 系列开发教程为你介绍了基本的知识以开始开发 macOS app,但还有很多要学习!
苹果有一些特别棒的 文档 ,涵盖了 macOS 开发的所有方面。
我还强烈建议看看其他在 raywenderlich.com 的 macOS 教程。
如果您有任何问题或意见,请在下面评论!