Mac 윈도우에 Foundation 배열을 표시하려면 요소 유형을 검증한 다음 각 값에 대한 뷰를 만드세요. 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로 저장하고 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 명령줄 도구가 설치되어 있어야 합니다. 인터페이스를 프로그래밍 방식으로 만들기 때문에 스토리보드, 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 검사를 실행하고 윈도우를 확인하세요.
어떤 레이아웃 및 수명 주기 계약을 유지해야 하나요?
스택 초기화자의 문서화된 기본값은 중요합니다. 수평 스택을 만들고, 제공된 뷰를 앞쪽 정렬 영역에 배치하며, 스택에 대해 자동 크기 조정 마스크 변환을 비활성화합니다. 이 예제는 방향을 세로로, 정렬을 앞쪽으로 명시적으로 변경합니다. 배열 캐스트로는 이러한 레이아웃 설정을 정할 수 없습니다. init(views:), alignment
스택은 내부 뷰의 배치를 관리합니다. 스택과 이를 둘러싼 콘텐츠 뷰 사이의 제약 조건은 그 배치가 윈도우 안에서 어느 위치에 놓일지 정합니다. 여기서는 앞쪽 및 위쪽 제약 조건으로 20포인트 여백을 설정하고, 뒤쪽 및 아래쪽 부등식으로 짧은 목록이 콘텐츠 영역을 채우도록 강제하지 않으면서 공간을 확보합니다. Apple은 스택 뷰가 Auto Layout을 사용한다고 문서화하고, 자동 크기 조정 마스크 변환을 비활성화하면 직접 제약 조건을 지정할 수 있다고 설명합니다. NSStackView, translatesAutoresizingMaskIntoConstraints
이 예제는 짧은 레이블 세 개를 위한 고정 크기 데모입니다. 텍스트가 더 길거나 행이 많거나 인터페이스 크기를 조정할 수 있어야 한다면, 예제를 확장하기 전에 줄 바꿈이나 스크롤 같은 별도의 레이아웃 방식을 결정해야 합니다. 더 큰 컬렉션을 성공적으로 브리징해도 표시 내용이 화면에 맞는다는 보장은 없습니다.
수명도 유형 변환과 별개입니다. Apple은 앱의 델리게이트 속성을 weak로 선언합니다. 따라서 정적 appDelegate 속성이 명시적인 강한 소유자 역할을 합니다. 델리게이트는 윈도우도 유지하며, Apple의 Swift 및 ARC 클라이언트 지침에 따라 윈도우의 isReleasedWhenClosed을 false으로 설정합니다. delegate, isReleasedWhenClosed
인터페이스 생성은 applicationDidFinishLaunching에서 이루어집니다. Apple 문서에 따르면 이 메서드는 메인 루프가 시작된 후 이벤트 처리가 시작되기 전에 추가 초기화를 수행하는 위치입니다. 프로그램의 진입점은 run()을 호출해 이벤트 루프를 시작합니다. 변환 코드를 기존 AppKit 앱으로 옮길 때도 이러한 역할을 유지하세요. applicationDidFinishLaunching(_:), run()
레이블은 생성 시점의 스냅샷이기도 합니다. 이 예제는 각 레이블의 텍스트를 한 번 설정합니다. 이후 원본 컬렉션을 변경해도 레이블이나 정렬된 뷰를 갱신하거나 다시 만들려면 명시적인 코드가 필요합니다. 브리징과 매핑 작업만으로 지속적인 연결이 만들어지지는 않습니다.
컨테이너와 요소를 혼동한 실수를 빠르게 찾으려면 어떻게 해야 하나요?
다음 순서에 따라 어떤 계약이 실패했는지 확인하세요. 유형 검증과 표시 및 수명 주기 검사를 분리하므로, 레이아웃 문제가 있다고 해서 성공한 브리지를 다시 작성할 필요가 없습니다.
| 증상 | 확인할 점 | 지원되는 다음 단계 |
|---|---|---|
[String]로 캐스트하면 nil이 반환됨 |
모든 입력 요소가 문자열 요구 사항을 충족하나요? | 컬렉션을 명시적으로 거부하거나 의도적인 변환 정책을 정의하세요. |
배열 전체를 String으로 캐스트하려고 함 |
대상에 값 하나가 필요한가요, 컬렉션이 필요한가요? | [String]을 검증하고, 작업에 실제로 필요할 때만 요소 하나를 선택하세요. |
| 스택 초기화자가 인수를 거부함 | 인수가 [NSView]이 아니라 [String]인가요? |
먼저 각 문자열을 레이블로 매핑하세요. |
| 레이블이 가로로 표시됨 | 스택 방향을 설정했나요? | 이 목록에는 .vertical을 설정하세요. |
| 뷰의 위치가 잘못되었거나 제약 조건이 충돌함 | 둘러싼 뷰의 제약 조건과 자동 크기 조정 마스크 설정이 일치하나요? | 입력 캐스트와 별개로 레이아웃을 확인하세요. |
| 델리게이트 콜백이 오지 않음 | 강한 소유자가 델리게이트를 유지하고 있나요? | weak 속성에 할당한 뒤에도 델리게이트가 유지되도록 하세요. |
| 이후 모델 변경이 표시되지 않음 | 명시적인 업데이트 코드가 있나요? | 레이블 값이나 정렬된 뷰를 명시적으로 갱신하세요. |
이 작업에서 안정적인 접근 방식은 Foundation 입력을 한 번 검증하고, 결과로 얻은 유형 지정 모델을 유지하며, 값마다 레이블 하나를 만들고, 스택과 앱의 수명을 명시적으로 설정하는 것입니다. 각 단계에 확인 가능한 계약이 있으므로 Mac 인터페이스가 확장되어도 문제를 더 쉽게 진단할 수 있습니다.
출처
Foundation 유형 사용하기 — Apple
NSArray — Apple
Array — Apple
compactMap(_:) — Apple
map(_:) — Apple
init(labelWithString:) — Apple
NSStackView — Apple
init(views:) — Apple
alignment — Apple
delegate — Apple
isReleasedWhenClosed — Apple
run() — Apple
activate() — Apple