Skip to content

Latest commit

 

History

History
1437 lines (1412 loc) · 61.5 KB

macOS Controls Tutorial - Part 2:2.md

File metadata and controls

1437 lines (1412 loc) · 61.5 KB

macOS控件教程:2/2部分

更新日志: 已由Ernesto García更新至 Xcode 8.2 / Swift 3 版。 之前 Michael Briscoe 更新至Xcode 6.3 / Swift 1.2。 初始版本 作者为 Ernesto García

欢迎回到macOS控件教程的系列的第二部分,也就是最后一部分。

这个教程的第一部分 ,你开始了构建一个Mad Lib风格的macOS app,在这里用户可以输入单词和短语来创建有趣的桔子。

继续下去,你会学到一些核心的macOS UI控件 - 也就是Text Fields,Combo Boxes,Pop Up Buttons,Push Buttons和Text Views.

更多macOS控件

在这个教程中的最后一部分,你会完成你的app,并学会如何使用下面的控件:

  • Sliders
  • Date Pickers
  • Radio Buttons
  • Check Boxes
  • Image Views

在这个两部分的教程的末尾,你会对macOS的控件有一个很好的理解。

这个教程将从你中断的地方再捡起来。如果你还没有之前的项目,这里是第一部分的 最后的项目

是时候开始工作了!

滑动 - NSSlider

slider是一个控件,让用户可以从一个范围中进行选择。slider有一个最小值和最大值,通过移动控件的“把手”,用户可以在两个限制中进行选择。slider可以是线性或径向的。你会问,两者间的区别是什么?

线性的slider可以是垂直的或水平的,他们让你可以通过沿着轨迹移动把手来选择一个值。关于线性slider的一个很好的例子,就是macOS中的鼠标偏好面板了:

slider-mouse

径向slider有一点的不同 - 它是一个小小的带有把手的圆心,它可以360度地进行旋转。你可以点击和拖拽把手到要求的位置来选择一个值。关于径向slider,你可以在Adobe Photoshop中来找到一个很棒的例子,它用来定义一个渐变的角度,就像下面这样:

在macOS中负责这个的控件是 NSSlider

全部三种类型的slider(水平、垂直和径向)实际上都是一个控件 NSSlider 。唯一的区别只是它们的外表。Interface Builder对于每种类型的slider在Object Library中都有一个对象,如同下面展示的这样:

sliders-ib

Slider语义

当使用slider的时候,通常你会执行两个任务,获取或设置当前的值,以及获取和设置slider让位的最高值和最低值。这些property被展示如下:

// getting & setting an integer value
let theInteger = mySlider.integerValue
mySlider.integerValue = theInteger
// getting & setting a float value
let theFloat = mySlider.floatValue
mySlider.floatValue = theFloat
// getting & setting a double value
let theDouble = mySlider.doubleValue
mySlider.doubleValue = theDouble
// getting & setting the minimum value of the range
let theMinimumValue = mySlider.minValue
mySlider.minValue = theMinimumValue
// getting & setting the maximum value of the range
let theMaximumValue = mySlider.maxValue
mySlider.maxValue = theMaximumValue

再一次,这里没有什么好奇怪的 - 如果你现在已学到了什么,它就是实现标准macOS控件的一个相当简单的练习。移步到下一部分,让你的app包含一个 NSSlider

选择一个数字,任何数字

打开 Main.storyboard 。在Object Library的面板中定位到 Label 控件,并将它拖拽到content view上Phrase label的下面。改变窗口的垂直方向的大小,并将 Go! 按钮向下拖拽,如果你需要给它空间的话。

双击控件来编辑它的默认文本,改变为 Amount: [10] 。找到 水平Slider 控件并将它拖拽到窗口上,并将它放置到label的右边。

点击slider并选择它。打开 Attributes Inspector 并设置最小值为2,最大值为10。改变当前的值为5。这会作为当用户首次运行app时,slider的默认的值。

确保 Continuous 已选中。这告诉slider来通知 any slider值的任何变化。

slider-add

现在你创建两个outlet;一个是slider的,另一个则是label。稍等,你可能会说 - 有一点的不同。为何要给label添加一个label?

这是因为,这样就可以在slider的值发生变化的时候,更新label的文本来列出当前的数量;因此为何要给slider的 Continuous property。哈哈,现在就有意义了吧,不是吗?

打开Assistant editor并确保 ViewController.swift 已打开。 按住Ctrl拖拽label ViewController.swift 上来创建一个新的outlet。

slider-drag

在弹出的屏幕中,将outlet命名为 amountLabel

amountlabel-drag2

对slider重复上面的过程,将outlet命名为 amountSlider

现在你需要添加一个 action ,当slider的值发生变化的时候进行调用。你早已在第一部分中为你的button创建了action,因此你会获得更多的实践!

选择slider并 按住Ctrl拖拽 ViewController.swift 中类定义中的任何地方:

slider-action-drag

在弹出的窗口中,确认设置connection为一个 action 而不是一个outlet。将它命名为 sliderChanged ,就像下面这样:

slider-action2

现在你需要在action被调用时更新label。

打开 ViewController.swift 并添加下面的代码到 sliderChanged() 中:

let amount = amountSlider.integerValue
amountLabel.stringValue = "Amount: [(amount)]"

快速回顾上面展示的代码,你第一次读取了slider当前的值,然后你label的值为一个包含slider的值的字符串。请注意,用户不能从UI中编辑 amountLabel 的值,但仍然可以通过编程的方式来编辑它。

注意: 这个例子使用 integerValue 获取了一个很好的整数,但如果你需要更加精确,你可以为你的slider使用 floatValue doubleValue 的值。

运行项目。尝试来回滑动slider,来查看label的值跟随slider当前的值变化而变化:

buildrun-slider

有一个小问题。你注意到了么?当app第一次运行的时候,标签并不会展示正确的值!虽然这不是一个大问题,但看起来app并未完成。这是因为label仅会在slider的把手挪动的时候才会更新。

不要害怕 - 这很容易解决。

添加下列的代码到 viewDidLoad() 尾部:

// Update the amount slider
sliderChanged(self)

现在app就会在启动时调用 sliderChanged() ,它就会更新label。Neat!

运行项目。现在label就在一运行时展示正确的值,这是一个很小的接触,但这是“配合和完成”,让你的app看起来更优美的元素。

更复杂的值,例如日历日期呢?是的,macOS也有那些的处理了!:]

今夜的“热日期” - NSDatePicker

Date Picker是一个展示日期和时间的macOS控件,提供了一个方法来让用户编辑这些值。Date Picker可以被指定来展示一个日期,一个时间或同时展示日期和时间。在macOS中负责这个的控件是 NSDatePicker

Date Picker可以以两种风格的样式来展示:文本的,它的日期和时间信息将会被展示到text field中;以及图形的,它的日期会以一个日历的形式展现,时间则以一个时钟的形式来展现。你可以在macOS的日期&时间偏好面板中找到全部风格的例子:就像下面截图中展示的这样:

date-macos

对于date picker,你最常见的要执行的任务就是获取和设置它的日期和时间了,已经设置在你的控件中所允许的最小和最大的日期/时间的值。用来实现这些的property已展示在下面!

// getting & setting the date/time value
let myDate = myDatePicker.dateValue
myDatePicker.dateValue = myDate
// getting & setting the minimum date of the range
let theMinimumDate = myDatePicker.minDate
myDatePicker.minDate = theMinimumDate
// getting & setting the maximum date of the range
let theMaximumDate = myDatePicker.maxDate
myDatePicker.maxDate = theMaximumDate

再一次的 - 这个控件拥有非常简单的getter和setter风格的接口来更新这些值。现在是时候(原谅我的调皮!)把这个控件放到你的项目上了。

我误了一个非常重要的日期

跟着习惯的步骤,添加一个 Label 到你的窗口上。将它的标题改为 Date: ,对齐方式改为 向右 。在对象面板中找到 Date Picker ,并把它拖拽到窗口上,将它放到label的右边。如果需要的话,改变窗口的大小,并将 Go! 按钮向下移动:

add-datepicker

为date picker创建一个outlet,就在之前你完成的每个控件的outlet的下面。在弹出的窗口中,将property命名为 datePicker

就像app中的其它控件一样,当运行你的app时,向用户展示一个默认的值会比较好。选取当前的日期作为默认值听起来是一个不错的选择!:]

打开 ViewController.swift 并添加下面的代码到 viewDidLoad() 方法的末尾:

// Set the date picker to display the current date
datePicker.dateValue = Date()

运行项目!你应该会看到你漂亮的新date picker正在展示当前的日期,就像下面截图中的一样:

buildrun-datepicker

视频杀掉了音频...按钮

Radio buttons是一个特殊类型的控件,经常以组的形式出现;典型地,它们会展示为一个带有在一边的可选按钮的选项的列表。它们的表现在某种程度上时独一无二的;在组中的任意按钮都可以被选择,但只要选择了其中一个按钮,就会将组中其它按钮的选择都取消掉。在相同的组中,同时只有一个按钮可以被选中。

关于radio buttons的一个展示一系列选项的很好的例子,就是iTunes的备份的选项,就像下面这样:

Radio buttons是以radio groups的形式组织起来的。

当你点击了组中的一个按钮,它就会被选中,并且系统会自动将组中其余的按钮取消选择。你只需要考虑获取和设置合适的值。多么得方便!

但你该怎么定义一个组?引用文档中所说的:

“Radio buttons自动地作为了一个组(选择一个按钮将取消全部其它相关的按钮),当它们拥有相同的父view和动作方法。”

因此,所有你需要做的就是添加radio buttons到一个view上,创建一个动作,并设计这个组中所有按钮的动作。

然后你只需要改变按钮的状态(On/Off),系统自己会管理取消选择其它的按钮。

// Select a radio button 
radioButton.state = NSOnState
// Deselect a radio button
radioButton.state = NSOffState
// Check if a radio button is selected.
let selected = (radioButton.state == NSOnState)

再一次地,一个复杂的控件被削减为一些非常简单的方法。继续阅读,来了解如何在你的app中实现一个radio button控件!

呼叫家的地方 - 添加Radio Buttons

添加一个新的 Label 到你的app上(你现在应该对此感到非常舒服了!),并将它的标题改为 Place: 。在Object Library面板中找到 Radio Button ,并将它拖拽到content view上,就在label的旁边。双击radio buttons,来将他们的标题分别改为: RWDevCon 360iDev WWDC

radioadd

现在,你需要为每个radio button创建一个新的outlet - 现在你应当相当熟悉另一个动作了!打开 Assistant Editor 按住Ctrl拖拽 第一个radio button到 ViewController.swift 的源文件中,就在已存在的property的下面。将outlet命名为 rwDevConRadioButton 。对另外两个radio buttons重复同样的过程,将outlet分别命名为 threeSixtyRadioButton wwdcRadioButton

运行项目。

buildrun-radio-error

点击radio button,你会立刻注意到一个问题。你可以选择全部的radio button。它们并没有表现得像一个组。为什么?因为要让不同radio button成为一个组,需要有两个条件:它们必须拥有相同的父view(这是关键),他们需要有一个共同的动作。在我们的app中并不满足第二个条件。

为了解决上述情况,你需要为这些按钮创建一个共同的动作。

你早已是个使用Interface Builder创建动作的专家了吧,对么?所以,我们来学习一种替代的方法,在代码中创建动作,并将其指派到Interface Builder的radio buttons上。

打开 ViewController.swift 并在类的实现中添加下列的代码:

@IBAction func radioButtonChanged(_ sender: AnyObject) {
}

这是一个典型的动作方法,它包含一个 @IBAction 的标注,因此Interface Builder就可以找到并使用它。现在,打开 Main.storyBoard 并指派这个动作给radio button。

前往 Document Outline 按住Control单击 (或右击)View Controller。在弹出的窗口中,找到 Received Actions 区,并从靠近 radioButtonChanged: 的圈拖拽到 RWDevCon radio button的上面。只需简单的动作,你就将动作指派到了radio button的上面。

radio-addaction

重复上述过程,将相同的动作指派到另外两个radio button上面。你的received action现在应当看起来就像是这样:

radio-actions-all

运行项目:

buildrun-radio

现在radio buttons就应该表现得像一个组了。

现在,你需要让 RWDevCon radio button成为app启动时的默认项。你只需要设置radio button的state为On,然后其它的按钮就会被自动地取消选择。

打开 ViewController.swift 。并添加下列的代码到 viewDidLoad() 方法的尾部:

// Set the radio group's initial selection
rwDevConRadioButton.state = NSOnState

运行项目。你会看到在app启动时, RWDevCon radio button已被选中。

radio-rwdev

Radio button是在你的app中切换值的一种方法,但还有着执行类似功能的一个另外的macOS控件 - check boxes!

勾选全部的盒子 - Check Box Button

通常,你会在app中使用check box来展示一些布尔值的状态。这些状态常常会以某种方式影响app的一个特性的打开或关闭。

你会在当用户打开或关闭一个功能的地方找到check box。你会在Settings app中几乎每一个屏幕中找到它。例如,在Energy Saver窗口中,你会使用check box来打开或关闭不同的选项。

check-example

使用check box相当容易;大多数情况下,你只需要关心获取和设置这个控件的状态。check box的状态可以是下列之一: NSOnState (在所有地方打开特性), NSOffState (在所有地方关闭特性)以及 NSMixedState (在部分地方打开特性,但并非全部)。

这里是你使用它的方式:

// Set the state to On
myCheckBox.state = NSOnState
// Set the state to Off
myCheckBox.state = NSOffState
// Get the state of a check box
let state = myCheckBox.state

超级得简单!是时候添加一个checkbox到你的app上。

单击和双击 - 添加Checkboxes

打开 Main.storyboard 。在Object Library中找到 Check Box Button 并将它拖拽到content view上。双击它,并将标题改为 Yell!! 就像下面图中展示的这样:

check-add

现在,为check box添加一个outlet并将它命名为 yellCheck 。你现在已经正式地成为一个创建outlet的专家了!

现在,你会将check box在app启动时的默认值设为off。为了做到这点,添加下列的代码到 viewDidLoad() 的尾部:

// set check button state
yellCheck.state = NSOffState

运行项目!你应当看到check box的状态是未勾选的。点击它来查看其行为:

buildrun-check

选择,选择 - NSSegmentedControl

一个segmented control,是由 NSSegmentedControl 类所代表的,它是当你需要从一些选项中做出一些选择时,对radio button的替代品。你可以在Xcode的Attributes Inspector中找到它:

segmented-alignment

它是非常容易被使用的。你只需要获取或设置被选择的segment来找出用户的选择。

// Select the first segment
segmentedControl.selectedSegment = 0
// Get the selected segment
let selected = segmentedControl.selectedSegment

调节声音 - 添加Segmented Controls

如果你还记得, readSentence() 方法含有一个控制语速(Normal, Fast, Slow)的参数。你将使用一个segmented control来改变语速。

打开 Main.storyboard 并添加一个label到content view上面。将它的标题改为 Voice Speed: 。找到 Segmented Control 并将它拖拽到content view上面。你可以双击控件的每个segment来改变他的标题。将标题分别改为 Slow Normal 以及 Fast

segmented-add

ViewController 中为segmented control创建一个outlet,并命名为 voiceSegmentedControl 。现在,你想要在app一启动时选择Normal segment,它对应的序号是1(segment的序号是从0开始计算的)。打开 ViewController.swift 并添加下面的代码到 viewDidLoad()

// Set the segmented control initial selection
voiceSegmentedControl.selectedSegment = 1

就像它看起来一样得容易。只需将 selectedSegment 的property设置为1。现在,运行项目,查看Normal segment是否已选中。

buildrun-selected

OK!你已经添加了所有创建你有趣的mad lib句子所需的控件。你所缺少的只是收集每一个控件的值,并将它们连接成一个句子,并将它们展示到屏幕上!

全部拉到一起

你需要再来两个控件展示结果:一个label用来展示完整的句子,已经用来展示图片的image view,使用户的交互显得更加有生气!

打开 Main.storyboard 。在Object Library面板中找到 Wrapping Label 并将它拖拽到窗口上,就在 Go!! 按钮的下方。通过 Attributes Inspector 来让它看起来更漂亮:将border改为Frame,就是那四个按钮中的第一个。

之后,双击label移除它的默认文本,选择文本并删除它。

resulttext-add

现在你必须创建一个outlet以方便设置这个新的label的值,来容纳你的新的滑稽的桔子!就像之前一样, 按着Ctrl拖拽 label到 ViewController.swift 文件中并将property命名为 resultTextField

现在,离开这个控件,让它停留在现在的样子;你将在稍候编写一些代码来填充它。

看到风景的房间 - NSImageView

Image View是一个非常简单易用的控件 - 惊喜! - 展示一个图像。我敢打赌你都想不到是这样!:]

在运行时,你需要和Image View的几个property进行交互:

// Get the image from an image view
let myImage = myImageView.image
// Set the image of an image view
myImageView.image = myImage

在设计的时候,你可以配置其外观:border, scaling和alignment。是的,这些property也可以在代码中进行设置,但在设计时在Interface Builder中设置它会更加容易,就像下面这样:

imageview-props

只为一个漂亮的脸 - 填充图像的“井”

是时候添加一个image view到你的app上了!在 Object Library 中找到 Image Well 并拖拽到view上,放到wrapping label的左边。如果必须的话,放心地去调整app窗口的大小。

imagewell-add

用和你之前创建其它控件相同的方法,为image view创建一个新的outlet: 按着Ctrl拖拽 image view到 ViewController.swift 文件上,在弹出的窗口中将property命名为 imageView

运行项目。你的app现在看起来应该是这样:

buildrun-uifinished

哇!你的UI已经最终完成了 - 剩余的唯一一件事就是撰写代码来组装欢闹的句子,并填充你上面添加的image view!

是时候让它工作起来了

现在你需要基于那些输出来组装句子。

当用户点击 When the user clicks the Go! 按钮的时候,你会从不同的控件中收集全部的值,来组装成完整的句子,然后在你之前添加的wrapping label中展示出来。

然后,为了装饰全文界面,你会在你上一部分添加的image view上展示一个图片。

为这个项目(正常地进入你的 下载 目录)。下载 资源文件 。如果下载到的zip文件没有自动解压,就解压它,得到 face.png 图像文件。在 Project Navigator 中选择 Assets.xcassets ,并将图片文件拖拽到assets列表中。

assets

现在是最终的时间来添加app的核心了 - 构建Mad Lib句子的代码!

打开 ViewController.swift 并添加下列的property到类的实现中:

fileprivate var selectedPlace: String {
var place = "home"
if rwDevConRadioButton.state == NSOnState {
place = "RWDevCon"
}
else if threeSixtyRadioButton.state == NSOnState {
place = "360iDev"
}
else if wwdcRadioButton.state == NSOnState {
place = "WWDC"
}
return place
}

上面的代码添加了一个计算型的property,它会根据哪个radio button被选中返回place的名称。

现在,用以下的代码替换 goButtonClicked() 方法中的全部内容:

// 1
let pastTenseVerb = pastTenseVerbTextField.stringValue
// 2
let singularNoun = singularNounCombo.stringValue
// 3
let amount = amountSlider.integerValue
// 4
let pluralNoun = pluralNouns[pluralNounPopup.indexOfSelectedItem]
// 5
let phrase = phraseTextView.string ?? ""
// 6
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
let date = dateFormatter.string(from: datePicker.dateValue)
// 7
var voice = "said"
if yellCheck.state == NSOnState {
voice = "yelled"
}
// 8
let sentence = "On (date), at (selectedPlace) a (singularNoun) (pastTenseVerb) (amount) (pluralNoun) and (voice), (phrase)"
// 9
resultTextField.stringValue = sentence
imageView.image = NSImage(named: "face")
// 10
let selectedSegment = voiceSegmentedControl.selectedSegment
let voiceRate = VoiceRate(rawValue: selectedSegment) ?? .normal
readSentence(sentence, rate: voiceRate)

看起来有好多的代码,但当你把它拆开一部分一部分看时,每部分都是相当明确的:

  1. pastTenseVerbTextField 获取文本。
  2. 在这部分代码中,你通过 stringValue property从combo box中或取字符串。你可能会问,为何不只在选中的行进行查阅,然后检索关联到这行的字符串。非常简单,这是因为用户可以输入它们自己的文本到combo box中,因此选择使用 stringValue 来获取当前的字符串,它既可以被选择,也可以被输入。
  3. 接下来,使用 integerValue 方法读取slider当前的值。记住,如果你需要更精确的控件,你还可以使用 floatValue doubleValue
  4. 这里,你从popup button中获取了复数名词。这如何做到?在你的 pluralNouns 数组中使用下标语法,找到恰当的复数名词,并获取其 indexOfSelectedItem property。
  5. 接下来是用户输入的短语。要获取它,只需从 string property中获取我们的text view的字符串的值。再一次,你使用了nil coalescing语法,因为这个property是可选的,它的值可能是 nil
  6. 调用date picker的 dateValue 方法获取日期。然后使用 NSDateFormatter 将其妆花为一个人类可读的字符串。
  7. 你应该说还是喊?只需获取checkbox的状态:如果它是 NSOnState ,指派 yelled 到字符串变量中。否则,让它使用默认的 said
  8. 现在,你已经收集了所有你构建mad lib句子所需要的信息!这是神奇的事将发生的地方,基于用户的输出,从各种控件中的不同的值构建成了一个字符串。
  9. 现在你所有你努力工作所得出的结果了可以进行展示了!首先,通过设置results label的 stringValue property来展示最终的句子。然后,通过向用户展示一副图像,来给app添加一些魅力,这和加载图像并设置image view控件的property一样得简单。
  10. 最后,大声地说出来吧!从当前被选择的segment获取语速,并调用将文本转换为朗读的方法。

就是这个!你已经完成了!运行项目app,你已可以为自己构建一些欢闹的句子。

buildrun-final

祝贺 - 你已经完成了构建Mad Lib app,并沿途学习了大量关于最常用macOS控件的知识。

随意玩弄控件,选择不同的值,输入有趣的名称或动词,每次点击 Go! 按钮时来查看你创建了一个多么有趣的故事!:]

从这儿去向哪里?

这里是包含这篇教程中全部代码的 最终项目

为了获得对macOS控件的更深的理解,我推荐你去阅读下表列出的苹果编程指南,它们包含了大量的关于如何使用可用的控件的信息。

特别低,我强烈地推荐你阅读 macOS人机交互指南 。这篇指导解释了关于macOS用户交互的概念和理论,以及苹果对于开发人员如何使用控件和设计他们UI的期望,来提供一致和令人愉快的体验。任何想要为Mac平台进行开发的人都应当去阅读一下,尤其计划要通过Mac App Store发布它们的应用的时候。

这里有一些有用的链接来增强,或更进一步地探索你在这篇教程中学到的概念: