[译] 零基础 macOS 应用开发(三)
欢迎回到我们的零基础 macOS 应用开发教程的最后一部分(共三部分)! 在第一部分中,你已经学会了如何安装 Xcode 和如何创建一个示例 app;在第二部分中你为一个更加复杂的 app 创建了 UI,但因为你还没有编写任何代码,所以它还不能工作。在这个部分中,你将会编写所有 Swift 代码并让你的 app 真正活起来! 开始如果你还没有完成第二部分,或你希望从一个更加纯净的情况继续学习,你可以下载第二部分中已经完成了 UI 布局的工程文件。打开你下载的或你跟着第二部分完成的工程文件,并运行一下它,确认一下是否所有的 UI 都能正确显示,打开偏好设置窗口看看它是否能正常显示。
沙盒机制在你开始编写代码之前,请花一些时间来了解一下 macOS 的沙盒机制。如果你是一个 iOS 开发者,你已经了解了这个概念,如果你不曾了解过,继续往下阅读。 一个沙盒化了的 app 拥有自己独立的存储空间,沙盒会禁止你的 app 访问另一个 app 创建的文件以及其他的许可和限制。对于 iOS app,使用沙盒是必须的,而对于 macOS app,这只是一个可选项;但如果你希望通过 Mac App Store 进行分发和销售,你的 app 必须沙盒化,由于沙盒带来的诸多限制,你的 app 可能会出现一些问题。 要为你的 app 启用沙盒,在 Project Navigator(项目导航器)中选择项目文件,也就是文件列表里最顶上的蓝色图标。在 Targets 列表中选择 EggTimer(其实 Targets 列表里也只有一个项目可以选择),然后在上方的标签中点击 Capabilities(功能)标签,点击 App Sandbox(应用沙盒)那一栏的开关,这个视图将会展开并显示你的 app 可以申请的许多权限。这个例子中的 app 不需要任何特殊的权限,因此它们都不需要打开。
管理你的文件看一眼你的 Project Navigator(项目导航器),所有的文件都堆在一起,缺乏组织,这个 app 不会有很多文件,但把文件整理的井井有条始终都会是个好习惯,也能帮助我们更快速地定位到你需要的文件,这一点对于大型项目尤其有用。
按住 Shift 的同时分别点击两个 View Controller 文件,把他们同时选中,右键点击并选择 New Group from selection(用所选项目创建新的分组),给新建的分组起名为 View Controllers。 这个项目将会包含一些 Model 文件,所以右键点击 EggTimer 分组,选择 New Group(新建分组),把这个分组命名为Model**。 最后,选中 Info.plist 和 EggTimer.entitlements,把它们扔掉一个叫 Supporting Files 的文件夹里。 拖动分组和文件调整他们的顺序,直到你的项目看起来像这样:
MVC这个 app 将会应用 MVC 模式:Model View Controller(模型 - 视图 - 控制器)。
我们要给 app 创建的第一个 Model 对象名叫
为了能和 编写 EggTimer 类在项目导航器中选中 Model 分组,并点击 Xcode 菜单栏上的 File → New → File…,选择 macOS → Swift File,并点击 Next,给这个文件起名为 EggTimer.swift 并点击 Create 来创建它。 在这个文件中加入以下代码: class EggTimer { var timer: Timer? = nil var startTime: Date? var duration: TimeInterval = 360 // 默认的计时时间是 6 分钟 var elapsedTime: TimeInterval = 0 } 这样 第二件事是在类中添加两个计算属性(Computed Properties),这两个属性是用来决定 var isStopped: Bool { return timer == nil && elapsedTime == 0 } var isPaused: Bool { return timer == nil && elapsedTime > 0 } 在 EggTimer.swift 文件 protocol EggTimerProtocol { func timeRemainingOnTimer(_ timer: EggTimer,timeRemaining: TimeInterval) func timerHasFinished(_ timer: EggTimer) } 你可以理解为:这个协议制定了一份合同,任何宣布遵守 现在你定义了一个协议, 将这些代码属性添加到 var delegate: EggTimerProtocol? 让 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) } } …所以这些代码到底是在做些什么?
你会看到 Xcode 提示我们出现了一些错误,不过当我们完成了 // 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() { // 停止计时器 & 重设所有属性 timer?.invalidate() timer = nil startTime = nil duration = 360 elapsedTime = 0 timerAction() } 这些代码是做什么的?
以上的这些方法都会调用 ViewController现在
var eggTimer = EggTimer() 将 eggTimer.delegate = self 写完上面的代码以后会出现一个错误,因为 extension ViewController: EggTimerProtocol { func timeRemainingOnTimer(_ timer: EggTimer,timeRemaining: TimeInterval) { updateDisplay(for: timeRemaining) } func timerHasFinished(_ timer: EggTimer) { updateDisplay(for: 0) } } 因此我们还需要为 extension ViewController { // MARK: - 显示 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) } }
所以 这里是这些 IBAction 的方法,你可以用它们来替代之前的 IBAction。 @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) } 这里的三个 IBAction 将会调用你之前添加的 现在编译并运行你的 app,并点击 Start 按钮。你还可以用 Timer 菜单来控制这个 app,试着去用键盘快捷键来操作你的 app。 现在我们还需要完善一些功能:Stop 和 Reset 按钮始终是被禁用的,而且你只可以定 6 分钟的时。 如果你有足够的耐心,你将会看到鸡蛋的颜色随着时间渐渐改变,并在完成时显示一个「DONE!」。
按钮和菜单界面上的按钮以及菜单里的菜单项应该随着 timer 的状态自动启用或禁用。 把这个方法添加到 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) } } 这个方法使用 在第二部分中,你创立了一个 Timer menu item 作为 切换到 AppDelegate.swift,在其中添加这个方法: func enableMenus(start: Bool,stop: Bool,reset: Bool) { startTimerMenuItem.isEnabled = start stopTimerMenuItem.isEnabled = stop resetTimerMenuItem.isEnabled = reset } 为了让你的你的 app 能在初次启动时自动配置按钮的启用状态,在 enableMenus(start: true,stop: false,reset: false) 每当用户按下了任何一个按钮或菜单项的时候, configureButtonsAndMenus() 再次编译并运行你的 app,你可以看到按钮们如预期地启用和禁用了。点击菜单里的菜单项试试,它们应该拥有和按钮一样的功能。
偏好设置窗口这个 app 还有一个很重要的问题:如果你希望煮鸡蛋的时间不是 6 分钟呢? 在第二部分中,你已经设计好了一个偏好设置窗口来允许用户来选择需要的倒计时时间,这个窗口是由 用户的设置可以通过一个叫 在 Project Navigator(项目导航器) 中,右键点击 Model 分组,并选择 Xcode 菜单上的 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") } } } 所以这些代码又干了些啥?
通过使用 getter 和 setter, 现在切换回 PrefsViewController.swift,我们需要把用户修改的设置内容在界面上显示出来。 第一步,在 IBOutlet 之下添加这些代码: var prefs = Preferences() 这一步中你创建了一个 接下来,添加这些方法: 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)" } 好像是很大一坨代码 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |