macOS开发入门 Part 3

原文:macOS Development for Beginners: Part 3
作者:Sarah Reichelt
译者:kmyhy

欢迎回到 macOS 开发入门教程三部曲的最后一部,也就是第三部!

在第一部分,你学习了如何安装Xcode,如何创建一个简单的 App。在第二部分,你为一个有点小复杂的 App 创建了 UI,但它仍然不能工作,因为你还没编写任何代码。在这一部分,你将编写一些 Swift 代码,让你的 App 焕发生机!

开始

如果你没有完成第二部分或者想有一个干净的开始,你可以从这里下载第二部分已完成的项目,它的 UI 已经布局好了。打开这个项目或你在第二部分中完成的项目,运行项目确认 UI 部分显示正常。打开 Preferences 窗口查看是否正常。

沙盒

在开始编写代码之前,我们花点时间讨论一下沙盒。如果你是一个 iOS 开发者,你可能知道这个概念——否则的话,请继续。

一个沙盒 App 拥有自己工作空间,它会分成几个独立的文件存储区域,并且无法访问其他 Ap 所创建的文件,同时访问和权限会有一定的限制。对于 iOS App,除了沙盒我们别无他法。对于 macOS App,情况稍好一点;但是如果你想将App 发布到 App 商店,那么也只能访问沙盒。作为一个通用原则,你应该让你的 App 成为“沙盒的”,因为这会让你的 App 出现问题的几率更小。

要打开 Egg Timer App 的沙盒选项,从项目导航器中选中项目——也就是最上面的蓝色图标。在 Targets 列表中(其实列表中也只有那么一个 target)选择 EggTimer,然后点击顶部标签栏的 Capabilities。将 App Sandbox 的开关打开。这会展开一个列表显示你能够为 App 声明的各种权限。本 App 不需要这些权限,因此全部都不用选。

组织你的文件

回到项目导航器。所有的文件都是没有任何层次列在一起。这个 App 的文件还不算多,但将它们分门别类地组织起来有助于查找它们,对于大项目来说尤其如此。

选中两个 view controller 文件,你可以先选中一个,在按住shift 键选中另一个。右键,选择 New Group from Selection。group命名为 View Controllers。项目中还有一些模型文件,因此选择最顶层的 EggTimer 目录,右键,选择 New Group,命名为 Model。

最后,选中 Info.plist 和 EggTimer.entitlements,将它们拖进 Supporting Files 文件夹。

将你的文件夹和文件组织为如下图所示:

MVC

这个 App 使用了 MVC 模式:模型视图控制器。

App 中的模型对象主要是一个叫做 EggTimer 的类。这个类拥有几个属性:定时器的开始时间,时长和已经用掉的时间。还有一个 Timer 对象,用于每秒触发一次更新。方法包括了 EggTimer 对象的启动、停止、恢复 和 重置。

EggTimer 模型类负责保存数据和执行动作,但它不知道如何显示。Controller(在这里就是 ViewController),知道 EggTimer 类(模型),也知道用于显示这些数据的 View。

要和 ViewController 进行通讯,EggTimer 使用了一个委托协议。当它发生改变时,EggTimer 会向委托对象发送消息。ViewController 将自己设置为 EggTimer 的委托,因此它会接收到这个消息,然后将新的数据更新到它的视图。

实现 EggTimer

在项目导航器中选中 Model 文件夹,然后选择 File/New/File… Select 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 的状态。

在 EggTimer.swift 中,但是在 EggTimer 类之外添加一个委托协议——我喜欢将协议定义在文件的顶部,就在 import 语句后面。

protocol EggTimerProtocol {
  func timeRemainingOnTimer(_ timer: EggTimer,timeRemaining: TimeInterval)
  func timerHasFinished(_ timer: EggTimer)
}

协议声明了一种义务,任何声明要采用 EggTimerProtocol 的对象必须实现这两个方法。

现在你已经定义了一个协议,EggTimer 可以指定一个可空的 delegate 属性,用于保存一个实现该协议的对象。EggTimer 不知道也不关心对象的类型是什么,因为只需要知道 delegate 有这两个方法即可。

在已有属性后面添加:

var delegate: EggTimerProtocol?

EggTimer 的 timer 对象启动之后会以一秒钟一次的频率调用一个方法。添加这个方法用于给 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)
    }
  }

这是什么意思?

  1. startTime 是一个可空类型的 Date – 当它为空时,定时器不应该运行,因此什么也不做。
  2. 重新计算 elapsedTime 属性。startTime 是比当前时间还要早的时间,因此 timeIntervalSinceNow 会产生一个负值。因此将一个负号放在前面,以便 elapsedTime 会是一个正数。
  3. 计算定时器还剩下的时间,rounded() 方法会对秒数取整。
  4. 如果定时器结束,重置它并通知委托对象它已经运行结束。因为 delegate 是可空类型,所以要用 ?进行解包。如果 delegate 未设置,这些方法不会调用,因此不会产生任何错误。

你会看到一个错误,因为我们还没有编写最后的一点代码: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,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()
  }

这几个方法是什么意思?

  1. startTimer 方法将开始时间记录为当前时间 Date(),然后创建一个循环定时器。
  2. resumeTimer 方法会在定时器被暂停,然后被重新启动的时候调用。这个开始时间会根据已经用去的时间来重新进行计算。
  3. stopTimer 停止定时器。
  4. resetTimert 停止定时器并将属性设置为默认值。

所有方法都会调用 timerAction,以便界面会立即刷新。

ViewController

EggTimer 已经准备好了,让我们转到 ViewController.swift,让 UI 的显示和它保持一致。

ViewController 已经创建了 @IBOutlet 属性,但现在需要添加一个属性:

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 方法调用了私有方法根据剩余时间来获取要显示的文本和图片,然后将之分别显示在文本框和 image view 中。

textToDisplay 方法将剩余时间转换成 M:SS 格式。imageToDisplay 方法计算煮鸡蛋的时间用去了整个时间的百分之几,然后返回对应的图片。

ViewController 已经有了一个 EggTimer 对象,它也能够从EggTimer 获得数据并显示结果,但按钮代码还没有写。在第二部分,我们其实已经为按钮创建了 @IBAction。

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!)。

按钮和菜单

按钮应该根据定时器的状态来改变可用/不可用状态,Timer 菜单中的菜单项也是一样的。

在 ViewController 中添加下列函数,就放在显示方法的同一个扩展中:

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 中添加的计算属性)来设置按钮的可用状态。

在第二部中,你创建了 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 在三个按钮的 Action 方法的最后加入这句:

configureButtonsAndMenus()

运行 App,你可以看到按钮的可用/不可用状态终于正常了。查看菜单项,它们和按钮的状态保持一致。

偏好设置

这个 App 还剩一个很大的问题——如果你不想让鸡蛋只能煮 6 分钟呢?

在第二部中,你设计了一个偏好设置窗口,允许选择不同的时间。这个窗口由 PrefsViewController 负责,但它还需要一个模型对象负责数据的存储和查找。

偏好设置将使用 UserDefaults 存储,它一种在 App 容器的 Preferences 目录中以键值形式存储小数据的方式。

在项目导航器中,右键 Model 文件夹,选择 New File… Select 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")
    }
  }

}

这段代码是什么意思?

  1. selectedTime 是一个 TimeInterval 类型的计算属性。
  2. 当读取这个属性值时,我们通过 UserDefaults 单例对象访问一个键名为 selectedTime 的 Double 值。如果这个值还没定义,UserDefautls 会返回 0,如果这个值大于 0,则返回 UserDefaults 中的值。
  3. 如果 selectedTime 值未定义,使用 360(6分钟)作为默认值。
  4. 当 selectedTime 被修改时,将新值保存到 UserDefaults 的 selectedTime 键中。

通过一个计算属性的 getter 方法和 setter 方法,UserDefaults 数据存储机制会自动替我们处理。

回到 PrefsViewController.swift,首先需要让界面显示出 defaults 中存储的现有值。

首先,在 outlet 下面添加一个属性:

var prefs = Preferences()

这里创建了一个 Preference 实例以便访问 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)"
}

代码有点多,分成几个部分解释:

  1. 获取 prefs 的 selectedTime 对象并转换成分钟。
  2. 默认设置为 Custom,表示没有找到预设值。
  3. 遍历 presetsPopup 的选项列表,并检查它们的 tag 属性。还记得你在第二部中将每个选项的 tag 设置为它们的分钟数吗?如果找到匹配的情况,勾选这个选项,退出循环。
  4. 设置 slider 的值,调用 showSliderValueAsText。
  5. showSliderValueAsText 将 “minute” 或者 “minutes” 添加到数字后面,然后显示在 text field 中。

现在,在 viewDidLoad 中加入:

showExistingPrefs()

当视图加载,调用这个方法,显示偏好设置。注意,根据 MVC 模型,Preferences 模型不应该知道如何以及如何显示它——那是 PrefsViewController 的事情。

现在你已经能够显示设置的时间了,但改变时间的时候这个 popup 按钮什么也不会做。你需要实现一个方法,保存新数据,通知对此感兴趣的人数据已经改变。

在 EggTimer 对象中,你用委托模型传递数据给需要的对象。这次(有点不同),你在数据被修改时广播一个通知。任何监听了这个通知的对象会收到通知并进行处理。

在 PrefsViewController 中新增方法:

func saveNewPrefs() {
    prefs.selectedTime = customSlider.doubleValue * 60
    NotificationCenter.default.post(name: Notification.Name(rawValue: "PrefsChanged"),object: nil)
  }

这里从 slider 获取数据(任何改变都会在这里体现)。对 selectedTime 属性进行赋值会导致新数据自动保存到 UserDefaults。然后通过通知中心发送 PrefsChanged 通知。

接下来,你会看到 ViewController 如何监听这个通知并进行处理。

最后一步是你在第二部中在 PrefsViewController 中添加的 @IBAction 中进行的:

// 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()
  }
  1. 当 popup 按钮中有新的选项被选中时,判断它是否是 Custom 选项。如果是,使 slider 可用并退出。如果不是,用 tag 属性去获取分钟数,然后设置 slider 的值和文本,并禁用 slider。
  2. 当 slider 值发生改变,修改文字显示。
  3. 点击取消按钮,关闭窗口,不保存改变。
  4. 点击 OK 按钮,调用 saveNewPrefs 然后关闭窗口。

运行 App,进入偏好设置。在 popup 按钮中选择不同的选项——注意 slider 和 text field 会做相应变化。选择 Custom,然后挑选一个时间。点击 OK,回到偏好设置窗口,确认你的选定的时间是否依然显示。

现在退出 App 然后重新打开,进入偏好设置窗口,查看已保存的设置。

实现所选的设置

偏好设置窗口看起来不错——它能够保存和恢复你选定的时间。但回到主窗口,你仍然只能煮 6 分钟的鸡蛋!

因此你需要修改 ViewController.swift,将保存好的值应用到计时中去,监听定时器的时间变化通知和重置通知。

为 ViewController 新增一个扩展——这个扩展会将所有的设置功能干净地包装到一起:

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 分钟。现在,ViewCotnroller 访问 Preference,你可以将硬编码的 360 秒修改为 prefs.selectedTime。

在 ViewController.swift 中搜索 360,将每一处都修改为 prefs.selectedTime – 你可能找到 3 个 360。

运行 App。如果你先前已经改变过煮蛋的时间,不管你怎么选择剩余时间都会显示。进入偏好设置窗口,选择不同的时间点击 OK——你的新时间立即在 ViewController 收到通知后显示。

开始计时,回到 Preferences 窗口。倒计时在后面的窗口中进行。改变煮蛋时间点击 OK。定时器会使用你的新时间,但停止计时并充值计数。这也是可以的,但如果让 App 稍微提示一下用户会更好。添加一个对话框,提示用户接下来会发生什么吧!

在和偏好设置有关的 ViewController 扩展中,添加函数:

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()
      }
    }
  }

什么意思?

  1. 如果定时器处于暂停或停止状态,那么直接重置定时器,不需要询问。
  2. 创建一个 NSAlert,这个类用于显示对话框。设置 alert 的文字和风格。
  3. 添加两个按钮:Reset 和 Cancel。它们按照从右到左顺序排列,第一个按钮是默认按钮。
  4. 以模式对话框方式显示 alert,等待用户操作。如果用户点击第一个按钮(Reset),重置定时器。

在 setupPrefs 方法中,将这句 self.updateFromPrefs() 修改为:

self.checkForResetAfterPrefsChange()

运行 App,开启定时器,进入 Preferences 窗口,修改时间,点击 OK。你会看到对话框弹出,你可以在这时选择重置或者取消。

声音

这个 App 中还有一个部分没有实现,那就是声效。一个不会”叮”的煮蛋器不是真正的煮蛋器。

在第二部中,你下载了 App 的 assets 文件夹。其中包含了你用到的图片,还包含了一个声音文件:ding.mp3。如果你需要再次下载这个声音文件,下载链接在这里

将 ding.mp3 文件拖入项目导航器的 EggTimer 文件夹——一个理想的地方是放在 Main.storyboard 后面。确保选中 Copy items if needed is checked 同时勾选 EggTimer target。然后点击 Finish。

要播放声音,你必须使用 AVFoundation 框架。ViewController 会在收到 EggTimer 的定时器时间结束通知时播放声音,因此打开 ViewController.swift 文件。在文件顶部,找到导入 Cocoa 框架的地方。

在这行下面,添加:

import AVFoundation

ViewController 需要用播放器来播放声音,因此添加一个属性:

var soundPlayer: AVAudioPlayer?

为 ViewController 增加一个新扩展用于放声音相关逻辑是个不错的注意,因此在已有的扩展之后重新定义一个 ViewController 扩展。

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 方法负责完成大部分工作——首先检查 bundle 中是否存在 ding.mp3 文件。如果文件存在,使用这个文件的 URL 初始化一个 AVAudioPlayer,然后准备播放。这会提前缓存声音文件以便在某个时候立即开始播放。

如果 soundPlayer 对象不为空,playSound 方法向播放器发送 play 消息,但如果 prepareSound 失败,soudPlayer 会初始化为 nil,因此什么也不做。

当 Start 按钮被点击时,只需要准备一次声频文件,因此在 startButtonClicked 方法最后加入:

prepareSound()

然后在 EggTimerProtocol 协议扩展的 timerHasFinished 方法中加入:

playSound()

运行 App,选择一个煮蛋时间,然后开始计时。当计时结束,你应当听到一声”叮“。

结束

完整项目代码在此处下载

这个 macOS 开发系列入门教程让你具备 macOS App 开发的基本知识——但你要学的东西仍然还有许多。

苹果文档中有大量优秀的文档,介绍了 macOS 开发中的方方面面。

我隆重推荐 raywenderlich.com 的其它 macOS 教程。

有任何问题和建议,请在下面留言!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。