지난 시간에 NavigationStack에 대한 기본적인 표현들을 알아봤습니다!
먼저 탭뷰와 함께 NavigationStack을 사용하게 될 경우를 살펴볼게요
처음에 어떤 식으로 Navigation을 사용할지 모를 때는
각각의 탭들을 네비게이션View로 감싸줬었는데 탭들이 네비게이션 된 후에도 살아있게 되더라구요
그래서 방법을 찾다가 NavigationStack을 제일 최상단으로 빼주게 되었습니다.
struct ChampionModel: Identifiable, Hashable {
let id = UUID().uuidString
let name: String
let dialogue: String
let imageURL: String
}
extension ChampionModel {
static let mockChampions: [ChampionModel] = [
ChampionModel(name: "아트록스", dialogue: "종말을 내려주마.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Aatrox_0.jpg"),
ChampionModel(name: "아리", dialogue: "똑똑한 여우는 절대 잡히지 않거든.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Ahri_7.jpg"),
ChampionModel(name: "애쉬", dialogue: "계속 나아가야 해.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Ashe_9.jpg"),
ChampionModel(name: "다이애나", dialogue: "밤이 오면 달이 떠오르지.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Diana_2.jpg"),
ChampionModel(name: "카이사", dialogue: "이게 바로 내 존재의 이유다!", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Kaisa_1.jpg"),
ChampionModel(name: "케일", dialogue: "날 두려워하라", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Kayle_1.jpg"),
ChampionModel(name: "룰루", dialogue: "보라색 맛났어!", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Lulu_3.jpg"),
ChampionModel(name: "라이즈", dialogue: "대상혁", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Ryze_3.jpg"),
ChampionModel(name: "자르반", dialogue: "내 의지로, 여기서 끝을 보겠노라!", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/JarvanIV_9.jpg"),
]
}struct HomeView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(ChampionModel.mockChampions) { champion in
NavigationLink(value: champion) {
HomeRowView(champion: champion)
}
.tint(.primary)
}
}
.padding(.bottom, 50)
}
.navigationDestination(for: ChampionModel.self) { champion in
HomeRowDestinationView(champion: champion)
}
}
}
struct HomeRowView: View {
let champion: ChampionModel
var body: some View {
VStack {
HStack(alignment: .top) {
Circle()
.frame(width: 30, height: 30)
VStack(alignment: .leading) {
Text(champion.name)
.font(.headline)
Text(champion.dialogue)
.font(.body)
AsyncImage(url: URL(string: champion.imageURL)) { image in
image
.resizable()
.scaledToFit()
.cornerRadius(10)
} placeholder: {
ProgressView()
}
.frame(width: 320, height: 190)
}
}
.padding()
Divider()
}
}
}struct HomeRowDestinationView: View {
let champion: ChampionModel
var body: some View {
VStack(spacing: 20) {
AsyncImage(url: URL(string: champion.imageURL)) { image in
image
.resizable()
.scaledToFit()
.cornerRadius(10)
} placeholder: {
ProgressView()
}
.frame(width: 320, height: 190)
Text(champion.name)
.font(.largeTitle)
NavigationLink(value: champion.name) {
Text("스킨 사기")
.font(.title)
.bold()
}
}
.navigationDestination(for: String.self) { name in
Text(name).font(.largeTitle).bold() + Text(" 스킨을 사시겠어요?")
}
}
}struct HomeSkinBuyView: View {
let champion: ChampionModel
var body: some View {
Text(champion.name).font(.largeTitle).bold() + Text(" 스킨을 사시겠어요?")
}
}위의 HomeRowDestinationView의 destination을 교체 해주고!
enum ViewOptions: Hashable {
case homeFirst(champion: ChampionModel)
case homeSecond(champion: ChampionModel)
@ViewBuilder func view() -> some View {
switch self {
case .homeFirst(let champion): HomeRowDestinationView(champion: champion)
case .homeSecond(let champion): HomeSkinBuyView(champion: champion)
}
}
}NavigationLink(value: ViewOptions.homeFirst(champion: champion)) {
HomeRowView(champion: champion)
}
.tint(.primary)value에 들어가는 값을 ViewOptions로 만들어줍니다!
struct ContentView: View {
@State var currentTab: Tab = .home
@State var path: NavigationPath = .init()
이렇게 되면 path를 매번 넘겨주는 상황이 발생한다
- EnvironmentObject로 넘겨버리자!
NavigationPath를 관리해주는 객체를 만드는 아이디어!
final class NavigationPathFinder: ObservableObject {
static let shared = NavigationPathFinder()
private init() { }
@Published var path: NavigationPath = .init()
func addPath(option: ViewOptions) {
path.append(option)
}
func popToRoot() {
path = .init()
}
}NavigationLink를 모두 Button으로 바꿔준 뒤 action에 path 아이템이 추가되게!
- 애플 스크럼딩거
struct ScrumsView: View {
@Binding var scrums: [DailyScrum]
@Environment(\.scenePhase) private var scenePhase
@State private var isPresentingNewScrumView = false
let saveAction: () -> Void
var body: some View {
NavigationStack {
List($scrums) { $scrum in
NavigationLink(destination: DetailView(scrum: $scrum)) {
CardView(scrum: scrum)
}
.listRowBackground(scrum.theme.mainColor)
}
.navigationTitle("Daily Scrums")
.toolbar {
Button {
isPresentingNewScrumView = true
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("New Scrum")
}
}
.sheet(isPresented: $isPresentingNewScrumView) {
NewScrumSheet(scrums: $scrums, isPresentingNewScrumView: $isPresentingNewScrumView)
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}연관값으로 바인딩 되는 값을 넘기는 게 불가능하다!
-> Navigation이 된 뷰에서 데이터의 업데이트가 일어났을 때 -> 부모뷰에서 알 수 없다! -> Reload해주거나 array 모델을 업데이트 해줘야함
- 메인 파일
import SwiftUI
@main
struct NavigationStackAndTabsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(NavigationPathFinder.shared)
}
}
}- ContentView (정리는 해야합니다!)
enum ViewOptions: Hashable {
case homeFirst(champion: ChampionModel)
case homeSecond(champion: ChampionModel)
@ViewBuilder func view() -> some View {
switch self {
case .homeFirst(let champion): HomeRowDestinationView(champion: champion)
case .homeSecond(let champion): HomeSkinBuyView(champion: champion)
}
}
}
final class NavigationPathFinder: ObservableObject {
static let shared = NavigationPathFinder()
private init() { }
@Published var path: NavigationPath = .init()
func addPath(option: ViewOptions) {
path.append(option)
}
func popToRoot() {
path = .init()
}
}
struct ContentView: View {
@State var currentTab: Tab = .home
@EnvironmentObject var navPathFinder: NavigationPathFinder
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
NavigationStack(path: $navPathFinder.path) {
ZStack(alignment: .bottom) {
TabView(selection: $currentTab) {
HomeView()
.tag(Tab.home)
Text("게시판뷰")
.tag(Tab.forum)
Text("스터디뷰")
.tag(Tab.study)
Text("프로필뷰")
.tag(Tab.profile)
}
CustomTabView(currentTab: $currentTab)
}
.navigationDestination(for: ViewOptions.self) { option in
option.view()
}
}
}
}
struct ChampionModel: Identifiable, Hashable {
let id = UUID().uuidString
let name: String
let dialogue: String
let imageURL: String
}
extension ChampionModel {
static let mockChampions: [ChampionModel] = [
ChampionModel(name: "아트록스", dialogue: "종말을 내려주마.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Aatrox_0.jpg"),
ChampionModel(name: "아리", dialogue: "똑똑한 여우는 절대 잡히지 않거든.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Ahri_7.jpg"),
ChampionModel(name: "애쉬", dialogue: "계속 나아가야 해.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Ashe_9.jpg"),
ChampionModel(name: "다이애나", dialogue: "밤이 오면 달이 떠오르지.", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Diana_2.jpg"),
ChampionModel(name: "카이사", dialogue: "이게 바로 내 존재의 이유다!", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Kaisa_1.jpg"),
ChampionModel(name: "케일", dialogue: "날 두려워하라", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Kayle_1.jpg"),
ChampionModel(name: "룰루", dialogue: "보라색 맛났어!", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Lulu_3.jpg"),
ChampionModel(name: "라이즈", dialogue: "대상혁", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Ryze_3.jpg"),
ChampionModel(name: "자르반", dialogue: "내 의지로, 여기서 끝을 보겠노라!", imageURL: "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/JarvanIV_9.jpg"),
]
}
struct HomeView: View {
@EnvironmentObject private var navPathFinder: NavigationPathFinder
var body: some View {
ScrollView {
LazyVStack {
ForEach(ChampionModel.mockChampions) { champion in
Button {
navPathFinder.addPath(option: .homeFirst(champion: champion))
} label: {
HomeRowView(champion: champion)
}
.tint(.primary)
}
}
.padding(.bottom, 50)
}
}
}
struct HomeRowView: View {
let champion: ChampionModel
var body: some View {
VStack {
HStack(alignment: .top) {
Circle()
.frame(width: 30, height: 30)
VStack(alignment: .leading) {
Text(champion.name)
.font(.headline)
Text(champion.dialogue)
.font(.body)
AsyncImage(url: URL(string: champion.imageURL)) { image in
image
.resizable()
.scaledToFit()
.cornerRadius(10)
} placeholder: {
ProgressView()
}
.frame(width: 320, height: 190)
}
}
.padding()
Divider()
}
}
}
struct HomeRowDestinationView: View {
@EnvironmentObject var navPathFinder: NavigationPathFinder
let champion: ChampionModel
var body: some View {
VStack(spacing: 20) {
AsyncImage(url: URL(string: champion.imageURL)) { image in
image
.resizable()
.scaledToFit()
.cornerRadius(10)
} placeholder: {
ProgressView()
}
.frame(width: 320, height: 190)
Text(champion.name)
.font(.largeTitle)
Button {
navPathFinder.addPath(option: .homeSecond(champion: champion))
} label: {
Text("스킨 사기")
.font(.title)
.bold()
}
}
}
}
struct HomeSkinBuyView: View {
@EnvironmentObject var navPathFinder: NavigationPathFinder
let champion: ChampionModel
var body: some View {
VStack(spacing: 30) {
Text(champion.name).font(.largeTitle).bold() + Text(" 스킨을 사시겠어요?")
Button("안 살래요") {
navPathFinder.popToRoot()
}
}
}
}