NavigationStack
A view that displays a root view and enables you to present additional views over the root view.
iOS 16버전부터 업데이트된 NavigationStack에 대해 알아보고, 그와 관련된 예시를 보여드리겠습니다.
NavigationStack은 기존의 NavigationView를 대체할 것으로,
루트 뷰를 보여주고, 루트 뷰에 대한 추가적인 뷰를 보여주는 것입니다.
사실 이름에서 알 수 있듯 화면을 네비게이션 형태로 넘겨주는 것인데, 스택의 방식을 사용하여 넘겨주는 것입니다.
스택의 특징과 같이 Push, Pop, Pop to root view 등의 개념을 인지하고 있다면 매우 유용합니다.
@MainActor
struct NavigationStack<Data, Root> where Root : View
NavigationStack은 안에 들어갈 View가 아니라 Data와 Root를 가집니다.
또한 NavigationStack에 들어갈 데이터는 Hashable해야 합니다.
기존의 NavigationView은 View를 넘겨주는 반면에,
NavigationStack은 Data를 넘겨준다는 것이 큰 차이점이자 장점입니다.
예시
우선 같은 데이터 형식을 사용하여 데이터를 넘겨주는 예시입니다.
(대부분의 경우에는 같은 데이터 형식만을 보내지 않기 때문에 일반적인 상황은 아닙니다.)
import SwiftUI
struct ContentView: View {
var platforms: [Platform] = [.init(name: "Xbox", imageName: "xbox.logo", color: .green),
.init(name: "Playstation", imageName:"playstation.logo", color: .indigo),
.init(name: "PC", imageName: "pc", color: .pink),
.init(name: "Mobile", imageName: "iphone", color: .mint)]
var games: [Game] = [.init(name: "Minecraft", rating: "99"),
.init(name: "LOL", rating: "100"),
.init(name: "FIFA", rating: "94"),
.init(name: "Maple Story", rating: "70")]
var body: some View {
NavigationStack {
List {
Section("Platform") {
ForEach(platforms, id: \.name) { platform in
NavigationLink(value: platform) { //NavigationStack은 Data를 넘겨주기 때문에 value가 들어감
Label(platform.name, systemImage: platform.imageName)
.foregroundColor(platform.color)
}
}
}
Section("Games") {
ForEach(games, id: \.name) { game in
NavigationLink(value: game) { //Navigation Destination이 존재하기 때문에 > 생성됨
Text(game.name)
}
}
}
}
.navigationTitle("Gaming")
.navigationDestination(for: Platform.self) { platform in //platform 데이터를 넘겨줌!
ZStack {
platform.color.ignoresSafeArea()
Label(platform.name, systemImage: platform.imageName)
.font(.largeTitle)
.bold()
}
}
.navigationDestination(for: Game.self) { game in
Text("\(game.name) - \(game.rating)")
.font(.largeTitle.bold())
}
}
}
}
struct Platform: Hashable {
let name: String
let imageName: String
let color: Color
}
struct Game: Hashable {
let name: String
let rating: String
}
다음은 예시에 대한 영상입니다.
Data에 따라 다른 화면이 보이도록 넘겨줄 수 있습니다.
데이터에 대한 배열을 생성해놓고, 해당 배열에 접근함으로써 화면을 이동하였습니다.
하지만 대부분의 경우 같은 데이터 형식만을 가지고 화면이 구성되지 않기 때문에
데이터가 다른 경우에 대한 방법 또한 필요합니다.
따라서 이러한 경우에 필요한 NavigationPath에 대해 알아보겠습니다.
NavigationPath
A type-erased list of data representing the content of a navigation stack.
NavigationPath는 네비게이션 경로를 가지고 있는 리스트로 생각하면 될 것 같습니다.
NavigationPath를 사용하면 경로를 직렬화(serialize)할 수 있습니다
NavigationPath에 값을 추가하고 제거함으로써 네비게이션의 상태를 조정할 수 있습니다.
다음은 NavigationPath를 사용한 예시입니다.
import SwiftUI
struct ContentView: View {
var platforms: [Platform] = [.init(name: "Xbox", imageName: "xbox.logo", color: .green),
.init(name: "Playstation", imageName:"playstation.logo", color: .indigo),
.init(name: "PC", imageName: "pc", color: .pink),
.init(name: "Mobile", imageName: "iphone", color: .mint)]
//같은 데이터 형식을 사용한 예시 (화면을 넘길 때는 대부분 다른 뷰와 데이터를 보기 때문에 일반적인 상황은 아님)
var games: [Game] = [.init(name: "Minecraft", rating: "99"),
.init(name: "LOL", rating: "100"),
.init(name: "FIFA", rating: "94"),
.init(name: "Maple Story", rating: "70")]
// @State private var path: [Game] = []
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
Section("Platform") {
ForEach(platforms, id: \.name) { platform in
NavigationLink(value: platform) { //NavigationStack은 Data를 넘겨주기 때문에 value가 들어감
Label(platform.name, systemImage: platform.imageName)
.foregroundColor(platform.color)
}
}
}
Section("Games") {
// ForEach(games, id: \.name) { game in
// NavigationLink(value: game) { //Navigation Destination이 존재하기 때문에 > 생성됨
// Text(game.name)
// }
// }
Button("Add Games") {
path.append(games.first!) //path에 games.first!를 추가하여 이동하였음 -> '데이터'를 가지고 있음
// path = games //모든 games 데이터가 추가됨
}
}
}
.navigationTitle("Gaming")
.navigationDestination(for: Platform.self) { platform in //platform 데이터를 넘겨줌!
ZStack {
platform.color.ignoresSafeArea()
VStack {
Label(platform.name, systemImage: platform.imageName)
.font(.largeTitle)
.bold()
List {
ForEach(games, id: \.name) { game in
NavigationLink(value: game) { //Navigation Destination이 존재하기 때문에 > 생성됨
Text(game.name)
}
}
}
}
}
}
.navigationDestination(for: Game.self) { game in
VStack(spacing: 20) {
Text("\(game.name) - \(game.rating)")
.font(.largeTitle.bold())
//경로를 추가함
Button("Recommanded game") {
path.append(games.randomElement()!)
}
Button("Go to another platform") {
path.append(platforms.randomElement()!)
}
//path에 있는 데이터를 제거함으로 루트 뷰로 이동함
Button("Go Home") {
path.removeLast(path.count)
}
}
}
}
}
}
struct Platform: Hashable {
let name: String
let imageName: String
let color: Color
}
struct Game: Hashable {
let name: String
let rating: String
}
append를 통해 네비게이션 스택을 쌓아 화면을 이동할 수 있고,
removeLast를 통해 스택을 제거하여 이전 화면으로 돌아갈 수 있습니다.
다른 예시입니다.
NavigationStack에 value를 함께 전달합니다.
import SwiftUI
struct NavigationPathWithValue: View {
@State var stack = NavigationPath()
var body: some View {
NavigationStack(path: $stack) {
NavigationLink("Go to Child View", value: "Second")
.navigationDestination(for: String.self) { value in
SecondPage(viewNumber: value, stack: $stack)
}
}
}
}
struct SecondPage: View {
let viewNumber: String
@Binding var stack: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text("This view is \(viewNumber)")
NavigationLink("Go to Third Page", destination: ThirdPage(viewNumber: "Third", stack: $stack))
}
}
}
struct ThirdPage: View {
let viewNumber: String
@Binding var stack: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text("This view is \(viewNumber)")
NavigationLink(destination: FourthPage(viewNumber: "Fourth", stack: $stack)) {
Text("Go to Fourth Page")
}
Button("Go to Root View") {
stack.removeLast()
}
}
}
}
struct FourthPage: View {
let viewNumber: String
@Binding var stack: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text("This view is \(viewNumber)")
Button("Go to Root View") {
stack.removeLast()
}
}
}
}
struct NavigationPathExample_Previews: PreviewProvider {
static var previews: some View {
NavigationPathWithValue()
}
}
만약 굳이 데이터를 보내지 않고 싶다면 다음과 같이 작성할 수 있습니다.
하지만 NavigationStack 자체가 데이터를 전달하는 것이기 때문에, 루트 뷰의 데이터는 필요한 것 같습니다.
(데이터가 없다면 다시 루트 뷰로 되돌아갈 때 에러가 나타남)
import SwiftUI
struct NavigationPathNoValue: View {
@State var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
NavigationLink("Go to Second View", value: "start") //stack의 루트를 찾아야 하기 때문에 value는 있어야 함
.navigationDestination(for: String.self) { _ in
SecondView(path: $path)
}
}
}
}
struct SecondView: View {
@Binding var path: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text("SecondView")
NavigationLink("Go to Third Page", destination: ThirdView(path: $path))
}
}
}
struct ThirdView: View {
@Binding var path: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text("ThirdView")
NavigationLink(destination: FourthView(path: $path)) {
Text("Go to Fourth Page")
}
Button("Go to Root View") {
path.removeLast()
}
}
}
}
struct FourthView: View {
@Binding var path: NavigationPath
var body: some View {
VStack(spacing: 20) {
Text("FourthView")
Button("Go to Root View") {
path.removeLast()
}
}
}
}
struct NavigationPathNoValue_Previews: PreviewProvider {
static var previews: some View {
NavigationPathNoValue()
}
}
위 예시들은 깃허브에 올려놓았기 때문에, 확인해보실 분들은 다운받아서 확인해보시면 좋을 것 같습니다.
iOS 16버전에 출시되기 때문에 아직 현업에서 사용할 가능성은 낮지만, 언젠가는 사용할 것이기 때문에 알아두면 좋을 것 같습니다.
GitHub Link
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] ViewModifier (0) | 2023.08.11 |
---|