root 뷰와 root 뷰에서 추가적인 뷰들을 보여주는 뷰라고 합니다. 기존 NavigationView와 다른점은 view 대신 데이터와 값들로 작동을 한다는 것입니다.
Navigation Stack를 사용하면 root view를 중심으로 뷰를 스택처럼 쌓아서 사용할 수 있습니다. 사람들은 NavigationLink를 클릭함으로서 뷰를 스택 최상단에 쌓을 수 있습니다. 그리고 스택에 쌓인 뷰들은 뒤로가기 버튼이나 스와이핑 제스처를 통해서 pop 할 수 있습니다. 스택은 가장 최근에 추가된 뷰를 보여줍니다
navigation link를 만들기 위해서 navigation link를 만들려면, 스택의 뷰 계층 구조 안에 navigationDestination(for:destination:) 수정자를 추가하여 뷰를 데이터 타입과 연결한 후 다음 같은 종류의 데이터의 인스턴스를 제공하는 NavigationLink를 초기화합니다. 다음 스택은 Park 유형의 데이터를 제공하는 Navigation Link에 대한 ParkDetails 뷰를 표시합니다.
여기서 편리한 점은 원하는 값을 편하게 넘겨줄 수 있다는 것입니다.
NavigationLink(_, value: _)
- 첫번째 인자: 원하는 String 값으로 Link를 설명해주는 값
- value: 전달하고 싶은 데이터를 넣을 수 있습니다
NavigationDestination(for: _, destination: _)
- for: 전달할 데이터의 타입을 정의하는 곳
- destination: 데이터를 전달하여 해당 데이터로 다른 뷰를 만들수도 있습니다
화면이 이동했을 때 이전 화면으로 돌아가는 방법
뷰는 스택으로 쌓이고 해당 스택은 NavigationPath()에 저장되어 있습니다. 해당 스택에서 가장 위에 있는 뷰를 지우면 이전 화면으로 돌아가기 때문에 removeLast()를 해줍니다.
import SwiftUI
struct ContentView: View {
@State var stack = NavigationPath()
var body: some View {
NavigationStack(path: $stack) {
NavigationLink("Go to Child View", value: "10")
.navigationDestination(for: String.self) { value in
Text("Child Number is \(value)")
Button("Go to Root View") {
stack.removeLast()
}
}
}
}
}
만약 스택이 여러개 쌓여있지만 가장 최상단 root view로 한번에 이동하고 싶을때는 어떻게 하는가?
NavigationPath 프로퍼티에서 .init()을 하면 됩니다.
import SwiftUI
struct ContentView: View {
@State var stack = NavigationPath()
var body: some View {
NavigationStack(path: $stack) {
NavigationLink("Go to Child View", value: "10")
.navigationDestination(for: String.self) { value in
VStack {
NavigationLink("Go to Child's Child View", value: "20")
Text("Child Number is \(value)")
Button("Go to Parent View") {
stack.removeLast()
}
Button("Go to Root View") {
stack = .init()
}
}
}
}
}
}
여기서 좀 더 나아가서 실제 사용하는 방법 탐구해보기
아래와 같은 뷰를 NavigationStack으로 만든 코드입니다.
import SwiftUI
struct StationView: 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 body: some View {
NavigationStack {
List {
Section("Platforms") {
ForEach(platforms, id: \.name) { platform in
NavigationLink(value: platform) {
Label(platform.name, systemImage: platform.imageName)
.foregroundStyle(platform.color)
}
}
}
}
.navigationTitle("Gaming")
.navigationDestination(for: Platform.self) { platform in
ZStack {
platform.color.ignoresSafeArea()
Label(platform.name, systemImage: platform.imageName)
.font(.largeTitle).bold()
}
}
}
}
}
struct SettingView_Previews: PreviewProvider {
static var previews: some View {
SettingView()
}
}
struct Platform: Hashable {
let name: String
let imageName: String
let color: Color
}
Section("Platforms") {
ForEach(platforms, id: \.name) { platform in
NavigationLink(value: platform) {
Label(platform.name, systemImage: platform.imageName)
.foregroundStyle(platform.color)
}
}
}
NavigationLink(_, value: _)
- 첫번째 인자(선택): 원하는 값을 넣어서 화면이 이동했을때 해당 뷰 또는 데이터를 사용할 수 있게 데이터를 전달하는 것
- value: 전달하고 싶은 데이터를 넣을 수 있습니다.
.navigationDestination(for: Platform.self) { platform in
ZStack {
platform.color.ignoresSafeArea()
Label(platform.name, systemImage: platform.imageName)
.font(.largeTitle).bold()
}
}
NavigationDestination(for: _, destination: _): navigationlink를 통해서 받을 값을 받는다
- for: Platform 데이터 타입을 전달할 것이라 설정
- destination: Platform 타입의 데이터인 platform 데이터를 전달하여 해당 데이터로 다른 뷰를 만들수도 있습니다.
여기서 만약 Section이 두개가 된다면 어떻게 값을 전달할 것인가?
방법은 간단합니다.
navigationDestination을 두개 만들면 됩니다.
import SwiftUI
struct StationView: 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: "God of War", rating: "98"),
.init(name: "Fortnite", rating: "92"),
.init(name: "Madden 2023", rating: "88")]
var body: some View {
NavigationStack {
List {
Section("Platforms") {
ForEach(platforms, id: \.name) { platform in
NavigationLink(value: platform) {
Label(platform.name, systemImage: platform.imageName)
.foregroundStyle(platform.color)
}
}
}
Section("Games") {
ForEach(games, id: \.name) { game in
NavigationLink(value: game) {
Text(game.name)
}
}
}
}
.navigationTitle("Gaming")
.navigationDestination(for: Platform.self) { platform in
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 SettingView_Previews: PreviewProvider {
static var previews: some View {
SettingView()
}
}
struct Platform: Hashable {
let name: String
let imageName: String
let color: Color
}
struct Game: Hashable {
let name: String
let rating: String
}
추가로 다음처럼 좀 더 커스텀화해서 만들 수 있습니다.
import SwiftUI
struct StationView: 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: "God of War", rating: "98"),
.init(name: "Fortnite", rating: "92"),
.init(name: "Madden 2023", rating: "88")]
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
Section("Platforms") {
ForEach(platforms, id: \.name) { platform in
NavigationLink(value: platform) {
Label(platform.name, systemImage: platform.imageName)
.foregroundStyle(platform.color)
}
}
}
Section("Games") {
ForEach(games, id: \.name) { game in
NavigationLink(value: game) {
Text(game.name)
}
}
}
}
.navigationTitle("Gaming")
.navigationDestination(for: Platform.self) { platform in
ZStack {
platform.color.ignoresSafeArea()
VStack {
Label(platform.name, systemImage: platform.imageName)
.font(.largeTitle).bold()
List {
ForEach(games, id: \.name) { game in
NavigationLink(value: game) {
Text(game.name)
}
}
}
}
}
}
.navigationDestination(for: Game.self) { game in
VStack(spacing: 20, content: {
Text("\(game.name) - \(game.rating)")
.font(.largeTitle.bold())
Button("recommend game") {
path.append(games.randomElement()!)
}
Button("Go to another platform") {
path.append(platforms.randomElement()!)
}
Button("Go Home") {
path.removeLast(path.count)
}
})
}
}
}
}
struct SettingView_Previews: PreviewProvider {
static var previews: some View {
SettingView()
}
}
struct Platform: Hashable {
let name: String
let imageName: String
let color: Color
}
struct Game: Hashable {
let name: String
let rating: String
}
@State private var path = NavigationPath()
NavigationPath는 쌓여있는 뷰 스택이 저장되는 곳입니다.
.navigationDestination(for: Platform.self) { platform in
ZStack {
platform.color.ignoresSafeArea()
VStack {
Label(platform.name, systemImage: platform.imageName)
.font(.largeTitle).bold()
List {
ForEach(games, id: \.name) { game in
NavigationLink(value: game) {
Text(game.name)
}
}
}
}
}
}
.navigationDestination(for: Game.self) { game in
VStack(spacing: 20, content: {
Text("\(game.name) - \(game.rating)")
.font(.largeTitle.bold())
Button("recommend game") {
path.append(games.randomElement()!)
}
Button("Go to another platform") {
path.append(platforms.randomElement()!)
}
Button("Go Home") {
// path.removeLast(path.count)
path = .init()
}
})
}
위에 코드에서 List 내부에 NavigationLink(value: game) 부분은 어떻게 되는가 생각이 들었습니다.
해당 navigationDestination(for: Platform.self) 로 설정된 뷰에서 다른 값의 Link를 누르게 되면 해당 타입에 맞는 navigationDestination 이 맞춰서 실행됩니다.
다른 예시를 들어보겠습니다. 인스타그램의 회원가입을 가입하기 위해서는 다음과 같이 여러 개의 뷰들이 연속으로 등장합니다. 그리고
위에 뷰는 아래 코드와 같습니다. NavigationStack 으로 뷰를 이동하기 위해서는 NavigationLink 를 통해 destination view 를 직접 지정하는 방법과 또는 .navigationDesination(isPresented: _, detination: _) 를 사용하여 다른 뷰를 이동할 수 있습니다.
struct LoginView: View {
@State private var id: String = ""
@State private var password: String = ""
@State private var pressed = false
@Environment(Register.self) private var userinfo
var body: some View {
NavigationStack {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color.login, Color.loginColor2, Color.loginColor3]), startPoint: .topLeading, endPoint: .trailing)
.ignoresSafeArea()
VStack {
Spacer()
Image("instagram-icon")
.resizable()
.frame(width: 100, height: 100)
Spacer()
TextField("사용자 이름, 이메일 주소 또는 휴대폰 번호", text: $id)
.modifier(customTextFieldModifier(roundedCorners: 15, textColor: .defaultText))
.frame(maxWidth: .infinity)
.padding(.horizontal)
SecureField("비밀번호", text: $password)
.modifier(customTextFieldModifier(roundedCorners: 15, textColor: .defaultText))
.frame(maxWidth: .infinity)
.padding(.horizontal)
Button(action: {}, label: {
Text("로그인")
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 40)
})
.buttonStyle(customButtonStyle(labelColor: Color.white, backgroundColor: Color.accentColor, borderColor: Color.accentColor))
Button(action: {}, label: {
Text("비밀번호를 잊으셨나요?")
.foregroundStyle(Color.black)
.font(.callout)
})
.padding(.top, 5)
Spacer(minLength: 220)
NavigationLink {
NameInputView()
} label: {
Text("새 계정 만들기")
.frame(maxWidth: .infinity)
.frame(height: 40)
.contentShape(Capsule())
.background(Capsule().fill(Color.clear))
.scaleEffect(pressed ? 0.98 : 1.0)
.animation(.easeIn(duration: 0.2), value: pressed)
.opacity(pressed ? 0.9 : 1)
.overlay(
Capsule()
.stroke(Color.accentColor, lineWidth: 1.0)
.scaleEffect(pressed ? 0.98 : 1.0)
.animation(.easeIn(duration: 0.2), value: pressed)
.opacity(pressed ? 0.9 : 1)
)
}
.padding(.horizontal)
Text("made by taewon")
.padding(.top)
} //VSTACK
} //ZSTACK
} //NavigationStack
}
}
아래는 사용자 이름을 입력하는 뷰로 NavigationLink 를 사용하는 대신 .navigationDestination 으로 뷰를 이동하는 방법을 사용한 코드입니다.
struct NameInputView: View {
@Environment(Register.self) private var userinfo
@State private var username = ""
@State private var pressed = false
var buttonHeight: CGFloat = 40
var body: some View {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color.login, Color.loginColor2, Color.loginColor3]), startPoint: .topLeading, endPoint: .trailing)
.ignoresSafeArea()
GeometryReader(content: { geometry in
VStack {
HStack {
Text("이름 입력")
.font(.title3)
.bold()
Spacer()
}.padding()
HStack {
Text("친구들이 회원님을 찾을 수 있도록 이름을 추가하세요.")
.font(.callout)
Spacer()
}.padding(.horizontal)
TextField("성명", text: $username)
.modifier(customTextFieldModifier(roundedCorners: 15, textColor: .defaultText))
.frame(maxWidth: .infinity)
.padding(.horizontal)
Button(action: {
pressed.toggle()
}, label: {
Text("다음")
.frame(maxWidth: .infinity)
.frame(height: buttonHeight)
})
.buttonStyle(customButtonStyle(labelColor: Color.white, backgroundColor: Color.accentColor, borderColor: Color.accentColor))
.navigationDestination(isPresented: $pressed) {
PasswordInputView()
}
Spacer()
} //VSTACK
})
} //ZSTACK
}
}
pop to root
NavigationLink 에서 value 값을 통해서 .navigationDestination 에서 지정한 뷰로 해당 값을 넘길 수 있습니다. 해당 value 를 사용하지 않기 때문에 클로저에서 _ 로 value값을 받고 이동할 뷰를 정의합니다.
@State private var path: [String] = []
var body: some View {
NavigationStack(path: $path) {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color.login, Color.loginColor2, Color.loginColor3]), startPoint: .topLeading, endPoint: .trailing)
.ignoresSafeArea()
VStack {
Spacer()
Image("instagram-icon")
.resizable()
.frame(width: 100, height: 100)
Spacer()
TextField(text: $id, prompt: Text("사용자 이름, 이메일 주소 또는 휴대폰 번호").foregroundStyle(.gray)) {}.modifier(customTextFieldModifier(roundedCorners: 15, textColor: .black))
.frame(maxWidth: .infinity)
.padding(.horizontal)
SecureField(text: $password, prompt: Text("비밀번호").foregroundStyle(.gray)) {}
.modifier(customTextFieldModifier(roundedCorners: 15, textColor: .black))
.frame(maxWidth: .infinity)
.padding(.horizontal)
Button(action: { isLogged.isLogged = true }, label: {
Text("로그인")
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 40)
})
.buttonStyle(customButtonStyle(labelColor: Color.white, backgroundColor: Color.accentColor, borderColor: Color.accentColor))
Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, label: {
Text("비밀번호를 잊으셨나요?")
.foregroundStyle(Color.black)
.font(.callout)
})
.padding(.top, 5)
Spacer(minLength: 220)
//MARK: 계정 만드는 파트
NavigationLink(value: "LoginView") {
Text("새 계정 만들기")
.frame(maxWidth: .infinity)
.frame(height: 40)
.contentShape(Capsule()) // 버튼이 클릭될 프레임 선택
.background(Capsule().fill(Color.clear))
.scaleEffect(pressed ? 0.98 : 1.0)
.animation(.easeIn(duration: 0.2), value: pressed)
.opacity(pressed ? 0.9 : 1)
.overlay(
Capsule()
.stroke(Color.accentColor, lineWidth: 1.0)
.scaleEffect(pressed ? 0.98 : 1.0)
.animation(.easeIn(duration: 0.2), value: pressed)
.opacity(pressed ? 0.9 : 1)
)
.onTapGesture(perform: {
path.append("LoginView")
})
}.padding(.horizontal)
Text("made by taewon")
.padding(.top)
} //VSTACK
.navigationDestination(for: String.self) {_ in
NameInputView(path: $path)
}
} //ZSTACK
} //NavigationStack
}
이후 @Binding 을 사용하여 다음 뷰에서 path를 받아줍니다. path 에서 모든 값을 제거하게 된다면 root 뷰로 이동하게 되고 마지막 값을 지우게 되면 이전 뷰로 돌아가 되는데 이유는 navigation stack 이므로 스택 원리와 같기 때문입니다.
@Binding var path: [String]
var buttonHeight: CGFloat = 40
var body: some View {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color.login, Color.loginColor2, Color.loginColor3]), startPoint: .topLeading, endPoint: .trailing)
.ignoresSafeArea()
VStack {
HStack {
Text("이름 입력")
.font(.title3)
.bold()
Spacer()
}.padding()
HStack {
Text("친구들이 회원님을 찾을 수 있도록 이름을 추가하세요.")
.font(.callout)
Spacer()
}.padding(.horizontal)
TextField("성명", text: $username)
.modifier(customTextFieldModifier(roundedCorners: 15, textColor: .defaultText))
.frame(maxWidth: .infinity)
.padding(.horizontal)
Button(action: {
pressed.toggle()
}, label: {
Text("다음")
.frame(maxWidth: .infinity)
.frame(height: buttonHeight)
})
.buttonStyle(customButtonStyle(labelColor: Color.white, backgroundColor: Color.accentColor, borderColor: Color.accentColor))
.navigationDestination(isPresented: $pressed) {
PasswordInputView(path: $path)
}
Spacer()
Button(action: { path.removeAll()
}, label: {
Text("Button")
})
} //VSTACK
} //ZSTACK
}
'SwiftUI' 카테고리의 다른 글
Navigation Toolbar (0) | 2023.10.31 |
---|---|
List (0) | 2023.10.30 |
Computed Properties (0) | 2023.10.26 |
KeyPath, 키 경로 (0) | 2023.10.25 |
projectedValue (0) | 2023.10.25 |