Чтобы отобразить массив Foundation в окне Mac, проверьте типы его элементов, а затем создайте представление для каждого значения. Преобразование NSArray в [String] даст вам строки; для их отображения нужно отдельно преобразовать их в метки AppKit.

Это руководство предназначено для разработчика, который добавляет небольшой список в собственное приложение macOS. Конкретная задача — отобразить три вымышленных названия фигур: Circle, Square и Triangle — вертикальным списком. В примере явно заданы типы коллекции и элементов, компоновка и время жизни объектов.

Что именно меняется при преобразовании коллекции?

Apple указывает, что Array и NSArray — соответствующие друг другу типы Swift и Foundation, как и String с NSString. В обычном коде 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 и запустите его на Mac с установленными инструментами Swift командой 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. Интерфейс создаётся программно, поэтому не нужны раскадровка, 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, в котором три метки расположены в исходном порядке. Закрытие окна запрашивает завершение приложения. Для этого в примере возвращается true из метода делегата, предусмотренного Apple. applicationShouldTerminateAfterLastWindowClosed(_:)

Эти примеры были проверены, но здесь не компилировались и не запускались: в исследовательской среде нет ни компилятора Swift, ни macOS AppKit. Перед интеграцией примера выполните проверки Foundation и изучите окно на целевом Mac.

Какие условия компоновки и жизненного цикла нужно сохранить?

Важны задокументированные значения по умолчанию инициализатора стека: он создаёт горизонтальный стек, помещает переданные представления в область ведущего выравнивания и отключает преобразование маски автоматического изменения размера стека. В примере ориентация явно задана как вертикальная, а выравнивание — по ведущему краю. Приведение массива не задаёт ни одного из этих параметров компоновки. init(views:), alignment

Стек управляет расположением содержащихся в нём представлений. Ограничения между стеком и окружающим его содержимым задают положение стека в окне. Здесь ограничения по ведущему и верхнему краям задают отступ в 20 пунктов; нестрогие ограничения по правому и нижнему краям оставляют пространство, не требуя, чтобы короткий список занимал всю область содержимого. Apple указывает, что стековые представления используют Auto Layout, и поясняет: отключение преобразования маски автоматического изменения размера позволяет задавать собственные ограничения. NSStackView, translatesAutoresizingMaskIntoConstraints

Это демонстрация фиксированного размера с тремя короткими метками. Для более длинного текста, большого числа строк или изменяемого размера интерфейса сначала потребуется отдельное решение по компоновке, например перенос текста или прокрутка. Успешное преобразование типов для более крупной коллекции не гарантирует, что она поместится при отображении.

Время жизни объектов также не связано с преобразованием типов. Apple объявляет свойство делегата приложения как weak. Поэтому статическое свойство appDelegate служит явным владельцем, который сильно удерживает объект. Делегат также удерживает окно, а для окна установлено isReleasedWhenClosed со значением false, как рекомендует Apple для клиентов Swift и ARC. delegate, isReleasedWhenClosed

Интерфейс создаётся в applicationDidFinishLaunching. Согласно документации Apple, этот метод подходит для дополнительной инициализации после запуска основного цикла и до обработки им событий. Точка входа программы вызывает run(), чтобы запустить этот цикл обработки событий. При переносе преобразования в существующее приложение AppKit сохраните эти обязанности. applicationDidFinishLaunching(_:), run()

Метки также являются созданным снимком данных. В примере текст каждой метки задаётся один раз. Если позже изменить исходную коллекцию, потребуется явно обновить метки или заново создать упорядоченные представления; преобразование типов и операция отображения не создают постоянной связи.

Как быстро найти ошибку между контейнером и элементом?

Воспользуйтесь этой последовательностью, чтобы определить, какое условие нарушено. Она отделяет проверку типов от отображения и жизненного цикла, поэтому проблема с компоновкой не заставит заново переписывать успешно выполненное преобразование.

Симптом Проверка Подходящий следующий шаг
Приведение к [String] возвращает nil Соответствует ли каждый входной элемент требованию к строке? Явно отклоните коллекцию или задайте правила преобразования.
Весь массив приводится к String Нужен ли в результате один элемент или коллекция? Проверьте [String]; выбирайте отдельный элемент, только если этого действительно требует задача.
Инициализатор стека отклоняет аргумент Передан ли аргумент [String], а не [NSView]? Сначала преобразуйте каждую строку в метку.
Метки расположены горизонтально Задана ли ориентация стека? Для этого списка установите .vertical.
Представления размещены неправильно или ограничения конфликтуют Согласованы ли ограничения окружающего представления и параметры маски автоматического изменения размера? Проверяйте компоновку отдельно от приведения входных данных.
Вызовы делегата не приходят Удерживает ли объект-владелец делегат сильной ссылкой? Удерживайте делегат в памяти после присваивания слабому свойству.
Последующие изменения модели не отображаются Написан ли код явного обновления? Обновляйте значения меток или упорядоченные представления явно.

Для этой задачи надёжный подход таков: один раз проверить входную коллекцию Foundation, сохранить полученную типизированную модель, создать по метке на каждое значение и явно настроить стек и время жизни приложения. У каждого этапа есть понятное условие, поэтому по мере развития интерфейса Mac ошибки проще диагностировать.

Источники