네비게이션 기능 구현을 위해서는 NavigationStack에 뷰를 임베딩하면 된다.

  1. navigationTitle로 네비게이션 뷰 타이틀을 지정한다.
  2. toolbar Visiblity로 네비게이션 바 노출 여부를 지정한다.
  3. toolbar content 클로저에 뷰를 전달하여 네비게이션 바 UI를 구성한다.
  4. ToolBarItemGroup과 placement를 조합하여 네비게이션 바 내의 요소들의 위치를 조정한다.
NavigationStack {
    VStack {
        Text("Hello, World!")

        Button(action: {
            isHidden.toggle()
        }, label: {
            Text("Button")
        })
        .padding()
    }
    .navigationTitle("Hello")
    .toolbar(isHidden ? .hidden : .visible, for: .automatic)
    .toolbar {
        ToolbarItemGroup(placement: .topBarLeading) {
            HStack {
                Button(action: {
                    isHidden.toggle()
                }, label: {
                    Text("Button")
                })
                .padding()
            }
        }

        ToolbarItemGroup(placement: .topBarTrailing) {
            HStack {
                Button(action: {
                    isHidden.toggle()
                }, label: {
                    Text("Button")
                })
                .padding()
            }
        }
    }
}

# Push

네비게이션 푸시는 NavigationLink를 스택 내에 추가하면 된다.

NavigationStack {
    VStack {
        Text("Hello, World!")

        Button(action: {
            push = true
        }, label: {
            Text("Button")
        })
        .padding()

        NavigationLink("Push") {
            EmojiView(emoji: Emoji.smile.rawValue)
                .padding()
                .navigationTitle(Emoji.smile.rawValue)
        }
    }
    .navigationTitle("Conditional Push")
}

링크 컨텐츠에 추가한 뷰가 링크 버튼 탭 이후 화면이 푸시되면서 전달된다. 푸시된 화면 내에서 백버튼 탭으로 직접 pop하는 방법 외에 툴바에 직접 닫기 버튼을 정의해도 된다. @Environment 프로퍼티 래퍼를 사용하면 된다.

struct EmojiView: View {

    @Environment(\.dismiss) var dismiss

    var emoji: String

    var body: some View {
        Text(emoji)
            .font(.system(size: 200))
            .toolbar {
                Button {
                    dismiss()
                } label: {
                    Text("Close")
                }
            }
    }
}

# Displace

화면 Push이외에도 화면을 표시하는 다른 방식이 존재하는데, 그 중 하나로 Displace가 있다. 화면을 스택에 푸시하거나 팝하는 형태가 아닌 교체하는 방식으로 동작한다. 이러한 네비게이션 스타일을 Column Style이라고 한다.

화면이 넓은 기종에서 동작한다. 기존에는 NavigationView 기반으로 동작했는데, deprecated될 예정이어서 NavigationSplitView로 구현해야 한다.

NavigationSplitView(sidebar: {
    List {
        ForEach(Emoji.allCases, id: \.self) { emoji in
            NavigationLink(emoji.rawValue) {
                EmojiView(emoji: emoji.rawValue)
            }
        }
    }
    .navigationTitle("Emoji")
}, detail: {
    ZStack {
        Color.yellow

        Text("Secondary Scene")
            .font(.largeTitle)
    }
    .ignoresSafeArea()
})

사이드바 파라미터에 화면 교체를 위한 인터랙션 목록을 전달하고, 디테일 뷰에서 첫 화면에 표시할 뷰를 구현한다. 네비게이션 링크에 전달되는 뷰가 detail에서 표시할 뷰를 디스플레이스하는 방식으로 동작한다.

# 탭뷰

탭뷰를 통해 탭 기능을 구현할 수 있다. 탭뷰 안에 탭을 통한 전환 대상 뷰들을 정의해둔다. selection 파라미터에 인덱스와 관련된 바인딩 속성을 전달하고 각 탭뷰 요소에 tag 모디파이어를 추가하면 태그 속성값이 selection에 바인딩된다.

기본적으로 selectedIndex는 구현되지 않기 때문에 직접 태그를 정의해야 한다.

TabView(selection: $selectedIndex) {
    SymbolScene(name: "star", color: Color.red)
        .tabItem { Label("Star", systemImage: "star") }
        .tag(0)

    SymbolScene(name: "heart", color: Color.green)
        .tabItem { Label("heart", systemImage: "heart") }
        .tag(1)

    SymbolScene(name: "play", color: Color.blue)
        .tabItem { Label("Play", systemImage: "play") }
        .tag(2)
}
.onChange(of: selectedIndex) { oldValue, newValue in
    print(newValue)
}

# Selection Binding

탭뷰 셀렉션 바인딩을 통해 탭바 컨트롤러를 통한 화면 전환이 아닌 탭뷰 내에서 화면 전환도 구현할 수 있다.

struct SymbolScene: View {
    @Binding var selectedIndex: Int
    var name: String
    var color: Color


    var body: some View {
        VStack {
            Spacer()

            Image(systemName: name)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 200, height: 200)
                .foregroundColor(color)

            Button(action: {
                selectedIndex = selectedIndex >= 2 ? 0 : selectedIndex + 1
            }, label: {
                Text("Button")
            })

            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

위와 같이 바인딩속성을 추가하여 상위 탭뷰에서 각 탭뷰 요소에 생성자 파라미터로 바인딩을 전달하면 된다.

TabView(selection: $selectedIndex) {
    // selectedIndex를 바인딩으로 전달
    SymbolScene(selectedIndex: $selectedIndex, name: "star", color: Color.red)
        .tabItem { Label("Star", systemImage: "star") }
        .tag(0)

    SymbolScene(selectedIndex: $selectedIndex, name: "heart", color: Color.green)
        .tabItem { Label("heart", systemImage: "heart") }
        .tag(1)

    SymbolScene(selectedIndex: $selectedIndex, name: "play", color: Color.blue)
        .tabItem { Label("Play", systemImage: "play") }
        .tag(2)
}
.onChange(of: selectedIndex) { oldValue, newValue in
    print(newValue)
}