若要在 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 介面擴充時更容易診斷問題。

來源