macOS 新手开发:第 2 部分
欢迎回到 macOS 开发教程初学者系列 3 部分中的第 2 部分!
在本系列的第 1 部分中,学习了如何安装 Xcode、如何创建新 app、添加 UI、将 UI 连接到代码、运行 app、调试 app 以及如何获得帮助。如果其中还有没掌握的,回去再看一遍第 1 部分。
在本部分中,你将为更复杂的 app 创建用户界面。你将学习如何让窗口大小可被调整,以及设计和导航到第二个窗口,以显示 app 的偏好设置。
开始
打开 Xcode 然后在欢迎窗口点击 Create a new Xcode project 或者选择 File/New/Project… 就像你在第 1 部分做的,选择 macOS/Application/Cocoa Application。点击 Next,命名为 ** EggTimer**,确定语言是 Swift,勾上了 Use Storyboards。点击 Next 然后选择存储项目的位置。
构建并运行你的新 app,确保一切正常。
EggTimer App
你即将构建的的 app 叫做 EggTimer;它从用户选择的时间开始倒计时,显示剩余时间。有一张图片随着鸡蛋煮熟而变化,一个声音在鸡蛋煮好时播放。第二个窗口用于显示 app 的偏好设置。
从项目导航器里打开 Main.storyboard。在第 1 部分中已经讲过,已经存在三个组件:
- Application Scene
- Window Controller Scene
- View Controller Scene
Application Scene 包含 app 运行时显示的菜单栏和菜单。Window Controller 是 app 的一部分,定义窗口的行为:如何调整大小,新窗口如何显示,app 是否保存窗口大小和位置等。window controller 可以管理多个窗口,但是如果它们需要不同的属性,则需要添加另一个 window controller。
View Controller 在窗口内显示用户界面——你会在这里为主显示屏布局 UI。
注意,Window Controller 有一个指向它的箭头。这表示它将控制 app 启动时的初始画面。你可以在 Document Outline 里选择 Window Controller,然后到 Attributes Inspector 里勾选它。取消对 Is Initial Controller 的勾选,这个箭头就消失了。把它再勾起来吧,因为你希望它是初始控制器。
Window Controller
开始布局用户界面之前,确保你已经在项目导航器中选择了 Main.storyboard。点击 Window Controller 内部以选择它的窗口。Window Controller 是那个显示了文本 “View Controller” 的可视化编辑器,因为这是被它所包含的。对于这个 app,我们不想让窗口缩小到 346 x 471 像素以下。这也会是窗口的初始尺寸。
在 Utilities 里选择 Size Inspector,然后将 Content Size Width 设置为 346,Content Size Height 设置为 471。勾选 Minimum Content Size,并确保宽度和高度值与 content size 相同。可视化编辑器中的 Window Controller 的大小已经改变了。你现在应该需要移动一下它,使其不与其他对象重叠。
虽然不是绝对必要,但是如果将 View Controller 调整为与它包含的 Window Controller 相同的尺寸,看起来会更轻松一些。单击视图控制器,确保在 Document Outline 中选择了 View。在 Size Inspector 中将宽度和高度分别设置为 346 和 471。根据需要移动一下位置以查看所有对象。现在 WindowController 和 ViewController 在可视化编辑器中以相同的大小显示。
选择 WindowController 中的 Window,并在 Attributes Inspector 中将其 title 更改为 Egg Timer。将 Autosave name 设置为 EggTimerMainWindow,启动时会记录上次窗口的大小和位置。
如果你是 iOS 程序员,你会处理不同的设备类型、旋转方向的各种屏幕尺寸。在 macOS 编程中,你必须处理无限多种窗口大小和宽高比,这就是为什么我让这个窗口的初始尺寸看起来有点奇怪。幸运的是,Auto Layout 可以处理这一切。
布局 UI——第 1 部分
基本 UI 由 2 个 stack view 组成。第一个包含剩余时间文本和鸡蛋图像。第二个包含底部的3个按钮。从按钮开始:
- 在 Object Library 里搜索 “Button”
- 拖一个 Gradient button 到 View Controller 里
- 使用 Attributes Inspector,删除它的图片并把 title 设置为 Start。
- 把 font 改为 System 24。
- 展开按钮以显示所有文本。
- 选中 Start 按钮,按两次 Command-D 以创建 2 个拷贝。
- 把新按钮拖出来以便你能看见它们。
- 修改新按钮的标题为 Stop 和 Reset。
- 同时选中 3 个按钮,然后点击 Editor/Embed In/Stack View。
要让按钮填满 stack view,选中新的 Stack View,然后在 Attributes Inspector 里做如下改变:
- Distribution:Fill Equally
- Spacing:0
单击 Visual Editor 底部的 Add New Constraints 按钮,并如图所示设置左、右、底和高度约束。选择 Update Frames: Items of New Constraints 然后点击 Apply 4 Constraints。
Stack view 现在的位置已经正确了,但按钮比 stack view 要短一些。在 Document Outline 里,按住 Control 从 Start 按钮拖到 Stack View,然后选择 Equal Heights。对其它两个按钮执行相同操作。
按钮 stack view 现在就是想要的样子。
构建并运行 app。尝试调整窗口大小:按钮黏在窗口的底部,并自动调整大小以均匀填充宽度。
最后一次操作,在 Attributes Inspector 中取消选中 Enabled 来禁用 Stop 和 Reset 按钮。在定时器启动之前启用它们没有任何意义。
布局 UI——第 2 部分
第二个 stack view 包含剩余的时间和图片。把 Label 拖到 View Controller 中,设置 Title 为 6:00,然后设置 Alignment 为 center。当前系统字体(San Francisco)使用比例间距的数字,这意味着,如果你有一个计数器,数字改变的时候会跳跃——这真的很烦人。
把字体切换到 Helvetica Neue 以避免这种情况,设置字体大小为 100。这将使文本太大无法显示,因此展开 label field 直到能够看见。
要添加图片,请打开 Object Library,在filter field 中 输入 “image” 进行搜索。然乎会出现几个选项,但你想要的是 Image View。将其拖到 View Controller,放在 time remaining label 下方。
下载 此项目的资源 (图片和声音文件)。解压文件然后打开 Egg Images 文件夹。在 Xcode 中,点击 Project Navigator 中的 Assets.xcassets。
将 6 个图像文件拖动到 Assets library 中。它们现在可以用于这个 app。因为图像文件名包含 “@2x”,所以它们已自动分配给每个图像资源的 2x 部分。
返回 Main.storyboard,选择刚刚添加的 Image View,然后单击 Attributes Inspector 中的 Image 弹出窗口。可以看到刚刚添加的图像以及内置图像。选择 stopped。
制作第二个 stack view:选择 time remaining label 和 image view。选择 Editor/Embed In/Stack View。现在,您需要配置此 stack view 以填充可用空间。单击 Visual Editor 底部的 Add New Constraints 按钮,并设置以下约束:
Stack view 已按需扩展,但 image view 仍然太小。选择 image view,并将其左右约束设置为 Standard Value,如图所示。
在 Attributes Inspector 中,将 ** Scaling** 设置为 Proportionally Up or Down。
构建并运行 app 。调整窗口大小以检查所有 UI 元素是否按预期调整大小和位置。
将 UI 连接到 代码
正如在本系列的第 1 部分中学到的,你需要设置 @IBOutlets
和 @IBActions
以将 UI 连接到代码。在此窗口中,您需要以下元素的@IBOutlets
:
- Time remaining label
- Egg image view
- The 3 buttons
3 个按钮也需要 @IBActions
以在用户点击它们的时候触发函数。在项目导航器中,选择 Main.storyboard。按住 Option 单击 Project Navigator 中的 ViewController.swift 以在 Assistant 编辑器中打开它。如果空间不足,请使用右上角的按钮隐藏 Utilities 和 Navigator 面板。
选择 countdown timer label 并按住 Control 拖动到 ViewController
类中,就像在第 1 部分中做的那样。将 label 的名称设置为 timeLeftField
。对 egg image view 重复同样的操作,将其名称设置为 eggImageView
。设置按钮的 outlets,命名为 startButton
,stopButton
和 resetButton
。
这些按钮还需要 @IBActions
。按住 Control 从 Start 按钮拖动,但这次将 Connection 弹出窗口更改为 Action,并将名称设置为 startButtonClicked
。对其他按钮重复,创建名为 stopButtonClicked
和 resetButtonClicked
的 action。
如果你像我一样,忘记更改 Connection 弹窗窗口到 Action,你最终将有两个 @IBOutlets
而没有 @IBAction
。要删除额外的 @IBOutlet
,首先删除 ViewController
中多余的代码行。然后在 Utilities 中打开 Connections Inspector。
你会看到 Referencing Outlets 下的两个条目。点击错误的旁边的 X 将其删除。然后回去,记得改为 Connection 弹出窗口以使用 @IBAction
。
ViewController
代码现在应该如下所示:
在本系列的第 3 部分中,将向这些函数添加代码以使它们工作。现在关闭 Assistant Editor ,重新打开 Navigator 和 Utilities 面板(如果你把它们关上了的话。
菜单
在 Main.storyboard 中,单击 menu bar 或 Application Scene 以将其选中。app 模板提供了一组默认的菜单,但对于此 app,大多数菜单不必要。浏览菜单最简单的方法是使用 Document Outline。使用三角形展开显示 View 菜单及其内容。
菜单栏的结构是一系列嵌套菜单和菜单项。切换到 Utilities 面板中的 Identity Inspector
,以便在单击列表时可以看到列表中的每个条目。Main Menu 是 NSMenu
类的一个实例。它包含一个 NSMenuItems
数组:View 是其中之一。
View 菜单项包含一个带有自己的 NSMenuItems
的子菜单(NSMenu
)。注意 Separator
项,它只是 NSMenuItem
的一种特殊形式。
首先要做的是删除这个 app 不需要的菜单。选择 Document Outline 中的 File 菜单,然后按 Delete 将其删除。如果在可视编辑器中选择它并删除,将只删除了 File 菜单项中的菜单,因此会在菜单栏中留下一个空白。如果发生这种情况,选择那个空白,然后再次按 Delete 将其删除。
继续删除菜单,直到只剩 EggTimer,Window 和 Help。
现在你要添加一个新的菜单,模拟 3 个按钮的操作。在 Object Library 中搜索 “menu”。记住每个菜单以 menu item 开始,将 Menu Item 拖动到 EggTimer 和 Window 之间的菜单栏。它将显示为一个蓝色框,但这是因为它没有标题。
现在将 Menu 拖动到蓝色框中。如果你发现难以定位到蓝色框,请拖动到 Document Outline ,就在新的 Item 下面。新菜单仍然没有标题,但它现在有三个项目。
选择菜单(而不是 item),切换到 Attributes Inspector,把 title 改为 Timer。这将为你的新菜单分配一个名称。选择 Item 1,然后通过双击编辑或使用 Attributes Inspector 将其标题更改为 Start。
单击 Attributes Inspector 中的 Key Equivalent 字段,然后按 Command-S 分配一个快捷键。通常 Command-S 意味着保存,但是因为“文件”菜单已经被删除了,这就不是冲突了,尽管为其他功能重用常规快捷键并不是一个好的做法。
使用相同的方法将第二个项目的标题设置为 Stop,使用快捷键 Command-X,将第三个项目的标题设置为 Reset,使用快捷键 Command-R。
可以在可视化编辑器中的菜单栏顶部看到三个按钮。切换到 Identity Inspector。依次点击每一个,显示它们都链接到了 Application 、 First Responder 和 AppDelegate。第一响应者通常是最前面的视图控制器,并且它可以从菜单项接收动作。
按住 Option 单击 ViewController.swift,并在按钮的 @IBActions
下面添加以下代码:
// MARK: - IBActions - menus
@IBAction func startTimerMenuItemSelected(_ sender: Any) {
startButtonClicked(sender)
}
@IBAction func stopTimerMenuItemSelected(_ sender: Any) {
stopButtonClicked(sender)
}
@IBAction func resetTimerMenuItemSelected(_ sender: Any) {
resetButtonClicked(sender)
}
这些函数将被菜单调用,并且它们将调用按钮动作函数。你可以有直接调用按钮动作函数的菜单项,但我没有选择这样做的目的是使事件序列在调试时更加明显。保存文件并关闭 Assistant Editor。
按住 Control 从 Start 菜单项向上拖动到指示 First Responder 的橙色块。弹出窗口将显示一个数量繁多的选项列表。输入 “sta” 会快速滚动到正确的部分,并选择 startTimerMenuItemSelected。
用同样的方式将 Stop 和 Reset 菜单项连接到 stopTimerMenuItemSelected
。现在当 EggTimer 窗口在前面时,选择菜单项将调用这些函数。
然而,3 个按钮不是全部被同时启用,并且菜单项需要反映按钮的状态。这不会发生在 ViewController
中,因为它不会总是 First Responder,因此将在 AppDelegate
中控制菜单项。
打开 Main.storyboard 并显示菜单,按住 option 在 Project Navigator 中点击 AppDelegate.swift。按住 Control 从 Start 菜单拖动到 AppDelegate
,并分配一个 outlet 名称 startTimerMenuItem
。
在第 3 部分中,将添加代码以根据需要启用和禁用这些菜单项,但是现在,需要关闭自动启用和禁用。通常,app 会检查当前的 First Responder 是否具有菜单项的操作,如果没有就禁用它。对于这个 app,需要自己控制。选择 Timer 菜单,并取消选中 Attributes Inspector 中的 Auto Enables Items。
偏好设置窗口
EggTimer app 的主窗口现在看起来不错,但它需要一个偏好设置窗口,以便用户可以选择煮鸡蛋的程度。
“偏好设置”将显示在具有自己的窗口控制器的单独窗口中。这是因为“偏好设置”窗口将有不同的默认大小,并且不能调整大小。可以由同一窗口控制器显示多个视图控制器,但是它们将共享该窗口控制器的属性。
打开 Main.storyboard,关闭 Assistant Editor(如果开着的话),并在 Objects Library 中搜索 “window”。将一个新的窗口控制器拖动到可视化编辑器中。它也将创建一个视图控制器以显示其内容。将它们排列在窗口中,让新的窗口控制器靠近菜单栏,以便它们易于查看。
打开 EggTimer 菜单,并按住 Control 从 Preferences.. 拖动到新的窗口控制器。从出现的弹出窗口中选择 Show。这将创建一个 segue,以便每当用户从 EggTimer 菜单中选择 Preferences… 时,此窗口控制器将显示新的视图控制器。
Preferences 窗口控制器将显示一个新的视图控制器,因此现在需要为该视图控制器创建类。在 Project Navigator 中,选择现有的 ViewController.swift 文件;这确保新文件将放在 Project Navigator 中有逻辑的位置。选择 File/New/File…
选择 macOS/Cocoa Class 然后点击 Next。将类名设置为 PrefsViewController
,并使其成为 NSViewController
的子类。检查语言是否设置为 Swift,并取消选中 Also create XIB file for user interface。单击 Next 和 Create 以保存文件。
返回 Main.storyboard,选择新的视图控制器。确保选择视图控制器本身,而不是它的视图;使用 Document Outline 会更容易。在 Identity Inspector 中,将其类设置为 PrefsViewController
。
在偏好设置窗口控制器中选择窗口,并使用 Attributes Inspector 将其标题设置为 Preferences。不要设置自动保存名称,因为此窗口每次都会出现在屏幕中央。取消选中 Minimize 和 Resize 控件,以使窗口大小固定。
转到 Size Inspector,并为内容大小输入宽度 416 和高度 214。 在 Initial Position 下,从 2 个弹出窗口中选择 Center Horizontally 和 Center Vertically。
选择 PrefsViewController 中的 View,并使用 Size Inspector 将其宽度更改为 416,将高度更改为 214。
PrefsViewController
将显示一个弹出窗口,用于选择预设时间,以及用于选择自定义时间的滑块。这两个每个会有一个 label,还有两个按钮:Cancel 和 OK。还会有一个动态 label 显示当前选择的时间。
将一下控件拖到视图控制器中,如下排列它们:
- Label——设置 title 为 “Preset Egg Timings:”
- Pop Up Button
- Label——设置 title 为 “Custom Egg Timing:”
- Label——设置 title 为 “6 minutes”
- Horizontal Slider
- Push Button——设置 title 为 “Cancel”
- Push Button——设置 title 为 “OK”
由于此窗口不会调整大小,因此不需要应用任何自动布局约束——对象将始终像安排的那样显示。拖动对象安排它们的位置,蓝色指导线会帮助你。将 “6 minutes” label 的宽度扩展到窗口右侧附近,因为它可能包含更多文本。双击 Pop Up Button 查看前三个项目并将其标题设置为:
- For runny soft-boiled eggs (barely set whites): 3 minutes
- For slightly runny soft-boiled eggs: 4 minutes
- For custardy yet firm soft-boiled eggs: 6 minutes
从 Objects Library 中拖出两个 Menu Item,还有一个 Separator Menu Item,最后是另一个 Menu Item。如果您在放置时遇到任何问题,请使用 Document Outline。
把剩下的 menu item 标题设置为:
- For firm yet still creamy hard-boiled eggs: 10 minutes
- For very firm hard-boiled eggs: 15 minutes
- Custom
我不会假装自己是煮鸡蛋专家,所以我从 The Kitchn 那里得到了这些时间以及描述。
选择 popup 本身,而不是任何 item,并将其 Selected Item 设置为 6 minute 选项。
现在要做一个小窍门,让 app 准确知道选择的分钟数。对于 popup 中的每个 menu item,在 Attributes Inspector 中将其 tag 设置为分钟数:3,4,6,10,15。Custom menu item 的 tag 保留为0。
现在选择 Slider,并在 Attributes Inspector 中将 Tick marks 设置为 25,Minimum Value 设置为 1,Maximum Value 设置为25,Current Value 设置为6,并选中 Only stop on tick marks。看见刻度标记后,你可能需要将滑块向下移动几像素。通过取消选中 Enabled 来禁用滑块——只有在弹出窗口中选择了 Custom 时,才会启用。
连接 Preferences 的对象
按住 option 从 Project Navigator 中单击 PrefsViewController.swift,如果需要更多空间的话,隐藏侧边栏。popup 需要 slider 和显示 “6 minutes” 的 label 的 @IBOutlets。按住 Control 把它们拖到 PrefsViewController
里,outlet 命名如下:
- Popup:
presetsPopup
- Slider:
customSlider
- Label:
customTextField
接下来,按住 Control 以创建 @IBActions
,记得每次在弹出窗口里都要将 Action 设置为 Connection:
- Popup:
popupValueChanged
- Slider:
sliderValueChanged
- Cancel button:
cancelButtonClicked
- OK button:
okButtonClicked
代码现在应该如下所示:
“Preferences” 窗口的布局现已完成。构建并运行 app,并从 EggTimer 菜单中选择 Preferences。看完后单击标题栏按钮中的红色关闭按钮以关闭窗口。
App Icon
现在 UI 中只剩为 app 添加图标了。你已下载 app 的资源文件夹,并安装了一些图像到 Assets.xcassets 中。再次打开它,找到 egg-icon.png 文件。
从 Project Navigator 中选择 Assests.xcassets,点击 AppIcon 并将 egg-icon.png 拖动到 Mac 256pt 1x 框中。如第 1 部分所述,如果要发布 app,需要提供 AppIcon 中显示的所有尺寸,但对于此 app,单个大小就足够了。
构建并运行 app,确认 Dock 中有新图标。如果看到的仍然是默认图标,请从 Xcode 的 Product 菜单中选择 Clean,然后重试。
下一步?
现在,app 的 UI 已经完全实现了,但它还不能做任何事。如果在某个地方没有跟上,可以 下载 Xcode 项目 ,其中所有的 UI 都已为下一部分准备好。
在本系列教程的第 3 部分中,会添加代码以使 app 正常工作。
如果对本教程有任何问题或意见,请在下方评论!