要在 Mac 窗口中显示 Foundation 数组,请先验证其元素类型,再为每个值创建一个视图。将 NSArray 桥接为 [String] 可得到字符串;要显示它们,还需单独转换为 AppKit 标签。

本指南面向要在自己的 macOS 应用中添加一个小列表的开发者。具体任务是在垂直堆栈中显示三个虚构名称:Circle、Square 和 Triangle。示例明确说明集合类型、元素类型、布局和对象生命周期。

集合桥接实际上会改变什么?

Apple 将 Array 和 NSArray 记述为相对应的 Swift 与 Foundation 类型,正如 String 和 NSString 相对应一样。Apple 建议在常规 Swift 代码中使用 Swift 值类型,并在需要 Foundation 引用类型行为时访问它们。对于此任务,请将 Foundation 集合保留在输入边界,之后使用类型明确的 Swift 数组。使用 Foundation 类型

集合的外层类型和元素类型回答的是不同问题。NSArray 表示一个有序对象集合。[String] 则增加了一项具体要求:每个元素都必须能用作 Swift 字符串。这两种声明都没有描述视图集合。Apple 的堆栈初始化器明确要求 [NSView]。NSArray、init(views:)

阶段 此示例中的类型 约定
Foundation 输入 NSArray 包含提供的虚构对象
已验证模型 [String] 每个元素都满足字符串要求
展示对象 [NSView] 每个元素都是新创建的文本标签
布局容器 NSStackView 根据自身配置排列这些视图

需要区分的是桥接和构造。桥接连接受支持的表示形式,例如 Foundation 字符串和 Swift 字符串。构造 NSTextField 对象会创建具有自身属性和生命周期的展示对象。没有文档说明存在从字符串到标签的桥接;受支持的做法是为每个字符串调用标签初始化器。Array、init(labelWithString:)

如何验证元素而不丢弃项目?

当需要检查传入集合的元素约定时,请使用条件转换。Apple 说明 as? 会生成可选结果,而使用 as! 强制转换失败会触发运行时错误。构建界面之前,请明确处理失败情况。在 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。它特意包含了自己的入口点。将它改编到现有应用时,请使用该应用已有的入口点和生命周期,不要再添加一个入口点。

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 检查并查看窗口,再集成此示例。

哪些布局和生命周期约定必须保持?

堆栈初始化器文档中记载的默认值很重要:它会创建水平堆栈,将所提供的视图放在其 leading 重力区域,并禁用堆栈的自动调整大小遮罩转换。示例会显式将方向改为垂直,并将对齐方式设为 leading。数组转换无法选择这些布局设置中的任何一项。init(views:)、alignment

堆栈负责排列其中包含的视图。堆栈与其外层内容视图之间的约束会确定该排列在窗口中的位置。这里,leading 和 top 约束设置了 20 点的内边距;trailing 和 bottom 不等式则留出空间,不要求短列表填满内容区域。Apple 说明堆栈视图使用 Auto Layout,并解释禁用自动调整大小遮罩转换后,就可以自行提供约束。NSStackView、translatesAutoresizingMaskIntoConstraints

这是一个用于三个短标签的固定尺寸演示。若要显示更长文本、更多行或可调整大小的界面,需要在扩展示例之前单独决定布局方式,例如换行或滚动。成功桥接一个更大的集合,并不能保证其展示内容放得下。

对象生命周期与类型转换同样是两回事。Apple 将应用程序的委托属性声明为弱引用。因此,静态的 appDelegate 属性提供了一个明确的强引用所有者。委托也会保留窗口,并将 isReleasedWhenClosed 设为 false,遵循 Apple 针对 Swift 和 ARC 客户端的指导。delegate、isReleasedWhenClosed

界面在 applicationDidFinishLaunching 中构建。Apple 说明,该方法会在主循环启动之后、开始处理事件之前调用,适合进行进一步初始化。程序的入口点调用 run() 来启动事件循环。将转换移入现有 AppKit 应用时,请保留这些职责。applicationDidFinishLaunching(_:)、run()

这些标签也代表一个已构建的快照。示例为每个标签的文本赋值一次。之后更改源集合,需要编写明确的代码来更新标签或重建已排列的视图;桥接和映射操作不会建立持续连接。

如何快速定位容器与元素混淆的问题?

按以下顺序找出哪个约定未满足。此顺序将类型验证与展示及生命周期检查分开,因此布局问题不会让你回头重写已经成功的桥接代码。

症状 检查项 受支持的后续步骤
转换为 [String] 后得到 nil 每个输入元素都满足字符串要求吗? 明确拒绝该集合,或定义有意采用的转换策略。
正在将整个数组转换为 String 目标需要单个值还是一个集合? 验证 [String];只有任务确实需要单个元素时才选择一个。
堆栈初始化器拒绝该参数 参数是 [String],而不是 [NSView] 吗? 先将每个字符串映射为一个标签。
标签呈水平排列 是否配置了堆栈方向? 对此列表设置为 .vertical。
视图位置错误或约束冲突 外层视图约束和自动调整大小遮罩设置是否一致? 独立检查布局,不要将问题归咎于输入转换。
没有收到委托回调 是否有强引用所有者保留委托? 赋值给弱引用属性后,仍需让委托保持存活。
后续模型更改未显示 是否编写了明确的更新代码? 有意更新标签值或已排列的视图。

对于此任务,可靠的做法是验证一次 Foundation 输入,保留由此得到的类型明确模型,为每个值构造一个标签,并明确配置堆栈和应用生命周期。每个步骤都有清晰的约定,因此随着 Mac 界面扩展,问题会更容易诊断。

来源