struct NavigationSplitView<Sidebar, Content, Detail> where Sidebar : View, Content : View, Detail : View
// SwiftUI 4+
2분할 또는 3분할된 View를 만들 때, 즉 1개 또는 2개의 Sidebar를 만들 때 사용한다.
각 기종 및 윈도우 영역에 따라 NavigationStack
형태로 자동 변환되므로 편리하다. 예를 들어, iOS(portrait), watchOS, tvOS 등 사이드바를 지원하지 않는 기기의 경우 NavigationStack
형태로 변환되어 표시된다. iPadOS 또는 macOS에서도 화면분할, 스테이지 매니저, 윈도우 리사이징 등의 이유로 앱의 윈도우 영역이 좁은 경우 NavigationStack
형태로 변환되어 표시된다.
init(sidebar: () -> Sidebar, detail: () -> Detail) where Content == EmptyView
init(sidebar: () -> Sidebar, content: () -> Content, detail: () -> Detail)
init(columnVisibility: Binding<NavigationSplitViewVisibility>, sidebar: () -> Sidebar, detail: () -> Detail) where Content == EmptyView
init(columnVisibility: Binding<NavigationSplitViewVisibility>, sidebar: () -> Sidebar, content: () -> Content, detail: () -> Detail)
init(sidebar:, detail:)
init(sidebar:, content:, detail:)
columnVisibility
: 사이드바 표시 유무를 저장하고 접근하기 위해 사용한다. State
변수를 선언하여 Binding
으로 전달한다. NavigationStack
형태로 표시되는 경우, 해당 인자는 무시된다.struct NavigationSplitViewVisibility: Equatable, Codable
4개의 static 타입 프로퍼티가 있다:automatic
: 해당 기종 및 윈도우 영역에 따른 기본값을 사용한다.all
: 모든 사이드바를 표시한다.doubleColumn
: 하나의 사이드바만 표시한다. 이 경우 3분할 View에서의 첫번째 사이드바는 표시되지 않는다.detailOnly
: 사이드바를 표시하지 않는다.sidebar
인자 또는 content
인자의 클로저는 다음과 같이 사용해야 한다:
selection
인자를 받는 List
+ value
인자를 받는 NavigationLink
var categories : [Category]
// NavigationPath를 사용하면 다양한 타입을 단일 State 변수에 저장하여 path로 사용할 수 있다.
@State var selectedCategory : Category? = nil
@State var selectedItem : Item? = nil
var body: some View {
NavigationSplitView {
List(categories, selection: $selectedCategory) { category in
NavigationLink(category.title, value: category)
// 선택시 List의 selection 인자에 value 인자값을 대입한다.
}
} content: {
List(selectedCategory?.items ?? [], selection: $selectedItem) { item in
NavigationLink(item.title, value: item)
}
} detail: {
Text(selectedItem?.title ?? "nothing selected")
}
}
NavigationStack
및 NavigationSplitView
는 서로의 클로저 내에 중첩해서 사용할 수 있다.
위 사용방법은 대부분의 경우에 깔끔하게 동작하지만, Sidebar에는 반드시 List
를 포함해야만 한다는 전제조건이 붙는다. 즉, 다음과 같은 경우에 직면하면 골치 아프다:
List
이외의 View를 사용하고 싶은 경우다행히도 꼼수를 부려 해결할 수 있다. 다음 예제는 위 예제와 거의 동일하지만, 사이드바에 LazyVGrid
를 사용한다:
// 위와 동일...
var row = [GridItem(), GridItem(), GridItem()]
var body: some View {
NavigationSplitView {
LazyVGrid(columns: row) {
ForEach(categories) { category in
Button(category.title) { selectedCategory = category }
}
}
// List의 selection 인자는 NavigationSplitView와 상호작용한다.
List(selection: $selectedCategory) {}
} content: {
// 위와 동일...
} detail: {
// 위와 동일...
}
}
List
를 추가하고 selection
인자를 채운다.Button
, .onTapGesture
등으로 래핑하고, 선택 이벤트 발생시 selection
인자로 사용된 변수에 해당 요소 값을 대입한다. (앞서 사용한 NavigationLink
를 대체한다)content
클로저에서는 각 selection
인자에 해당하는 목록을 보여준다.주의사항:
List
는 눈에 보이거나 공간을 차지할 필요가 없고, 사용 여부는 상관없으나, 반드시 활성화된 상태로 존재해야 한다.NavigationLink
를 사용할 수 없다. value
인자를 받는 NavigationLink
는 selection
인자를 받는 List
의 내부 또는 .navigationDestination
수정자가 사용된 View의 내부에 위치해야 하기 때문이다.앞선 경우보다는 덜 우아하고 약간의 오버헤드가 발생하지만, 어쨌든 별다른 제약 없이 NavigationSplitView
를 사용할 수 있다.
func navigationSplitViewStyle<S>(_ style: S) -> some View where S : NavigationSplitViewStyle
func navigationSplitViewColumnWidth(_ width: CGFloat) -> some View
func navigationSplitViewColumnWidth(min: CGFloat? = nil, ideal: CGFloat, max: CGFloat? = nil) -> some View
위 수정자들로 Sidebar의 스타일을 지정하거나, Sidebar 및 detail View의 폭을 지정할 수 있다.
NavigationSplitViewStyle
프로토콜은 3개의 static 타입 프로퍼티를 가진다.
automatic
: 현재 맥락(기기 및 윈도우 크기)에 기반하여 자동으로 적합한 스타일로 표시한다.balanced
: Sidebar를 띄울 때 detail View의 영역을 줄여 공간을 확보한다. Sidebar가 detail View를 가리지 않는다.prominentDetail
: Sidebar를 띄울 때 detail View의 영역을 조절하지 않고 오버레이한다. Sidebar가 detail View를 가린다.해당 프로토콜에서는 스타일을 커스텀할 수 있도록 makeBody(configuration:)
함수 및 NavigationSplitViewStyleConfiguration
구조체를 제공하지만, 구조체 내에 공개된 프로퍼티는 없다.
navigationSplitViewColumnWidth(_:)
를 사용하여 사이드바의 폭을 특정 값으로 고정할 수 있다.
navigationSplitViewColumnWidth(min:, ideal:, max:)
를 사용하여 사이드바의 폭을 윈도우 크기에 기반하여 유연하게 조정되도록 할 수 있다.
NavigationSplitView {
MySidebar()
.navigationSplitViewColumnWidth(150)
} contents: {
MyContents()
.navigationSplitViewColumnWidth(
min: 150, ideal: 200, max: 400)
} detail: {
MyDetail()
}
요새 간단한 앱이라도 만들어보려고 이것저것 들쑤셔보고 있는데, 이 빌어먹을 스플릿뷰가 나를 멈춰세웠다. SwiftUI는 참 이쁘고 좋은 언어지만, 'List를 사용하지 않는 Sidebar'처럼 예외 케이스가 나오는 경우... 온갖 극찬이 절로 나온다. 보통 다음 셋 중 하나로 귀결된다:
어느 쪽이던 속이 보글보글 끓는 일이다. 와 맛있겠다
나는 세번째를 선택했고, 결론적으로는 꽤 간단한 해결책이 도출되었지만 그 과정이 쉽지만은 않았다.
애플 개발자 포럼에도 나처럼 머릿속에 라면을 끓이는 분들이 많은 것 같아서 답변을 달아드렸다.
developer.apple.com/forums: Driving NavigationSplitView with something other than List?
이쯤되니 어느정도 정리글이 윤곽이 잡혀버려서, 결국엔 블로그에도 올리게 됐다.
사실 블로그에 올리고 싶었던 글들은 따로 있다. 모두 얼마나 걸릴지는 모르겠다:
이렇게 써놓으면 언젠간 올리겠지.
developer.apple.com: NavigationSplitView
WWDC22: The SwiftUI cookbook for navigation