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 } } }
问题原因
NavigationDestination作用域错误:HomeView中的navigationDestination(for: Movie.self)定义在VStack上,而非NavigationStack的直接子节点,导致PopularMoviesView中的NavigationLink触发时,系统向上查找目的地的逻辑出现异常,引发视图重复入栈。- 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
相关产品推荐
相关产品推荐

