若要在 Mac 視窗中顯示 Foundation 陣列,請先驗證其元素類型,再為每個值建立檢視。將 NSArray 橋接為 [String] 可取得字串;若要顯示這些字串,還需要另外轉換成 AppKit 標籤。
本逐步教學適用於要在自己的 macOS App 中加入小型清單的開發者。具體任務是在垂直堆疊中顯示三個合成名稱:Circle、Square 和 Triangle。範例會明確說明集合類型、元素類型、版面配置和物件生命週期。
橋接集合實際上會改變什麼?
Apple 將 Array 和 NSArray 說明為對應的 Swift 與 Foundation 類型,就像 String 和 NSString 彼此對應一樣。Apple 建議一般 Swift 程式碼使用 Swift 值類型,並在需要 Foundation 參考型別的行為時才使用它們。在這個任務中,請在輸入邊界保留 Foundation 集合,之後改用具型別的 Swift 陣列。Working with Foundation Types
集合的外層類型和元素類型回答的是不同問題。NSArray 表示有序的物件集合。[String] 則增加了一項具體要求:每個元素都必須能作為 Swift 字串使用。這兩種宣告都不代表檢視集合。Apple 的堆疊初始化器明確要求 [NSView]。NSArray、init(views:)
| 階段 | 此範例中的類型 | 契約 |
|---|---|---|
| Foundation 輸入 | NSArray |
包含提供的合成物件 |
| 已驗證的模型 | [String] |
每個元素都符合字串要求 |
| 呈現物件 | [NSView] |
每個元素都是新建立的文字標籤 |
| 版面配置容器 | NSStackView |
根據其設定排列這些檢視 |
關鍵在於區分橋接和建構。橋接會連結受支援的表示形式,例如 Foundation 字串和 Swift 字串。建構 NSTextField 物件則會建立具有自身屬性和生命週期的呈現物件。沒有文件記載的字串至標籤橋接方式;受支援的做法是對每個字串呼叫標籤初始化器。Array、init(labelWithString:)
如何驗證元素而不丟棄項目?
當需要檢查輸入集合的元素契約時,請使用條件式轉型。Apple 說明 as? 會產生可選值結果,而使用 as! 強制轉型失敗時會觸發執行階段錯誤。在建立介面之前,請明確處理失敗情況。Handling Dynamically Typed Methods and Objects in Swift
以下是使用合成輸入的完整 Foundation 專用檢查。將它儲存為 BridgeCheck.swift,並在裝有 Swift 工具的 Mac 上執行 swift BridgeCheck.swift。其中的斷言表達了預期結果。
import Foundation
let source: NSArray = [
NSString(string: "Circle"),
NSString(string: "Square"),
NSString(string: "Triangle")
]
let names = source as? [String]
assert(names == ["Circle", "Square", "Triangle"])
let mixed: NSArray = [NSString(string: "Circle"), NSNumber(value: 7)]
assert((mixed as? [String]) == nil)
let empty: NSArray = []
assert((empty as? [String]) == [])
print("Collection checks passed.")
第一次轉型要求陣列中的元素都是字串,因此在取得具型別集合時會一併檢查必要的元素類型。混合輸入不符合這項契約;空輸入則符合,因為其中沒有不相容的元素。這些是針對所選輸入契約的示範檢查,不是測量結果或應用程式執行結果。
請勿只為了讓混合集合通過檢查,就改用 compactMap { $0 as? String }。Apple 將 compactMap 定義為只傳回非 nil 的轉換結果。在此範例中,這種做法會丟棄數字,顯示不完整的清單。若明確要求篩選,篩選可能合適;但此處的任務要求保留所有提供的項目。compactMap(_:)
驗證成功後,使用 map 為每個名稱建立一個標籤。Apple 說明 map 會對序列中的元素套用轉換,而 NSTextField(labelWithString:) 會建立靜態、不換行且文字不可選取的標籤。這讓該轉換適用於這三個短名稱。map(_:)、init(labelWithString:)
如何建構完整的 Mac 視窗?
這個獨立範例需要 macOS 14 或更新版本,以及已安裝的 Xcode 命令列工具。它以程式碼建立介面,因此不需要 storyboard、nib、資產或其他來源檔案。macOS 14 是範例使用 NSApplication.activate() 的必要條件;此方法會要求啟用,但不保證一定啟用。activate()
將下方完整程式儲存為 CollectionDemo.swift。程式刻意包含自己的進入點。若要將它改用於既有 App,請沿用該 App 已有的進入點和生命週期,不要再新增一個。
import Foundation
import AppKit
@MainActor
final class DemoDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow?
func applicationDidFinishLaunching(_ notification: Notification) {
let source: NSArray = [
NSString(string: "Circle"),
NSString(string: "Square"),
NSString(string: "Triangle")
]
guard let names = source as? [String] else {
print("Expected a collection containing only strings.")
NSApplication.shared.terminate(nil)
return
}
let labels: [NSView] = names.map { name in
NSTextField(labelWithString: name)
}
let stack = NSStackView(views: labels)
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
let content = NSView(frame: NSRect(
x: 0, y: 0, width: 360, height: 180
))
content.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(
equalTo: content.leadingAnchor, constant: 20
),
stack.topAnchor.constraint(
equalTo: content.topAnchor, constant: 20
),
stack.trailingAnchor.constraint(
lessThanOrEqualTo: content.trailingAnchor, constant: -20
),
stack.bottomAnchor.constraint(
lessThanOrEqualTo: content.bottomAnchor, constant: -20
)
])
let window = NSWindow(
contentRect: content.frame,
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: false
)
window.title = "Synthetic Shapes"
window.isReleasedWhenClosed = false
window.contentView = content
self.window = window
window.center()
window.makeKeyAndOrderFront(nil)
NSApplication.shared.activate()
}
func applicationShouldTerminateAfterLastWindowClosed(
_ sender: NSApplication
) -> Bool {
true
}
}
@main
@MainActor
enum CollectionDemo {
static let appDelegate = DemoDelegate()
static func main() {
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.delegate = appDelegate
app.run()
}
}
在包含該檔案的目錄中,從終端機編譯並啟動:
xcrun swiftc -parse-as-library CollectionDemo.swift -o CollectionDemo
./CollectionDemo
使用提供的輸入時,預期結果是標題為 Synthetic Shapes 的視窗,並依原始順序包含三個標籤。關閉視窗會要求應用程式終止。範例從 Apple 提供的委派方法傳回 true,以實現此行為。applicationShouldTerminateAfterLastWindowClosed(_:)
這些範例已檢視,但未在此處編譯或執行:研究環境沒有 Swift 編譯器,也沒有 macOS AppKit。整合範例前,請在目標 Mac 上執行 Foundation 檢查並檢視視窗。
哪些版面配置和生命週期契約必須維持?
堆疊初始化器記載的預設值很重要:它會建立水平堆疊、將提供的檢視放在前導重力區域,並停用堆疊的 autoresizing mask 轉換。範例會明確將方向改為垂直,並將對齊方式設為前導。陣列轉型無法設定這些版面配置參數。init(views:)、alignment
堆疊會管理其中檢視的排列方式。堆疊和外層內容檢視之間的限制條件,則決定該排列在視窗中的位置。此處的前導和頂端限制條件設定 20 點的邊距;尾端和底部的不等式保留空間,不要求短清單填滿內容區域。Apple 說明堆疊檢視會使用 Auto Layout,並解釋停用 autoresizing mask 轉換後,就能自行提供限制條件。NSStackView、translatesAutoresizingMaskIntoConstraints
這是為三個短標籤設計的固定大小示範。若文字較長、列數較多或介面可調整大小,擴充範例前需要另行決定版面配置方式,例如換行或捲動。成功橋接較大的集合,並不能保證其呈現方式能容納所有內容。
物件生命週期也與型別轉換無關。Apple 將應用程式的 delegate 屬性宣告為 weak。因此,靜態的 appDelegate 屬性提供明確的強參考持有者。delegate 也會保留視窗,而視窗將 isReleasedWhenClosed 設為 false,遵循 Apple 對 Swift 和 ARC 用戶端的指引。delegate、isReleasedWhenClosed
介面是在 applicationDidFinishLaunching 中建構的。Apple 將它說明為主事件迴圈啟動後、開始處理事件前,進行後續初始化的時機。程式的進入點會呼叫 run() 以啟動該事件迴圈。將轉換移入既有 AppKit App 時,請保留這些職責。applicationDidFinishLaunching(_:)、run()
這些標籤也代表已建構的快照。範例只會為每個標籤指定一次文字。之後若變更來源集合,就需要明確編寫程式碼來更新標籤或重建已排列的檢視;橋接和映射操作不會建立持續連動的關係。
如何快速找出容器與元素混淆的錯誤?
依照下列順序找出未符合的契約。此流程會將型別驗證與呈現及生命週期檢查分開,因此遇到版面配置問題時,不必重寫已成功的橋接。
| 症狀 | 檢查項目 | 建議的下一步 |
|---|---|---|
轉型為 [String] 後得到 nil |
每個輸入元素都符合字串要求嗎? | 明確拒絕此集合,或定義有意的轉換規則。 |
正將整個陣列轉型為 String |
目標需要的是單一值還是集合? | 驗證 [String];只有在任務確實需要時才選取單一元素。 |
| 堆疊初始化器拒絕引數 | 引數是 [String],而非 [NSView] 嗎? |
先將每個字串映射成標籤。 |
| 標籤水平排列 | 是否已設定堆疊方向? | 將此清單設為 .vertical。 |
| 檢視位置錯誤或限制條件互相衝突 | 外層檢視的限制條件與 autoresizing mask 設定是否一致? | 將版面配置與輸入轉型分開檢查。 |
| 未收到 delegate 回呼 | 是否有強參考持有者保留 delegate? | 將 delegate 保留到設定 weak 屬性之後。 |
| 後續模型變更未顯示 | 是否有明確的更新程式碼? | 明確更新標籤值或已排列的檢視。 |
對於此任務,可靠的流程是:驗證一次 Foundation 輸入、保留得到的具型別模型、為每個值建立一個標籤,並明確設定堆疊和 App 的生命週期。每個步驟都有清楚的契約,讓 Mac 介面擴充時更容易診斷問題。
來源
Working with Foundation Types — Apple
NSArray — Apple
Array — Apple
Handling Dynamically Typed Methods and Objects in Swift — Apple
compactMap(_:) — Apple
map(_:) — Apple
init(labelWithString:) — Apple
NSStackView — Apple
init(views:) — Apple
alignment — Apple
delegate — Apple
isReleasedWhenClosed — Apple
run() — Apple
activate() — Apple