new navigation APIs
기존 NavigationLink는 프로그래밍적으로 네비게이션을 하기 위해서는 binding을 링크 갯수만큼 만들어야 했다.
이제는 컨테이너 전체의 navigation을 묶어서 지정해줄 수 있다.
NavigationStack(path: $path) { // 네비게이션 스택 전체를 의미하는 콜렉션의 binding
NavigationLink("Details", value: value)
}
앱 구조를 표현할 수 있는 새로운 컨테이너 타입들
아이폰이나 slide over, watch, apple tv에서는 단일 스택으로 동작
two column 버전의 생성자와 three column 버전의 생성자를 제공
NavigationSplitView {
RecipeCategories()
} detail: {
RecipeGrid()
}
NavigationSplitView {
RecipeCategories()
} content: {
RecipeList()
} detail: {
RecipeDetail
}
그 외에도 column 너비, 사이드바 프레젠테이션, 프로그래밍 방식으로 column을 보이고 숨기는 것 등의 다양한 커스텀 옵션 제공
NavigationLink API의 변화
기존 버전
NavigationLink("Show Detail") {
DetailView()
}
신규 버전 → View가 아니라 value를 받는다.
NavigationLink("Apple Pie", value: applePieRecipe)
Recipes for navigation
싱글 스택 - 뷰 기반 네비 게이션
var body: some View {
NavigationStack {
List(Category.all) { category in
Section(category.localizedName) {
ForEach(dataModel.recipes(in: category)) { recipe in
NavigationLink(recipe.name) {
RecipeDetail(recipe: recipe)
}
}
}
}
.navigationTitle("Categories")
}
}
상태기반 싱글 스택 네비게이션
var body: some View {
NavigationStack {
List(Category.all) { category in
Section(category.localizedName) {
ForEach(dataModel.recipes(in: category)) { recipe in
NavigationLink(recipe.name, value: recipe)
}
}
}
.navigationTitle("Categories")
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
}
NavigationStack의 원리
NavigationSplitView
@State private var selectedCategory: Category?
@State private var selectedRecipe: Recipe?
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.localizedName, value: category)
}.navigationTitle("Categories")
} content: {
List(dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in
NavigationLink(recipe.name, value: recipe)
}.navigationTitle(selectedCategory?.localizedName ?? " Recipes")
} detail: {
RecipeDetail(recipes: selectedRecipe)
}
}
두 개를 모두 쓰는 경우 - Multi columns with stack
@State private var selectedCategory: Category?
@State private var path: [Recipe] = []
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.localizedName, value: category)
}.navigationTitle("Categories")
} detail: {
NavigationStack(path: $path) {
RecipeGrid(category: selectedCategory)
}
}
}
struct RecipeGrid: View {
var category: Category?
var body: some View {
if let category = category {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(dataModel.recipes(in: category)) { recipe in
NavigationLink(value: recipe) { RecipeTile(recipe: recipe) }
}
}
}
.navigationTitle(category.name)
.navigationDestination(for: Recipe.self) { recipe in RecipeDetail(recipe: recipe) }
} else {
EmptyView()
}
}
}
Persistent state
Navigation 상태를 모델링한다.
class NavigationModel: ObservableObject {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe] = []
}
// 뷰에서는 이렇게 바꾸자.
@StateObject private var navModel = NavigationModel()
이에 Codable을 채택한다.
class NavigationModel: ObservableObject, Codable {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe] = []
enum CodingKeys: String, CodingKey {
case selectedCategory
case recipePathIds
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
try container.encode(recipePath.map(\\.id), forKey: .recipePathIds)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.selectedCategory = try container.decodeIfPresent(
Category.self, forKey: .selectedCategory)
let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
}
}
SceneStorage를 통해서 저장하고 복구한다.
@SceneStorage("navigation") private var data: Data?
var body: some View {
NavigationSplitView { ... }
.task {
if let data = data {
navModel.jsonData = data
}
for await _ in navModel.objectWillChangeSequence {
data = navModel.jsonData
}
}
}
새로운 API로 빠르게 넘어가는 것을 권장한다.
List와 NavigationStackView, NavigationSplitView는 섞어쓰도록 만들어져 있음을 염두에 두라.
NavigationSplitView가 적절한 상황이 있다면 써보라.