You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NavigationStack跳转异常:PopularMoviesView重现后进入DetailView

SwiftUI导航异常问题:PopularMoviesView重复入栈

问题描述

期望导航流程

  • HomeView → DetailView
  • HomeView → PopularMoviesView → DetailView

实际异常现象

第一种流程执行正常,但第二种流程中:从HomeView进入PopularMoviesView无问题,点击PopularMoviesView中的跳转按钮进入DetailView时,PopularMoviesView会被重复推入导航栈,点击返回按钮后才会显示DetailView。实际执行的导航流程变为:

  • HomeView → DetailView
  • HomeView → PopularMoviesView → PopularMoviesView → DetailView

相关代码

HomeView

struct HomeView: View {

    @State var moviesListViewModel = MoviesListViewModel()   
    
    var body: some View {
        NavigationStack{
            ZStack {
                Color.black.opacity(0.9)
                    .ignoresSafeArea(edges: .all)
                VStack {
                    ScrollView{
                        MoviesScrollView()
                            .environment(moviesListViewModel)
                    }
                    .frame(maxWidth:.infinity,maxHeight:.infinity,alignment: .topLeading)
                }
                .navigationDestination(for: Movie.self, destination: { item in
                    MovieDetailView(movieDetails: item)
                        .navigationBarBackButtonHidden()
                })
            }
        }
        
    }
}

MovieDetailView

struct MovieDetailView: View {
    
    @State var videoPlayerViewModel = VideoPlayerViewModel()
    @State var movieCastViewModel = MovieCastViewModel()
    
    let movieDetails:Movie
    
    @Environment(\.dismiss) var dismiss
    
    init(movieDetails: Movie) {
        self.movieDetails = movieDetails
        print("Movie Detail Screen")
    }

    var body: some View {
        
        ZStack{
            Color(.black.opacity(0.9))
                .ignoresSafeArea()
        
            VStack {
                
                HStack{
                    Image(systemName: "arrow.left")
                        .onTapGesture {
                            dismiss()
                        }
                    Text(movieDetails.originalTitle)
                        .multilineTextAlignment(.leading)
                        .lineLimit(1)
                    
                }
                .frame(maxWidth: .infinity,alignment:.leading)
                .padding(.leading)
                .foregroundStyle(.white)
                .bold()
                .font(.title2)
                
                ScrollView(.vertical){
                    VStack{
                        VStack(spacing:0) {
                            Text(movieDetails.originalTitle)
                                .font(.largeTitle)
                                .padding(.bottom,5)
                                .padding(.top,25)
                                .frame(maxWidth: .infinity,alignment:.leading)
                            
                            HStack{
                                Text(movieDetails.releaseDate)
                                Text(movieDetails.adult ? "Rated" : "Not Rated")
                                    .frame(maxWidth: .infinity,alignment:.leading)
                                
                            }
                            
                            .foregroundStyle(.gray)
                        }
                        .frame(maxWidth: .infinity,alignment:.leading)
                        .padding(.leading)
                        .bold()
                        
                        
                        ExtractedVideoView()
                            .environment(videoPlayerViewModel)
                        
                        
                        HStack{
                            LazyImage(source: "https://image.tmdb.org/t/p/w500/\(movieDetails.posterPath)") { state in
                                if let image = state.image{
                                    image
                                        .resizingMode(.aspectFit)
                                        .aspectRatio(contentMode: .fill)
                                }
                            }
                            .scaledToFill()
                            .frame(width: 150)
                            NavigationLink(value: movieDetails.overview) {
                                Text(movieDetails.overview)
                                    .multilineTextAlignment(.leading)
                                    .fixedSize(horizontal: false, vertical: true)
                                    .frame(maxWidth: .infinity,alignment:.leading)
                                    .frame(width: 200,height: 200)
                                    .foregroundStyle(.white)
                                    .lineLimit(6)
                            }
                        }
                       
                        .padding(.top)
                        .padding(.horizontal)
                        
                        HStack {
                            Spacer()
                            VStack{
                                Image(systemName: "star.fill")
                                    .foregroundStyle(Color("yellowColor"))
                                    .bold()
                                    .font(.title2)
                                Text("\(String(format: "%.1f", movieDetails.voteAverage))/10")
                                    .bold()
                                    .font(.title2)
                                Text("\(movieDetails.voteCount)")
                                    .foregroundStyle(.gray)
                            }
                          
                            Spacer()
                            
                            VStack{
                                Image(systemName: "star")
                                Text("Rate this")
                                    .padding(.bottom)
                            }
                            
                            .font(.title2)
                            .foregroundStyle(.blue)
                            Spacer()
                        }
                        
                        HeadingView(title: "Cast", subTitle: "Cast For This Movie")
                        
                        CastView()
                            .environment(movieCastViewModel)
                    }
                    .navigationDestination(for: String.self, destination: { overview in
                        SummaryView(summanry: overview)
                    })
                    .foregroundStyle(.white)
                    .frame(maxWidth: .infinity,maxHeight: .infinity,alignment:.topLeading)
                }
                .scrollContentBackground(.hidden)
                .scrollIndicators(.hidden)
            }
            
            
        }
        .task{
            Task{
                try await videoPlayerViewModel.getVideoDetails(videoID: movieDetails.id)
                try await movieCastViewModel.getMovieCastList(movieID: movieDetails.id)
            }
        }
        .onAppear{
            print(movieDetails.posterPath)
        }
        .navigationBarBackButtonHidden()
        
        
    }
    
}

PopularMoviesView

struct PopularMovies: View {
   
    
    init(){
        print("Poppular Movies Screen")
    }
 
    
    @State var isLoading:Bool = true
    @State var pageNumber:Int = 1
    @Environment(MoviesListViewModel.self) var moviesListViewModel
    @Environment(\.dismiss) var dismiss
    var body: some View {
        ZStack{
            Color.black.opacity(0.9)
                .ignoresSafeArea(edges: .all)
            VStack{
                HStack{
                    Image(systemName: "arrow.left")
                        .imageScale(.large)
                        .onTapGesture {
                            dismiss()
                        }
                    Text("Popular movies")
                        .font(.title2)
                    
                    Spacer()
                    
                    HStack{
                        
                        if pageNumber > 1{
                            Button {
                                pageNumber -= 1
                                Task{
                                    
                                    try await moviesListViewModel.getAllPopularMoviesList(page: pageNumber)
                                }
                            } label: {
                                
                                Image(systemName: "arrow.left")
                                
                            }
                            .disabled(pageNumber<1 ? true : false)
                        }
                        
                        Text("\(pageNumber)")
                        
                        Button {
                            pageNumber += 1
                            Task{
                                try await moviesListViewModel.getAllPopularMoviesList(page: pageNumber)
                            }
                        } label: {
                            
                            Image(systemName: "arrow.right")
                            
                        }
                    }
                    .bold()
                    .foregroundStyle(Color("yellowColor"))
                    .padding(.trailing)
                    
                }
                .foregroundStyle(.white)
                .bold()
                .frame(maxWidth: .infinity,alignment:.leading)
                .padding(.leading)
                
                ScrollView(.vertical){
                    if isLoading != true {
                        LazyVStack(spacing: 10) {
                            ForEach(moviesListViewModel.movieAllPopularListArray, id: \.id) { data in
                                if let results = data.results {
                                    ForEach(results, id: \.uniqueID) { innerData in
                                        NavigationLink(value:innerData){
                                            MovieCard2(imageURL: innerData.posterPath, rating: innerData.voteAverage, movieTitle: innerData.title, releaseDate: innerData.releaseDate, viewersCount: innerData.voteCount)
                                            
                                            Divider()
                                                .background(Color.gray)
                                        }
                                    }
                                }
                            }
                        }
                        .padding(.leading)
                    }
                    else{
                        ProgressView("Loading...")
                            .controlSize(.large)
                            .foregroundStyle(Color("yellowColor"))
                    }
                }
            }
            .scrollContentBackground(.hidden)
            .navigationBarBackButtonHidden()
            .frame(maxWidth: .infinity,alignment:.leading)
            .scrollIndicators(.hidden)
            
        }
        .task {
            Task{
                try await moviesListViewModel.getAllPopularMoviesList(page: pageNumber)
            }
        }
        .onChange(of: moviesListViewModel.isLoading) { oldValue, newValue in
            isLoading = newValue
        }
    }
}

问题原因

  1. NavigationDestination作用域错误:HomeView中的navigationDestination(for: Movie.self)定义在VStack上,而非NavigationStack的直接子节点,导致PopularMoviesView中的NavigationLink触发时,系统向上查找目的地的逻辑出现异常,引发视图重复入栈。
  2. ViewModel持有方式不当:HomeView中用@State持有MoviesListViewModel,若ViewModel是ObservableObject,@State无法保证其生命周期稳定,可能导致视图不必要重绘,干扰导航栈状态。

解决方案

方案1:调整NavigationDestination的作用域

将HomeView中的navigationDestination移到NavigationStack的直接子节点层级,确保整个导航栈内的视图都能正确匹配目的地:

struct HomeView: View {
    @State var moviesListViewModel = MoviesListViewModel()   
    
    var body: some View {
        NavigationStack{
            ZStack {
                Color.black.opacity(0.9)
                    .ignoresSafeArea(edges: .all)
                VStack {
                    ScrollView{
                        MoviesScrollView()
                            .environment(moviesListViewModel)
                    }
                    .frame(maxWidth:.infinity,maxHeight:.infinity,alignment: .topLeading)
                }
            }
            // 移到NavigationStack的直接子视图下
            .navigationDestination(for: Movie.self, destination: { item in
                MovieDetailView(movieDetails: item)
                    .navigationBarBackButtonHidden()
            })
        }
    }
}

方案2:在PopularMoviesView中添加独立的navigationDestination

在PopularMoviesView内部添加针对Movie.self的navigationDestination,让导航行为在当前视图层级直接处理:

struct PopularMovies: View {
    init(){
        print("Poppular Movies Screen")
    }
 
    @State var isLoading:Bool = true
    @State var pageNumber:Int = 1
    @Environment(MoviesListViewModel.self) var moviesListViewModel
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        ZStack{
            Color.black.opacity(0.9)
                .ignoresSafeArea(edges: .all)
            VStack{
                // 原有视图代码不变
                HStack{ /* ... */ }
                ScrollView(.vertical){ /* ... */ }
            }
            // 添加当前视图的navigationDestination
            .navigationDestination(for: Movie.self, destination: { item in
                MovieDetailView(movieDetails: item)
                    .navigationBarBackButtonHidden()
            })
        }
        .task { /* ... */ }
        .onChange(of: moviesListViewModel.isLoading) { /* ... */ }
    }
}

方案3:优化ViewModel持有方式

如果MoviesListViewModel遵循ObservableObject,改用@StateObject持有,保证其生命周期与视图一致,避免不必要的重绘:

struct HomeView: View {
    // 替换@State为@StateObject
    @StateObject var moviesListViewModel = MoviesListViewModel()   
    
    var body: some View {
        // 原有代码不变
    }
}

内容的提问来源于stack exchange,提问作者Hemanth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 05:42:04