到這裡,我們才開始真正的學習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的菜單。

讓我們在這裡,遇見明天的自己!姜友華

推薦閱讀:

相关文章