到这里,我们才开始真正的学习macOS程序开发。

macOS程序是桌面应用程序,幸运的是这类程序我们见的用的太多,所以能让我们容易地去理解它。桌面应用程序分两个部分:即界面层和逻辑层。逻辑层又分两部分,业务逻辑层与数据逻辑层。其中逻辑层的实现,大部分可以依赖我们前面所学的Swift知识,那界面层呢?只好现在从头开始学了。程序界面的作用,无外乎是提供了一种人机交互方式。所以界面层的实现也可以分两部分:首先是页面布局的设计,即我们所看到的;其次是功能的应用,即我们所操作的。Xcode是一个功能强大且易于使用的编辑器。它提供了两种方式用来处理界面层:Storyboard与Swift,Storyboard能让我们做到所见即所得,同时能与Swift合用,使得开发变得很轻松。所以,大约来说,前者简单后者有效。

恭喜你,从本章开始,你可以看到具体的运行程序了。每一次程序的成功运行,是我们再一次在军事演习上的胜利,再一次人生的在进步。好吧,让辛苦留住嘴角的微笑,让努力带走额头的山川,我们开始吧。

请留意,此后章节有大量的图片,请留心你的流量,我可没有钱陪给你。还有,从这一章开始,不再区分有无经验,只概述与正文两部分,你需要查看概述确定你有无需要阅读本章内容。

????? 概述 ?????

我们将使用Storyboard、Swift相互协作的方式来设计界面。Xcode编辑器会为我们提供了大量的组件,用来设计界面。也就是说,在大多数情况下,我们不需要自己来设计组件就能完成程序开发。这些组件是例如按钮,文本输入框,表格这一类的东西。每个组件只跟定位、数据、样式、事件有关,当你了解了单个组件,你就大约学会了界面设计:

  1. 定位,即组件在页面中的位置。可使用相对或绝对数据来确定其位置,并能为其添加约束等限制。
  2. 样式,即组件最终呈现的模样。有两种方式确定其样式:其一,为属性赋值;其二,通过实现委托的方法进而确定组件是如何显示数据。
  3. 数据,即组件需要呈现的内容。有三种方式为组件提供数据:其一,为属性赋值;其二,通过实现委托的方法来为其提供数据;其三,Cocoa数据绑定。
  4. 事件,即组件对事件的接收与处理。是通过实现委托方法来确定将要监听和处理某操作。

下面我们将从上述的四个方面来学习如何设计程序界面。

????? 正文 ?????

一、建立一个名为「iWriter」的macOS工程。

在开始之前,我们需要建立一个macOS工程,就叫它:「iWriter」。

  1. 启动Xcode,在「Welcome to Xcode」页面,点击左侧第二项「Create a new Xcode project」。
  2. 在「Choose a template for your new project」页面,选择顶部菜单项「macOS」,选择第一栏「Application」中第一项「Cocoa App」后,点击「Next」。
  3. 在「Choose options for your new project」页面,「Product Name」项填入「iWriter」,「Language」项选择「Swift」,勾选其下的「Use Storyboards」,勾选最下的「Include Unit Tests」及「Include UI Tests」,然后点击「Next」。注意:Team、Organization Name、Organization Identifier跟你的苹果开发账号有关,如有需要,请参考forgot2015的苹果 iOS 开发者公司账号申请流程。
  4. 在跳出的文件流览框里,选择工程存放的位置后,点击「Creat」,创建完成了「iWrter」项目。

二、先了解一下Xcode界面:

有了macOS工程之后,我们还需要了解Xcode的自身的布局、各区名称及其作用。这里需要我们用一下心,因为以下章节都需用到,同时到了下一章,我们只提各区及按钮的英文名,你需要知道我们讲的是那个位置和该如何操作。如Size栏,就是Inspector Area(检查区)的Size(尺寸)栏等。

  • 界面的分区及功能按钮,有图参考:
  1. Run or Stop(运行、停止)按钮,启动或停止当前项目程序
    1. 左,点击,编译并运行当前方案。可以理解为,为当前程序运行一个实例;
    2. 右,点击,关闭正在运行的方案或程序,可以理解为,关闭当前程序的运行实例。
  2. Library(库)按钮,打开库窗口
    1. 编辑区为Swift时,显示大括弧图标,点击跳出代码库窗口;
    2. 编辑区为Storyboard时,显示方圆图标,点击显示组件库窗口。
  3. Editor Control(编辑区控制)按钮,切换编辑区模式
    1. 左,Standard Editor(基本编辑器),为Storyboard或Swift提供单一区域;
    2. 中,Assistant Editor(助理编辑器),为Storyboard或Swift提供双区域,当Storyboard与Swift以左右分区显示时,方便Storyboard组件关联到Swift的ViewController中;
    3. 右,Version Editor(版本编辑器),为Swift提供前后两个版本的代码比较。
  4. Area Control(大区控制)按钮,显示或隐藏非编辑区域
    1. 左,显示或隐藏Navigator Area(导航区);
    2. 中,显示或隐藏Debug Area(调试区);
    3. 右,显示或隐藏Inspectors Area(检查区)。
  5. Navigator Area(导航区),从左到右依次为:
    1. Project(项目)栏,为项目文件提供分群组导航;
    2. Source Control(代码管理)栏,为代码分支、标签、远程提供导航;
    3. Symbol(标识)栏,为代码按属性、方法提供导航;
    4. Find(查找)栏,为工程按字元串查找提供导航;
    5. Issue(问题)栏,为工程按编译、运行的问题提供导航,区分警示(黄)与错误(红);
    6. Test(测试)栏,为单元、界面测试提供导航;
    7. Debug(调试)栏,为调试时线程节点提供导航;
    8. Breakpoint(断点)栏,为设置的所有断点提供导航;
    9. Report(报告)栏,为每次编译与运行的信息提供导航。
  6. Editor Area(编辑区),有Storyboard、Swift、Form等等编辑方式:
    1. 上,面包屑(文件链)栏,可快速切换各级文件;
    2. 下,操作区,有Storyboard、Swift、Form编辑方式。
  7. Inspector Area(检查区),后六项在Storyboard中选择组件后激活,从左到右依次为:
    1. File(文件)栏,当前文件信息,可设置;
    2. Help(帮助)栏,为所选类提供帮助文档;
    3. Identity(身份)栏,组件身份,在组件关联后,可使用;
    4. Attribute(属性)栏,当前组件或控制器属性信息,可设置。
    5. Size(尺寸)栏,当前组件或控制器尺寸信息,可设置。
    6. Connection(关联)栏,当前组件或控制器关联到代码中的属性、事件等信息,可设置。
    7. Binding(绑定)栏,数据Cocoa绑定。
    8. Effect(效果)栏,当前组件或控制器的整体效果信息,可设置。
  8. Debug Area(调试区),调试时显示运行中数据:
    1. 左,运行时,提供当前对象、属性、变数值;
    2. 右,运行时,输出运行错误。同时可通过po命令输出指定对象、属性、变数、方法的值。
  • Editor Area(编辑区)在Storyboard时,特有的区域、按钮说明:
  1. Ducoment Outline(文档大纲),可查看编辑区视图层级结构;
  2. MacOS Appearance(MacOS 模式),应用在MacOS模式下,浅色模式与深色模式的显示效果;
  3. Layout(定位)按钮及其它,从左到右:
    1. Update Frames(更新框架),用于修正自动布局中黄色警告;
    2. Embed In(嵌入),将组件嵌入到跳出窗口中选择的视图中;
    3. Align(对齐),对齐多个组件或视图;
    4. Add New Constraints(约束),四边约束、长宽约束、等宽等高约束、等宽高比约束;
    5. Resolve Auto Layout Issues(解决自动布局问题)。
  4. Autoresizing(自动布局),Inspector Area(检查区)Size(尺寸)栏中的设置项,可实现约束功能。

三、开始为View Controller添加组件,数据方式为:属性赋值。

在设计界面时,如对组件不熟悉,建议使用Storyboard进行设计,然后以Outlet属性关联到View Controller中。

  • 我们将在Storyboard里,添加一个Button(按钮)。记住,所有组件的添加到Storyboard的方式均与之相同,后面不再重复。
  1. 点击Navigator Area(导航区)的Project(项目)栏。
  2. 在iWriter项目下iWriter群组里,找到Main.storyboard项。点击它,确定Editor Area(编辑区)切换到Storyboard
  3. Library(库)按钮此时显示方圆图标,点击Library(库)按钮,跳出库窗口。
  4. 库窗口上方有搜索框,可搜索。右上角有显示模式的按钮,点击可以切换组件的显示方式。点击切换到图文显示方式,这样我们可以看到各组件的样式及其相关说明。
  5. 选择Push Button组件,并将其拖到最下一个视图控制器。拖到视图控制器时,移动并留意出现的参考线,选择其居中后放置。

定位。

  1. 在Storyboard的ViewController里,选择组件Push Button;
  2. 在Storyboard的Layout(定位)里,点击Align(对齐)按钮,跳出对齐窗口。勾选水平、垂直约束,并设置约束值均为0,确认添加两个约束。表示Push Button组件将在ViewController里,水平居中和垂直居中;
  3. 打开Inspector Area(检查区)中Size(尺寸)栏,我们可以看见新添加两个约束。点击其右边「Edit」可重新编辑其约束值。

样式。

  1. 点击Inspector Area(检查区)的Attribute(属性)栏。
  2. 在Font项上,点击「T」图标,跳出窗口,选择其中的Size项,输入「24」后确认。
  3. 查看ViewController,「Button」组件的字体明显变大。

数据。

  1. 点击Inspector Area(检查区)的Attribute(属性)栏。
  2. 在Title项上,输入「Click Me」后确认。
  3. 查看ViewController,「Button」组件的字元改为「Click Me」。

事件。

首先,将组件作为Outlet属性关联至ViewController(视图控制器)的类属性里。

  1. 点击Editor Control(编辑区控制)的中间按钮,切换Editor Area(编辑区)为Assistant Editor(辅助编辑器)。
  2. 保证左区打开Main.storyboard,右区打开ViewController.swift。可通过各区顶部的面包屑来打开。左边为iWriter->iWriter->Main.storyboard->Main.storyboard(Base),右边为Automatic->ViewController.swift。
  3. 在左区,按住键盘的「control」键,选择并拖移「Button」组件到右区。
  4. 放置右区的类的属性区域。
  5. 跳出关联窗口,在Connection项选择Outlet,表示作为以Outlet属性关联到代码。在Name栏输入「SingleButton」,点击Connection,完成属性关联。
  6. 查看ViewController.swift代码,类里有一个叫「SingleButton」的属性,并被标记为Outlet。
  7. 查看Inspector Area(检查区)的Connection(关联)栏,多了一条关联的信息。

其次,将组件作为Action方法关联至ViewController(视图控制器)的类方法里。

  1. 方法如上,需要在第5步,跳出的关联窗口里,将Connection项选择Action,Name项输入「clickButton」,最终显示如图。
  2. 查看Inspector Area(检查区)的Connection(关联)栏里,多了一条关联信息。
  3. 然后在新加的「clickButton」方法里添加下列代码:

let number:Int = Int(arc4random() % 1000);
self.singleButton.title = "(number)"

最后,点击Run and Stop(运行、停止)中的左按钮,编辑运行当前方案。这是我们开发的第一个程序,酷!

好了,我们需要学习删除组件。让我们来删除该按钮。先删除Inspector Area(检查区)的Connection(关联)栏里的关联项,再删除相关代码和该组件。如没有删除组件在Connection(关联)栏里的关联项,而先删除组件,会出现编译错误。

  1. 删除关联。点击Editor Control(编辑控制区)中Assistant Editor(助理编辑器),确定Editor Area(编辑区)左为Storyboard编辑,右为Swift编辑。在Storyboard编辑里,选择组件;在Inspector Area(检查区)打开Connection(关联)栏;关闭Action、Outlet关联项。
  2. 删除代码。在Swift里,删除前面有灰圈的代码块。
  3. 删除组件。在Storyboard里,删除该组件。

三、再来一个复杂的组件,数据方式:实现委托。

  • 我们为ViewController添加一个Outline View(大纲视图)组件,该步与上述相同。

定位

  1. 选择Outline View组件,点Editor Area右下角第四图标Constraints(约束)按钮,跳出Constraints(约束)窗口。将上下左右约束值各输入20,确认添加四条约束。表示Outline View(大纲视图)组件离ViewController四周的距离固定。
  2. 查看Editor Area(编辑区),发现Outline View(大纲视图)组件的尺寸与位置均发生了变化。
  3. 查看Inspector Area(检查区)的Size栏,发现新添加了4条约束。点击其右侧Edit,可修改约束值。

样式

  1. 在Editor Area(编辑区)里,点击左下角图标,打开Document Outline(文档大纲)栏;
  2. 选择View Controller Scene档中的Outline View项;
  3. 在Inspector Area(检查区)的Attribute(属性)栏中,可设置栏目数量、是否显示表头等。取消Table View档中的Headers项的勾选,表示Outline View(大纲视图)组件不显示表头;
  4. 选择Document Outline(文档大纲)栏中Table Column项;
  5. 在Storyboard编辑区选择Column的右边中点,拖拉控制其宽度;
  6. 在Inspector Area(检查区)的Size(属性)栏中输入width项的数值,可设计其宽度。

数据与事件

像我们这样的初学者一般建议通过Storyboard添加组件。为什么呢?从我们刚才为Storyboard添加的Outline View组件中可以看出,其Outline View组件被嵌入到一个Clip View中。如果想在代码添加NSOutlineView(Outline View对应的类)实例,由于没有嵌入到Clip View中,结果是无法在ViewController中显示出来的。

首先,为引用的组件添加Identifier

  1. 在Editor Area(编辑区)里,选择Document Outline(文档大纲)栏的第一个Table Column项;
  2. 在Inspector Area(检查区)的Identity(身份)栏中,改Identifier项的值为leftColumn。
  3. 同理改第二个Table Column的Identifier为rightColumn,改第一个Table Cell View为leftCell,改第二为rightCell;

其次,将组件以Outlet属性关联到ViewController的类代码中。

  1. 切换Editor Area(编辑区)为Assistant(辅助)模式,左为Main.storyboard,右为ViewController.swift。选择Document Outline(文档大纲)栏的Outline View项。注意,上下级都没用,这也是所以不能直接将Storyboard里的Outline View组件拖入的原因。
  2. 按住键盘的「control」键,将Outline View项拖入ViewController.swift的类的属性区。并在跳出的窗口中,为Connection项选择Outlet,为name项输入outlineView。最后确定Connection。

最后,为组件添加数据。

  1. 定义一个数据类型。示例中以目录类为数据类型,该设计支持无限级别。
  2. 基于数据类型准备数据。目录数据保存在文件中时用数组形式,数组按级别有序排列,应用时需要将其格式化为目录类的数据。
  3. 应用Outline View组件到ViewController中,需要实现其DataSource及Delegate两类委托。
  4. 实现其DataSource委托,通过查看其定义,确定需要实现其中三个方法。
  5. 实现其Delegate委托,通过查看其定义,确定需要实现其中一个方法。

//
// ViewController.swift
// iWriter
//
// Created by Jiangyouhua on 2019/6/27.
// Copyright ? 2019 Jiangyouhua. All rights reserved.
//

import Cocoa

/** 1. 定义目录类。**/
class Catalog{
var title: String
var page: Int
var sub: [Catalog]
init (title: String, page:Int){
self.title = title
self.page = page
self.sub = [Catalog]()
}
}

/** 2. 准备数据。 **/
class CatalogItem{
static func bookCatalog()->[Catalog]{
var catalogs = [Catalog]()
let array = [
["level":0, "title":"第一章", "page": 1],
["level":1, "title":"第一章, 第一节", "page": 1],
["level":1, "title":"第一章, 第二节", "page": 10],
["level":1, "title":"第一章, 第三节", "page": 13],
["level":0, "title":"第二章", "page": 15],
["level":1, "title":"第二章, 第一节", "page": 15],
["level":1, "title":"第二章, 第二节", "page": 19],
["level":1, "title":"第二章, 第三节", "page": 23],
["level":0, "title":"第三章", "page": 26],
["level":1, "title":"第三章, 第一节", "page": 26],
["level":1, "title":"第三章, 第二节", "page": 30],
["level":1, "title":"第三章, 第三节", "page": 33]
]

var temp = Dictionary<Int, Catalog>()
for dic in array{
if let title = dic["title"]! as? String ,
let page = dic["page"]! as? Int,
let level = dic["level"]! as? Int{
let catalog = Catalog(title: title, page: page)
if level == 0 {
catalogs.append(catalog)
}else{
if let c = temp[level - 1]{
c.sub.append(catalog)
}
}
temp[level] = catalog
}
}
return catalogs
}
}

/** 3. 在ViewController中应用Outline View **/
class ViewController: NSViewController {

@IBOutlet weak var outlineView: NSOutlineView! // Outline View 组件
var catalogs: [Catalog]! // Outline View 的数据

override func viewDidLoad() {
super.viewDidLoad()

self.catalogs = CatalogItem.bookCatalog()
outlineView.dataSource = self // 数据源委托
outlineView.delegate = self // 事件委托
// Do any additional setup after loading the view.
}

override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}

}

/** 4. 实现数据源委托 **/
extension ViewController: NSOutlineViewDataSource {
// 顶级元素或子元素数量。
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let catalog = item as? Catalog {
return catalog.sub.count
}
return catalogs.count
}

// 顶级元素或子元素数据。
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any{
if let catalog = item as? Catalog {
return catalog.sub[index]
}
return catalogs[index]
}

// 是否有子元素。
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool{
if let catalog = item as? Catalog {
return catalog.sub.count > 0
}
return false
}
}

/** 5. 实现事件委托 **/
extension ViewController: NSOutlineViewDelegate{
// 为各Cell添加数据,需要区别各列,所以在Storyboard中需要为各列及Cell添加Identifier。
func outlineView(_ outlineView: NSOutlineView,
viewFor tableColumn: NSTableColumn?, item: Any) -> NSView?{
var view: NSTableCellView?
if tableColumn?.identifier.rawValue == "leftColumn"{
view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("leftCell"),
owner: self) as? NSTableCellView
if let catalog = item as? Catalog {
if let textField = view?.textField {
textField.stringValue = catalog.title
}
}
}else{
view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("leftCell"),
owner: self) as? NSTableCellView
if let catalog = item as? Catalog {
if let textField = view?.textField {
textField.stringValue = String( catalog.page )
}
}
}

return view
}
}

好了,Build and Run(构建、运行),看看效果,点开各栏后展示如下。

四、最后来个Horizontal Slider组件,数据方式:Cocoa绑定。

  • 我们为ViewController添加Horizontal Slider组件,该步与上上述相同。

定位。

  1. 拖动组件左边,直到左边到左近边缘出现参考线即可。拖动组件右边,直到右边到右近边缘出现参考线即可。
  2. 在Inspector Area(检查区)的Size(尺寸)栏里,点击Autoresizing项外框四条箭关线,确认了上左右边边距,表示Horizontal Slider组件离ViewController的上左右边距固定不变。

数据

  1. Storyboard中选择Horizontal Slider组件;
  2. 在Inspector Area(检查区)的Bindings(绑定)栏中, Value档里Value项下勾选Bind to,使用默认值,Model Key Path项中填入score。表示将Horizontal Slider的值保存到NSUserDefaultsController(系统的用户中)。
  3. 在Inspector Area(检查区)的Bindings(绑定)栏中, Availability档里Enabled项下勾选Bind to,使用默认值,Model Key Path项中填入score。表示启动时Horizontal Slider将从NSUserDefaultsController(系统的用户中)获取值数据。

Build and Run(构建、运行),尝试一下:你将发现Horizontal Slider值位置,就是你上次关闭前的值位置。

另:了解macOS界面,请访问:developer.apple.com/des

今天就学到这,下一章,macOS的菜单。

让我们在这里,遇见明天的自己!姜友华

推荐阅读:

相关文章