SwiftUI: 내비게이션 (Navigation) 및 데이터 전달 기초
소개
SwiftUI상에서 내비게이션 (앞, 뒤로 이동하는 뷰들의 집합)을 구현하는 방법입니다. 참고로 iOS 16.0을 기준으로 방법이 매우 다릅니다.
아래와 같은 앱을 만들 것입니다. 3단계 내비게이션이 마련되어 있으며
- 첫 번째 버튼을 누르면 두 번째 화면으로 이동하며 임의의 색상과 그 색상에 대한 텍스트가 표시됩니다.
- 두 번째 화면에서는
Go to Jacob Link가 있으며 이 링크를 클릭하면 다음 세 번째 화면으로 이동합니다. 또는< Back 버튼을 누르면 뒤로 이동합니다. - 세 번째 화면에서는
Jacob [색상]이라는 이름이 표시됩니다.
http://www.giphy.com/gifs/UjmLm56fvyQJiaPRjs
또한
- 첫 번째 화면에서
[파발]기능이 있어서 3번째 화면으로 파발 메시지를 전달할 수 있습니다. - 세 번째 화면에서
[왜곡]버튼을 누르면 파발 메시지가 왜곡되어 첫 번째 화면으로 재전달됩니다.
http://www.giphy.com/gifs/TJikax42WUZzg13lsu
iOS 16.0 이상에서 내비게이션: NavigationStack
NavigationStack을 사용한 뒤 안에 Button 또는 List를 넣어 사용합니다. 일반적으로 List를 사용합니다.
먼저 NavigationStack을 body의 최외곽에 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var body: some View {
// ... //
if #available(iOS 16.0, *) {
NavigationStack {
List {
// ...NavigationLink들 추가... //
}
// List 아래 위치
.navigationTitle("조선시대 성문") // 내비게이션 창 제목
}
} else {
// Fallback on earlier versions
}
}
다음 List 안에 NavigationLink(_:value:)를 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
NavigationStack {
List {
// NavigationLink은 밖에 있으면 버튼, List 안에 있으면 목록 형태로 자동 변형
NavigationLink("동대문", value: Color.mint)
NavigationLink("서대문", value: Color.pink)
NavigationLink("남대문", value: Color.cyan)
NavigationLink("북대문", value: Color.teal)
TextField("파발로 보낼 말을 입력", text: $message)
}
// ... //
}
List는 테이블 뷰와 비슷한 모양과 용도의 목록을 만들 수 있습니다.NavigationLink는 내비게이션 내에서 다른 뷰로 이동할 수 있는 링크를 제공합니다. 기본적으로 버튼 형태로 있으며,List안에 있으면 누를 수 있는 목록 형태로 자동으로 변이합니다.NavigationLink의 첫 번째 파라미터는 버튼(링크) 이름, 두 번째value:파라미터는 다음으로 이동하는 View에 전달할 값을 입력합니다. 다양한 타입의 값을 입력하는 것이 가능합니다.- 위의 예제에서는
Color타입의 값을 전달합니다.
- 위의 예제에서는
[caption id=”attachment_5426” align=”alignnone” width=”193”]
List와 NavigationLink[/caption]
위의 NavigationLink를 클릭하면 수행할 작업을 추가합니다. navigationDestination이라는 메서드를 List 바로 다음(밑)에 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
@State var message: String = ""
// ... //
List {
// ... //
}
// List 아래 위치
.navigationTitle("조선시대 성문")
.navigationDestination(for: Color.self) { color in
ColorDetail(color: color, message: $message)
}
- for
- 값을 받아들일 타입을 입력합니다.
Color값을 받을 예정이므로Color.self를 입력합니다.
destination(트레일링 클로저)ColorDetail뷰(후술)로 이동합니다.color파라미터와message파라미터가 있으며color에는NavigationLink에서 지정한value값,message는 파발 메시지에 대한 바인딩(Binding) 값을 입력합니다.- 참고) SwiftUI: @State와 @Binding의 의미 / 뷰 간의 데이터 전송 (앞→뒤, 뒤→앞)
[동대문] 버튼을 누르면 Color.mint 값이 전달되며 다음 뷰로 넘어갑니다.
ColorDetail.swift (두 번째 화면)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct ColorDetail: View {
var color: Color
@Binding var message: String
@Environment(\.dismiss) var dismiss
var body: some View {
Text("\(color.description)")
.foregroundColor(color)
Divider()
if #available(iOS 16.0, *) {
// NavigationStack or NavigationView 없어도 됨
NavigationLink("Go to Jacob Link", value: color.description).navigationDestination(for: String.self) { lastName in
JacobDetail(lastName: lastName.capitalized, message: $message)
}
} else {
// Fallback on earlier versions
}
Divider()
Button {
dismiss()
} label: {
Label("Back", systemImage: "chevron.left")
}
}
}
color: 앞에서 전달한Color값message: 다음 뷰로 전달할 파발 메시지 (@Binding으로 연결)dismisssheetView,fullScreenCover에서는 창을 닫는 역할- 내비게이션에서는 현재 뷰를 종료하고 이전 뷰로 돌아가는 역할(뒤로가기)을 합니다.
- NavigationLink
JacobDetail뷰(후술, 3번째 화면)으로 이동합니다.
JacobDetail.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct JacobDetail: View {
var lastName: String
@Binding var message: String
@Environment(\.dismiss) var dismiss
var body: some View {
Text("Jacob \(lastName)")
Text("파발: \(message)")
Button("왜곡") {
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
message = String((0..<8).map {_ in letters.randomElement()!})
}
}
}
- message = …
@Binding으로 연결되어 있으므로 여기서message의 값을 변경하면 맨 첫번째 뷰의message도 변경이 됩니다.
http://www.giphy.com/gifs/TJikax42WUZzg13lsu
iOS 16.0 미만에서 내비게이션: NavigationView
NavigationStack은 iOS 16.0 이상에서만 사용할 수 있으므로 단독으로 사용할 수 없습니다. 그 미만의 버전에서는 NavigationView를 사용해야 합니다.
또한 윗 섹션에서 사용된 NavigationLink(_:value:) 마찬가지로 iOS 16.0 이상에서만 사용할 수 있으므로 대신 NavigationLink(_:destination:)을 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if #available(iOS 16.0, *) {
// ... //
} else {
// Fallback on earlier versions
NavigationView {
List {
NavigationLink("동대문") {
ColorDetail(color: Color.mint, message: $message)
}
NavigationLink("서대문") {
ColorDetail(color: Color.pink, message: $message)
}
NavigationLink("남대문") {
ColorDetail(color: Color.cyan, message: $message)
}
NavigationLink("북대문") {
ColorDetail(color: Color.teal, message: $message)
}
TextField("파발로 보낼 말을 입력", text: $message)
}
.navigationTitle("조선시대 성문")
.navigationViewStyle(.stack) // 추가
}
}
ColorDetail.swift도 이전 버전이 호환되도록 else 문을 추가합니다.
1
2
3
4
5
6
7
8
9
10
if #available(iOS 16.0, *) {
// NavigationStack or NavigationView 없어도 됨
NavigationLink("Go to Jacob Link", value: color.description).navigationDestination(for: String.self) { lastName in
JacobDetail(lastName: lastName.capitalized, message: $message)
}
} else {
// Fallback on earlier versions
NavigationLink("Go to Jacob Link") {
JacobDetail(lastName: color.description.capitalized, message: $message)
}}
https://giphy.com/gifs/iIDyddQQzHTqLqMv18
전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import SwiftUI
// 1st nav
struct ColorDetail: View {
var color: Color
@Binding var message: String
@Environment(\.dismiss) var dismiss
var body: some View {
Text("\(color.description)")
.foregroundColor(color)
Divider()
if #available(iOS 16.0, *) {
// NavigationStack or NavigationView 없어도 됨
NavigationLink("Go to Jacob Link", value: color.description).navigationDestination(for: String.self) { lastName in
JacobDetail(lastName: lastName.capitalized, message: $message)
}
} else {
// Fallback on earlier versions
// JacobDetail에서 Binding시 뒤로가기 오류를 방지하기 위해 추가 (NavigationView)
NavigationView {
NavigationLink("Go to Jacob Link") {
JacobDetail(lastName: color.description.capitalized, message: $message)
}
}
}
Divider()
Button {
dismiss()
} label: {
Label("Back", systemImage: "chevron.left")
}
}
}
// 2nd nav
struct JacobDetail: View {
var lastName: String
@Binding var message: String
var body: some View {
Text("Jacob \(lastName)")
Text("파발: \(message)")
Button("왜곡") {
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
message = String((0..<8).map {_ in letters.randomElement()!})
}
}
}
struct NavStackView: View {
@State var message: String = ""
var body: some View {
Image("동대문")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: 150)
if #available(iOS 16.0, *) {
NavigationStack {
List {
// NavigationLink은 밖에 있으면 버튼, List 안에 있으면 목록 형태로 자동 변형
NavigationLink("동대문", value: Color.mint)
NavigationLink("서대문", value: Color.pink)
NavigationLink("남대문", value: Color.cyan)
NavigationLink("북대문", value: Color.teal)
TextField("파발로 보낼 말을 입력", text: $message)
}
// List 아래 위치
.navigationTitle("조선시대 성문")
.navigationDestination(for: Color.self) { color in
ColorDetail(color: color, message: $message)
}
}
// .transition(.identity)
} else {
// Fallback on earlier versions
NavigationView {
List {
NavigationLink("동대문") {
ColorDetail(color: Color.mint, message: $message)
}
NavigationLink("서대문") {
ColorDetail(color: Color.pink, message: $message)
}
NavigationLink("남대문") {
ColorDetail(color: Color.cyan, message: $message)
}
NavigationLink("북대문") {
ColorDetail(color: Color.teal, message: $message)
}
TextField("파발로 보낼 말을 입력", text: $message)
}
.navigationTitle("조선시대 성문")
.navigationViewStyle(.stack)
}
}
}
}
struct NavStackView_Previews: PreviewProvider {
static var previews: some View {
NavStackView()
}
}

