Mac 앱에서 사용자 지정 단일 열 컬렉션을 만들려면, 안정적인 항목 ID 집합을 바인딩하는 선택 바인딩과 함께 SwiftUI의 List를 사용하세요. 네이티브 컨트롤이 범위 선택과 Command-클릭 선택을 처리하도록 두면서 행을 사용자 지정할 수 있습니다.
이 튜토리얼에서는 Tile 01부터 Tile 12까지 합성 레이블로 구성된 컬렉션을 만듭니다. 독자는 서로 인접한 항목을 선택하고, 개별 항목을 추가하거나 제거하며, 필터링하거나 표시 순서를 뒤집어도 선택 상태를 이해하기 쉽게 유지합니다. 뒤쪽 섹션에서는 그리드나 다른 사용자 지정 레이아웃이 필요한 컬렉션에 사용할 AppKit 방식도 설명합니다.
범위 선택과 Command-클릭 선택은 어떻게 동작해야 할까요?
Apple의 Mac 사용 설명서에는 익숙한 상호작용 방식이 설명되어 있습니다. 첫 번째 항목을 클릭한 다음 마지막 항목을 Shift-클릭하면 두 끝점 사이의 항목을 포함해 인접한 항목이 선택됩니다. Command-클릭은 서로 인접하지 않아도 되는 개별 항목을 선택하며, 이미 선택된 항목을 Command-클릭하면 다중 선택에서 해당 항목이 제거됩니다. 이 컬렉션에서 유지해야 할 기본 상호작용입니다. Mac 화면에서 항목 선택하기.
초기 구현에서는 순서가 있는 목록 하나를 사용하세요. 그러면 “사이”의 의미가 명확해집니다. 즉, 현재 표시 순서에서 두 끝점 사이에 있는 행을 뜻합니다. 공간형 컬렉션에서는 범위가 데이터 순서를 따를지 레이아웃의 영역을 따를지 추가로 결정해야 하므로, 별도로 테스트하는 편이 좋습니다.
사용자가 선택하는 대상에 따라 컨트롤을 선택하세요.
| 독자의 작업 | 적합한 시작점 | 선택 표현 방식 |
|---|---|---|
| 사용자 지정 전체 행 선택 | SwiftUI List |
항목 ID 집합 |
| 사용자 지정 레이아웃에서 타일 전체 선택 | AppKit NSCollectionView |
모델 ID에 매핑되는 선택 인덱스 경로 |
| 레이블 안의 문자 선택 | SwiftUI 텍스트 선택 | 텍스트 뷰가 제공하는 텍스트 선택 |
| 텍스트 문서 안의 범위 선택 | AppKit 텍스트 시스템 | 문자 또는 텍스트 범위 |
Apple은 List를 선택 기능을 선택적으로 지원하는 단일 열 컨테이너로, NSCollectionView를 사용자 지정 가능한 레이아웃을 갖춘 순서 있는 컬렉션으로 설명합니다. 항목 선택을 위한 지원되는 시작점은 이 두 가지입니다. List, NSCollectionView.
완전한 SwiftUI 예제 만들기
Xcode에서 SwiftUI를 사용하는 macOS 앱 프로젝트를 만드세요. 이 예제의 배포 대상은 macOS 14 이상으로 설정하세요. 아래에서 사용하는 두 매개변수 형식의 onChange 클로저는 macOS 14가 필요합니다. 생성된 앱 선언을 아래 코드로 바꾸고, 타깃에 @main 진입점이 정확히 하나만 남도록 하세요. onChange(of:initial:_:), App.
다음 코드는 합성 데이터와 윈도우를 포함한 작은 앱 전체입니다. 외부 에셋은 사용하지 않습니다. 문서화된 API를 기준으로 검토했지만, 여기서 macOS에서 컴파일하거나 실행하지는 않았습니다.
import Foundation
import SwiftUI
struct Tile: Identifiable {
let id: Int
let title: String
}
@main
struct CollectionSelectionDemoApp: App {
var body: some Scene {
WindowGroup {
TileCollectionView()
}
}
}
struct TileCollectionView: View {
private let tiles: [Tile] = (1...12).map {
Tile(id: $0, title: String(format: "Tile %02d", $0))
}
@State private var selectedIDs: Set<Int> = []
@State private var query = ""
@State private var reversed = false
private var visibleTiles: [Tile] {
let filtered = tiles.filter {
query.isEmpty ||
$0.title.localizedCaseInsensitiveContains(query)
}
return reversed ? Array(filtered.reversed()) : filtered
}
private var selectionDescription: String {
let names = visibleTiles
.filter { selectedIDs.contains($0.id) }
.map(\.title)
return names.isEmpty ? "None selected" : names.joined(separator: ", ")
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField("Filter tiles", text: $query)
.textFieldStyle(.roundedBorder)
HStack {
Button("Reverse order") {
reversed.toggle()
}
Button("Clear selection") {
selectedIDs.removeAll()
}
.disabled(selectedIDs.isEmpty)
}
List(visibleTiles, selection: $selectedIDs) { tile in
HStack(spacing: 12) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray.opacity(0.2))
.frame(width: 32, height: 32)
VStack(alignment: .leading) {
Text(tile.title)
Text("Sample item \(tile.id)")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
.tag(tile.id)
}
Text("\(selectedIDs.count) selected")
Text(selectionDescription)
.font(.caption)
}
.padding()
.frame(minWidth: 440, minHeight: 480)
.onChange(of: query) { _, _ in
let visibleIDs = Set(visibleTiles.map(\.id))
selectedIDs.formIntersection(visibleIDs)
}
}
}
선택 바인딩의 요소 형식은 Tile.ID와 일치합니다. 둘 다 Int를 사용합니다. Apple 문서에 따르면 목록의 선택을 데이터 ID 형식의 집합에 바인딩하면 다중 선택이 활성화됩니다. Mac에서는 다중 선택을 위해 편집 모드에 들어갈 필요가 없습니다. 목록에서 선택 지원하기.
행은 사용자 지정이 가능합니다. 중립적인 도형, 제목, 보조 레이블로 구성되어 있습니다. 행 수준의 클릭 핸들러나 별도의 토글 동작은 없습니다. 목록이 선택을 관리하도록 하면 하나의 상호작용에 대해 서로 독립적인 두 코드가 선택 상태를 업데이트하는 일을 피할 수 있습니다.
항목 ID와 표시 순서를 분리하기
이 예제에서는 순서가 있는 항목 배열, 선택된 ID 집합, 그리고 해당 배열을 필터링한 결과라는 세 가지 개념을 의도적으로 분리합니다. Swift에서 배열은 순서가 있는 컬렉션이고 집합은 순서가 정의되지 않은 고유 값의 컬렉션입니다. 따라서 포함 여부를 나타내는 데는 집합이 적합하고, 화면에 표시할 순서는 배열이 제공합니다. 컬렉션 타입.
정수 ID는 이 합성 데이터에 고정되어 있습니다. 배열을 뒤집어도 항목의 위치만 바뀌고 ID는 바뀌지 않습니다. 실제 모델에서는 항목을 만들거나 불러올 때 안정적인 ID를 할당하고 이후 뷰 업데이트에서도 그 ID를 유지하세요. 행을 평가할 때마다 새로운 ID를 생성하지 마세요.
선택 요약은 visibleTiles를 따르고 selectedIDs의 포함 여부를 확인합니다. 이는 컬렉션의 이러한 특성을 바탕으로 한 구현 선택입니다. 집합의 순회 순서에 의존하지 않고 표시 순서대로 요약을 생성합니다.
필터링에는 명시적인 제품 정책이 필요합니다. 이 예제에서는 표시 항목만 선택 상태로 유지하는 정책을 채택합니다. 필터가 바뀔 때마다 선택된 ID와 계속 표시되는 ID의 교집합을 구합니다. 숨겨진 항목은 선택이 해제되고, 필터를 지워도 이전 선택 상태가 복원되지 않습니다. 레이블과 원본 배열은 바뀌지 않습니다.
예를 들어 Tile 02, Tile 03, Tile 04를 선택한 상태에서 Filter tiles에 04을 입력해 보세요. 이 예제의 정책에 따라 Tile 04만 계속 선택됩니다. 필터를 지우면 나머지 두 항목이 선택되지 않은 상태로 다시 나타납니다. 이는 코드의 동작을 설명하기 위한 예시이며, 실제 실행 결과를 보고한 것이 아닙니다.
다른 앱에서는 필터를 적용해도 선택을 유지해야 할 수 있습니다. 이 경우 선택 상태의 소유권을 모델로 옮기고, 숨겨진 선택 항목을 개수와 요약에 어떻게 표시할지 정의한 뒤, 표시된 컨트롤과 상태를 어떻게 조정할지 테스트하세요. 교집합 계산만 제거한다고 해서 숨겨진 선택 항목에 대한 정책이 완성되는 것은 아닙니다.
AppKit 컬렉션 뷰는 언제 사용해야 할까요?
컬렉션에 여러 열의 타일이나 단일 목록을 넘어서는 레이아웃이 필요할 때는 NSCollectionView를 사용하세요. Apple은 컬렉션의 데이터 소스, 항목 뷰 컨트롤러, 델리게이트, 레이아웃 객체를 각각 분리합니다. 이를 통해 컬렉션 컨트롤이 상호작용을 관리하는 동안 모양과 배치를 사용자 지정할 수 있습니다. NSCollectionView.
AppKit 구현은 다음 순서로 구성하세요.
- 컬렉션 뷰를 만들고
NSCollectionViewFlowLayout같은 최신 레이아웃을 지정합니다. - 데이터 소스를 제공하고 셀을 만들 때 사용할 항목 클래스나 nib을 등록합니다.
isSelectable을true로,allowsMultipleSelection를true으로 설정합니다.- 선택 항목이 없어도 되는 작업인지에 따라
allowsEmptySelection를 설정합니다. selectionIndexPaths을 읽은 다음, 유효한 각 인덱스 경로를 해당 모델 ID에 매핑합니다.- 항목 뷰의
isSelected상태를 사용해 선택 상태를 렌더링합니다.
다중 선택은 기본적으로 비활성화되어 있습니다. 따라서 타일을 선택 가능해 보이도록 그리는 것만으로는 충분하지 않으며, 활성화하는 설정 단계가 필요합니다. 항목의 isSelected 속성은 해당 항목의 선택 여부를 나타냅니다. allowsMultipleSelection, isSelected.
인덱스 경로는 현재 데이터 소스에서의 위치로 취급하세요. Apple 문서에 따르면 selectionIndexPaths은 상호작용에 따른 선택 중 업데이트되며 프로그래밍 방식으로도 설정할 수 있습니다. 데이터 소스의 유효한 항목을 가리키지 않는 인덱스 경로를 설정하면 예외가 발생합니다. 순서를 바꾸거나 필터링한 뒤에는 오래된 위치를 재사용하지 말고 안정적인 ID를 바탕으로 선택 위치를 다시 구성하세요. selectionIndexPaths.
문서화된 범위를 지키세요. allowsMultipleSelection은 둘 이상의 항목을 선택할 수 있게 하지만, 해당 문서에는 모든 사용자 지정 레이아웃이나 수식어 조합에서의 완전한 범위 앵커 알고리즘이 명시되어 있지 않습니다. 실제 레이아웃과 지원되는 macOS 버전에서 Shift-클릭 동작을 확인하세요. 정확한 선형 범위 선택이 필요한 작업이라면 위의 단일 열 구현이 더 명확한 시작점입니다.
AppKit 컨트롤이 필요한 SwiftUI 앱에서는 NSViewRepresentable을 통해 포함하세요. makeNSView에서 AppKit 뷰 계층을 만들고 updateNSView에서 모델 변경 사항을 조정한 다음, 코디네이터를 사용해 델리게이트 이벤트를 SwiftUI 상태로 전달하세요. SwiftUI가 표현된 뷰의 프레임과 경계를 제어합니다. NSViewRepresentable.
프로그래밍 방식 업데이트에도 명시적인 동기화 경로가 필요합니다. Apple에 따르면 선택이 프로그래밍 방식으로 설정되면 collectionView(_:shouldSelectItemsAt:)와 collectionView(_:didSelectItemsAt:) 모두 호출되지 않습니다. 모든 할당 후 콜백이 실행될 것이라고 기대하지 말고, 공유 작업을 통해 모델을 업데이트하세요. collectionView(_:shouldSelectItemsAt:), collectionView(_:didSelectItemsAt:).
텍스트 선택으로 같은 기능을 구현할 수 있을까요?
SwiftUI의 textSelection(.enabled)는 뷰 안의 텍스트 선택을 제어합니다. 컨테이너에 적용하면 포함된 각 텍스트 뷰에서 텍스트를 선택할 수 있습니다. 하지만 컬렉션 항목 선택 바인딩이나 임의의 카드 전반에 걸친 공유 범위 선택 모델을 문서화하지는 않습니다. 레이블에서 문자를 선택하는 작업에 사용하세요. textSelection(_:).
텍스트 문서 인터페이스의 경우 AppKit은 NSTextView.selectedRanges를 통해 문자 선택을 제공합니다. 이 범위는 순서가 있고 서로 겹치지 않으며 연속적이지 않을 수 있습니다. TextKit의 NSTextSelection.textRanges도 논리적으로 정렬된 텍스트 범위를 나타내며 macOS 12부터 사용할 수 있습니다. 이 API는 텍스트 위치를 설명하므로, 이를 타일 ID로 변환하는 방법은 앱별로 설계해야 합니다. selectedRanges, textRanges.
행 레이블 안에서 텍스트 선택을 활성화하기 전에 해당 레이블이 어떤 동작을 담당할지 결정하세요. 이 튜토리얼의 작업에서는 레이블을 클릭해도 행 전체 선택에 참여해야 하므로, 예제에서는 텍스트 선택을 설정하지 않습니다.
간단한 선택 행렬로 상호작용 확인하기
앱의 실제 윈도우에서 다음 항목을 확인하세요. 처음 네 가지는 기본적인 Mac 선택 규칙을 확인하고, 나머지는 예제의 ID와 필터링 방식을 점검합니다. Apple이 문서화한 상호작용을 기준으로 삼고, 합성 레이블을 통해 예상되는 선택 항목을 쉽게 확인할 수 있습니다. Mac 화면에서 항목 선택하기.
| 동작 | 예상 결과 |
|---|---|
Tile 02 클릭 |
항목 하나가 선택됨 |
Tile 05 Shift-클릭 |
Tile 02부터 Tile 05까지 선택됨 |
Tile 08 Command-클릭 |
Tile 08이 선택 항목에 추가됨 |
선택된 Tile 03 Command-클릭 |
선택 항목에서 Tile 03이 제거됨 |
Clear selection 클릭 |
선택이 비워지고 요약에 None selected가 표시됨 |
Tile 04를 선택한 뒤 순서 뒤집기 |
선택된 ID가 계속 Tile 04를 가리킴 |
여러 행을 선택한 뒤 04로 필터링 |
이전에 선택된 항목 중 계속 표시되는 항목만 선택된 상태로 남음 |
| 필터 지우기 | 숨겨진 행이 다시 표시되지만 해제된 선택은 복원되지 않음 |
목록을 클릭한 뒤 키보드 탐색, 선택된 행을 화면 밖으로 스크롤했다가 다시 표시하는 동작, 필터링 후 새 범위 선택을 시작하는 동작도 확인하세요. AppKit 그리드에서는 행 경계를 넘어 선택하는 검사도 추가하세요. 결과가 의도한 작업과 다르면 먼저 일반 네이티브 컨트롤만 남긴 뒤 사용자 지정 행 콘텐츠와 제스처 핸들러를 한 번에 하나씩 다시 추가하세요.
상호작용은 네이티브 컨트롤이 담당하고, 포함 여부는 안정적인 ID로 관리하며, 표시 순서는 화면에 보여 주는 배열이 담당하도록 하세요. 각 역할이 명확하면 행 스타일, 필터링, 정렬을 적용해도 이해하기 쉬운 선택 동작을 유지할 수 있습니다.
출처
- Mac 화면에서 항목 선택하기 — Apple 지원.
- List — Apple 개발자 문서.
- App — Apple 개발자 문서.
- onChange(of:initial:_:) — Apple 개발자 문서.
- 컬렉션 타입 — Swift 프로젝트, The Swift Programming Language.
- NSCollectionView — Apple 개발자 문서.
- allowsMultipleSelection — Apple 개발자 문서.
- selectionIndexPaths — Apple 개발자 문서.
- isSelected — Apple 개발자 문서.
- NSViewRepresentable — Apple 개발자 문서.
- collectionView(_:shouldSelectItemsAt:) — Apple 개발자 문서.
- collectionView(_:didSelectItemsAt:) — Apple 개발자 문서.
- textSelection(_:) — Apple 개발자 문서.
- selectedRanges — Apple 개발자 문서.
- textRanges — Apple 개발자 문서.