내비게이션 스택을 이용해 콘텐츠 뷰들을 관리하는 컨테이너 뷰
스타일에 따라 UINavigationController 또는 UISplitViewController의 역할을 수행함
콘텐츠 뷰를 스택처럼 감싸주면 된다.
NavigationView {
Image("SwiftUI")
}
NavigationView {
Image("SwiftUI")
.navigationBarTitle("내비게이션 바 타이틀")
}
추가로 displayMode 설정도 가능하다.
내비게이션 바 타이틀은 꼭 내비게이션 뷰 안에서 사용해줘야 함
.navigationBarTitle("내비게이션 바 타이틀")
//automatic - 기본값
.navigationBarTitle("내비게이션 바 타이틀", displayMode: .large)
.navigationBarTitle("내비게이션 바 타이틀", displayMode: .inline)
UIBarButtonItem의 역할을 수행함
각 아이템을 버튼으로 정의하여 navigationBarItems 수식어에 전달
let leadingItem = Button(action : { print("Leading item tapped") }) {
Image(systemName: "bell").imageScale(.large)
}
let trailingItem = Button(action : { print("Trailing") }) {
Image(systemName: "gear").imageScale(.large)
}
return NavigationView {
Image("SwiftUI")
.navigationBarItems(leading: leadingItem, trailing: trailingItem)
.navigationBarTitle("내비게이션 바 아이템")
}
내비게이션 바 아이템은 HStack을 사용해서 여러개를 함께 넣어줄 수도 있다.
지정한 목적지로 이동할 수 있도록 만들어진 버튼
뷰를 눌렀을 때 또는 미리 지정된 특정 조건을 만족했을 때 화면을 전환함
내비게이션 스택에 뷰를 추가하여 내비게이션 계층 구조를 형성하는 데 사용
NavigationView {
NavigationLink(destination: Text("Destination View")) {
Image("SwiftUI")
}
.navigationBarTitle("내비게이션 링크")
}
내비게이션 링크에서도 버튼과 동일하게 이미지 랜더링 모드가 template으로 지정되어 원본 색상 정보를 잃어버릴 수도 있음
따라서, 버튼에서처럼 랜더링 모드를 지정하거나 PlainButtonStyle을 적용하여 본래 이미지 모습으로 돌릴 수 있음
NavigationLink(destination: Text("Destination View")) {
Image("SwiftUI")
.renderingMode(.original)
}
.buttonStyle(PlainButtonStyle())
랜더링 모드를 .original로 하면 원본 색상 유지가 가능
.template으로 지정하면 원본 색상 정보 상실
위 사진은 .template으로 랜더링 되어있어서 원본 색상 정보가 없어진 상태임
필요에 따라 내비게이션 바 또는 뒤로 가기 버튼을 숨길 수도 있다.
func navigationBarHidden(_hidden : Bool) -> some View
NavigationView {
.navigationBarTitle("내비게이션 바 히든")
.navigationBarHidden(true)
}
navigationBarBackButtonHidden 함수도 있는데, 이는 내비게이션 바에서 뒤로 가기 버튼을 숨기는 기능을 한다.
총 3가지의 스타일을 제공하며 navigationViewStyle 수식어를 이용해 원하는 스타일을 명시적으로 적용할 수 있다.
StackNavigationViewStyle : 첫째 뷰만 인식하고 나머지를 무시
DoubleColumnNavigationStyle : 첫째 뷰, 마지막 뷰만 인식
앞서 다뤘던 예제들은 모두 StackNavigationViewStyle에 해당함
NavigationView {
VStack(spacing: 20) {
NavigationLink(destination: Text("디테일 뷰 영역").font(.largeTitle)) {
Text("마스터 뷰 메뉴1").font(.title)
}
NavigationLink(destination: Text("디테일 뷰 영역").font(.largeTitle)) {
Text("마스터 뷰 메뉴2").font(.title)
}
}
.navigationBarTitle("네비게이션 뷰 스타일")
Text("디테일 뷰").font(.largeTitle)
}
<세로모드>
세로모드로 볼 때엔 Stack 내비게이션 스타일 때와 별 차이가 없어보임
<가로모드>
가로모드가 되면 좀 달라지는 것을 확인할 수 있다.
일단 내비게이션 바와 내비게이션 링크 버튼이 안보임
이들은, 가로상태에서 화면 좌측끝단->오른쪽 방향으로 밀면 나타난다.
또한, 내비게이션 링크 버튼 눌러도 창이 전환되지 않고, 옆에 그대로 뜨게 된다.
본 예제 기종은 아이폰 11프로맥스인데, 이 기종이 가로 모드일 때 사이즈 클래스가 Regular에 해당하기에 이러한 UI를 가지게 된다.
NavigationView { ... }
.navigationViewStyle(StackNavigationStyle())
내비게이션 뷰 스타일은 다음과 같이 내비게이션 뷰 밖에서 지정해준다.
하나의 열에 여러 개의 행으로 표현되는 UI를 구성해 다중 데이터를 쉽게 나열할 수 있도록 구성된 뷰
SwiftUI의 대표적인 예시이며
VStack와 자칫하면 헷갈릴 가능성이 높다
VStack은 단순히 세로 방향으로 뷰를 배치하지만, 리스트는 구분선과 같은 미리 정의된 UI를 통해 반복되는 뷰를 보기 쉽게 만들어준다.
List {
Text("1")
Text("2")
...
Text("10")
}
여담으로 리스트에서 뷰를 11개이상 넣으려고 할 시엔 오류가 발생한다.
List {
Text("List").font(.largeTitle)
Image("SwiftUI")
Circle().frame(width: 100, heigh: 100)
Color(.red).frame(width: 100, height: 100)
}
List(0...<100) {
Text("\($0)")
}
본 코드는 0~99까지의 값을 출력해주는 리스트이다.
여기서 범위 연산자는 (..<)만 사용이 가능하다.
RandomAccessCollection
RandomAccessCollection 프로토콜을 준수하는 데이터를 제공하는 것임
이 경우엔 데이터의 각 요소들을 구분하고 식별할 수 있도록 반드시 다음 2가지 방법 중 하나를 택해 id 값을 제공해야만 한다.
2.1 id 식별자 지정
id로 사용할 값을 직접 인수로 제공
id 매개변수엔 Hashable 프로토콜을 준수하는 프로퍼티 지정 가능
만일 데이터 타입 자체가 Hashable을 준수한다면 간단히 self라고 입력할 수도 있다.
2.2 identifier 프로토콜 채택
리스트처럼 id로 식별할 수 있는 데이터를 받아서 동적으로 뷰를 생성
전달받는 매개변수도 RandomAccessCollection 프로토콜이나 Range< Int > 타입을 사용한다
List {
ForEach(0..<50) {
Text("\(%0)")
}
}
List(0..<50) {
Text("\($0)")
}
위 코드들의 결과는 같지만, ForEach를 활용하면 정적 + 동적 컨텐츠를 만들 수 있어서 좋다.
let fruits = ["사과", "배", "포도", "바나나"]
let drinks = ["물","우유", "탄산수"]
var body: some View {
List {
Text("Fruits").font(.largeTitle)
ForEach(fruits, id: \.self) {
Text($0)
}
Text("Drinks").font(.largeTitle)
ForEach(drinks, id: \.self) {
Text($0)
}
}
}
}
리스트는 섹션을 이용해 데잍터를 쉽게 그룹화하는 것도 가능하다
struct Section<Parent, Content, Footer> { }
섹션에는 헤더와 푸터를 생략하거나 추가할 수 있고, 둘 중 하나만 사용할 수도 있다.
var body: some View {
let titles = ["Fruits", "Drinks"]
let data = [fruits, drinks]
return List {
ForEach(data.indices) { index in
Section(
header: Text(titles[index]).font(.title),
footer: HStack { Spacer(); Text("\(data[index].count)건") }
ForEach(data[index], id: \.self) {
Text($0)
}
}
}
}
}
리스트에서 상황에 맞는 스타일을 적용하는 방법
지금까지 살펴봤던 예제들은 모두 PlainListStyle에 해당하는 것이었음
1.GroupedListStyle
List { ... }
.lifeStyle(GroupedListStyle())
섹션이 그룹별로 명확히 나눴음을 확인할 수 있음
이와 유사하게 insetGroupedStyle도 존재한다.
한눈에 차이를 살표보면 아래와 같다.
위는 grouped, 아래는 insetGrouped 스타일이 적용된 상태이다.
기기마다 다른 사이즈 클래스를 가지기 때문에 사용되는 스타일이 위처럼 다르다.
기기의 사이즈 클래스로 인해 자동으로 지정되는 리스트 스타일을 강제적으로 변경도 가능하다.
List { ... }
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
위처럼 코드를 통해 가로일 때의 environment를 .regular로 설정하면 바꿀 수 있다.
자식 뷰에 부모 뷰와 기기에 대한 크기 및 좌표계 정보를 전달하는 기능을 수행하는 컨테이너 뷰
init(@ViewBuilder content: @escaping (GeometryProxy) -> Content)
여기서 content 매개변수는 지오메트리 프록시 타입의 정보를 받아 콘텐츠를 정의하는 함수를 전달한다.
content 매개 변수는 뷰 빌더 속성이 선언되어 있어 뷰를 나열하는 것만으로도 사용할 수 있다.
이때 뷰가 배열되는 방식은 ZStack으로 이뤄진다.
지오매트리 리더에 자식 뷰가 하나만 있을 때는 가운데 정렬
두 개 이상이 사용되면 최상단을 기준으로 배치
지오메트리 리더에 크기를 지정해주지 않으면 화면 전체 크기만큼 확장하게 된다.
지오메트리 프록시는 두 개의 프로퍼티와 하나의 메서드, 하나의 첨자를 제공하여 지오메트리 리더의 레이아웃 정보를 자식 뷰에 제공할 수 있다.
Frame
지오메트리 프록시는 프레임에 대한 정보도 제공하는데, 여기서 프레임은 단순히 그 자신의 CGRect값을 전달하는 것이 아니라 CoordinateSpace라는 열거형 타입이 가진 세가지 값 중 하나를 지정하면 그 좌표 공간에 관한 정보를 반환한다.
global : 화면 전체 영역을 지준으로 한 좌표 정보
local : 지오메트리 리더의 bounds를 기준으로 한 좌표 정보
named : 명시적으로 이름을 할당한 공간을 기준으로 한 좌표 정보
var body : some View {
HStack {
Rectangle().fill(Color.yellow).frame(Width:30)
VStack {
Rectangle().fill(Color.blue).frame(height: 200)
GeometryReader {
self.contents(geometry: $0)
}
.background(Color.green)
.border(Color.red, width: 4)
}
.coordinateSpace(name: "VStackCS")
}
.coordinateSpace(name: "HStackCS")
}
func contents(geometry g: GeometryProxy) -> some View {
VStack {
Text("Local").bold()
Text(stringFormat(for: g.frame(in: .local).origin))
.padding(.bottom)
Text("Global").bold()
Text(stringFormat(for: g.frame(in: .global).origin))
.padding(.bottom)
Text("Global").bold()
Text(stringFormat(for: g.frame(int: .global).origin))
.padding(.bottom)
Text("Named VStackCS").bold()
Text(stringFormat(for: g.frame(in: .named("VStackCS").origin))
.padding(.bottom)
Text("Named HStackCS").bold()
Text(stringFormat(for: g.frame(in: .named("HStackCS")).origin))
}
}
func stringFormat(for print: CGPoint) -> String {
String(format: "(x: %f, y: %f)", arguments: [point.x, point.y])
}