Skip to content

Instantly share code, notes, and snippets.

@woozoobro
Last active February 29, 2024 07:29
Show Gist options
  • Select an option

  • Save woozoobro/c2a78465d2dd098932bccae7a7188ecc to your computer and use it in GitHub Desktop.

Select an option

Save woozoobro/c2a78465d2dd098932bccae7a7188ecc to your computer and use it in GitHub Desktop.
NavigationPath활용

지난 시간에 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()
        }
    }
}

HomeRowDestination

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(" 스킨을 사시겠어요?")
        }
    }
}

HomeSkinBuyView

struct HomeSkinBuyView: View {
    let champion: ChampionModel
    var body: some View {
        Text(champion.name).font(.largeTitle).bold() + Text(" 스킨을 사시겠어요?")
    }
}

위의 HomeRowDestinationView의 destination을 교체 해주고!

Destination이 될 View들을 enum으로 선언!

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 변경해주기!

NavigationLink(value: ViewOptions.homeFirst(champion: champion)) {
    HomeRowView(champion: champion)
}
.tint(.primary)

value에 들어가는 값을 ViewOptions로 만들어줍니다!

path로 popToRoot 만들어주기!

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()
            }

        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment