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

SwiftUI移动端NavigationLink页面状态保留实现方案问询

问题描述

我需要实现以下SwiftUI导航场景:

  • 用户在mainPage输入内容点击后,调用viewModel.selectItem函数,该函数调用后端API并更新apiResponse值;当apiResponse变化时,加载ApiSymbolResults视图。
  • ApiSymbolResults视图包含Peer列表,点击任意peer会再次调用viewModel.selectItem(触发后端API更新apiResponse)并加载新的ApiSymbolResults视图。
  • 首次进入ApiSymbolResults时,返回按钮显示“Preview”,点击可返回mainPage并保留之前的输入状态。
  • 从ApiSymbolResults点击其他symbol时,加载新symbol的详情,同时通过NavigationLink保留历史页面;此时返回按钮要显示上一个页面的symbol名称,点击可直接回到上一页(无需重新请求API)。

目前已实现从ApiSymbolResults返回mainPage的功能,但无法实现第4点的导航栈历史保留与返回按钮动态显示的需求,开发环境为Xcode15、macOS Sonoma 14,现有代码如下:

struct  ApiSymbolResults: View {
    let response: String
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var viewModel: SearchViewModel

    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack(alignment: .leading) {
                    Spacer(minLength: 10)
                    if let responses = Global.shared.responseGot {
                        Text(responses.symbol.title)
                                      
                            }
                        }
                        .padding(.leading, 20)
                       
                   
                        ScrollView(.horizontal, showsIndicators: false) {
                            LazyHStack {
                                ForEach(responses.peer.removingDuplicates().filter { !$0.contains(".") }, id: \.self) { peer in
                                    NavigationLink(destination:  ApiSymbolResults(response: peer, viewModel: viewModel)) {
                                        Text(peer)
                                            .foregroundColor(.blue)
                                            .onTapGesture {
                                                viewModel.selectItem(with: peer)
                                                print("Clicked on peer: \(peer)")
                                            }
                                    }
                                }
                            }
                        }
                       
                        .padding(.leading, 20)


                        
                    }
                    Spacer()
                }
                .frame(width: geometry.size.width, alignment: .leading)
                .background(Color.white)
            }
            .navigationBarItems(leading: HStack {
                Image(systemName: "chevron.left")
                    .foregroundColor(.blue)
                Button("Preview") {
                    self.presentationMode.wrappedValue.dismiss()
                    viewModel.apiResponse = nil
                }
            }, trailing: Button(action: {
                print("Plus button tapped")
            }) {
                Image(systemName: "plus")
                    .foregroundColor(.white)
                    .padding(8)
                    .font(.system(size: 6, weight: .bold))
                    .background(Circle().fill(Color.blue))
            })
            .navigationBarBackButtonHidden(true)
        }
    }
}
问题分析
  • 依赖全局变量Global.shared.responseGot和共享viewModel状态,导致所有ApiSymbolResults实例共用同一数据源,无法保留每个页面的独立数据。
  • 手动隐藏系统返回按钮后,未根据导航栈层级动态调整返回按钮的标题与行为。
  • NavigationLink跳转时直接传递peer字符串,但未保存当前页面状态,返回时无法恢复原有API数据。
解决方案
  1. 为每个视图实例保存独立数据:将当前symbol的完整响应数据作为ApiSymbolResults的参数传入,避免共享状态导致的数据覆盖。
  2. 使用新导航栈API管理历史:用@Environment(\.navigationStack)(iOS 16+/macOS 13+)替代旧的presentationMode,通过导航栈路径动态判断页面层级。
  3. 异步加载后跳转:点击peer时先异步请求API获取数据,再将新的ApiSymbolResults实例推入导航栈,确保每个页面持有独立的完整数据。
修改后的代码
// 给ViewModel扩展异步获取数据的方法,避免直接修改共享状态
extension SearchViewModel {
    func fetchSymbolData(for symbol: String) async throws -> SymbolResponse {
        // 替换为实际的后端API调用逻辑,返回对应的数据模型
        let apiResponse = await callBackendAPI(symbol: symbol)
        return apiResponse
    }
}

struct ApiSymbolResults: View {
    // 每个页面持有独立的symbol响应数据
    let symbolData: SymbolResponse
    // 使用导航栈API管理导航历史
    @Environment(\.navigationStack) private var navigationStack
    @ObservedObject var viewModel: SearchViewModel
    
    // 初始化:传入已获取的完整数据
    init(symbolData: SymbolResponse, viewModel: SearchViewModel) {
        self.symbolData = symbolData
        self.viewModel = viewModel
    }
    
    // 初始化:从mainPage进入时,传入初始symbol并自动获取数据
    init(initialSymbol: String, viewModel: SearchViewModel) async throws {
        self.viewModel = viewModel
        self.symbolData = try await viewModel.fetchSymbolData(for: initialSymbol)
    }
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    // 展示当前symbol的标题
                    Text(symbolData.symbol.title)
                        .font(.title)
                        .padding(.leading, 20)
                    
                    // Peer列表横向滚动
                    ScrollView(.horizontal, showsIndicators: false) {
                        LazyHStack(spacing: 12) {
                            ForEach(symbolData.peer.removingDuplicates().filter { !$0.contains(".") }, id: \.self) { peer in
                                Button(action: {
                                    // 点击peer后异步获取数据,再推入导航栈
                                    Task {
                                        do {
                                            let peerData = try await viewModel.fetchSymbolData(for: peer)
                                            navigationStack.push(ApiSymbolResults(symbolData: peerData, viewModel: viewModel))
                                        } catch {
                                            print("获取peer数据失败:\(error)")
                                        }
                                    }
                                }) {
                                    Text(peer)
                                        .foregroundColor(.blue)
                                        .padding(.vertical, 6)
                                        .padding(.horizontal, 12)
                                        .background(Color.blue.opacity(0.1))
                                        .cornerRadius(8)
                                }
                            }
                        }
                    }
                    .padding(.leading, 20)
                    
                    Spacer()
                }
                .frame(width: geometry.size.width, alignment: .leading)
                .background(Color.white)
            }
            .navigationBarTitleDisplayMode(.inline)
            // 动态设置工具栏按钮
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    if navigationStack.path.count == 1 {
                        // 第一个ApiSymbolResults页面,显示Preview返回mainPage
                        Button(action: {
                            navigationStack.popToRoot()
                            viewModel.apiResponse = nil
                        }) {
                            HStack {
                                Image(systemName: "chevron.left")
                                Text("Preview")
                            }
                            .foregroundColor(.blue)
                        }
                    } else {
                        // 非根页面,显示上一个symbol名称作为返回按钮
                        Button(action: {
                            navigationStack.pop()
                        }) {
                            HStack {
                                Image(systemName: "chevron.left")
                                // 获取导航栈中上一个页面的symbol标题
                                if let previousPage = navigationStack.path.dropLast().last as? ApiSymbolResults {
                                    Text(previousPage.symbolData.symbol.title)
                                }
                            }
                            .foregroundColor(.blue)
                        }
                    }
                }
                
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        print("Plus按钮被点击")
                    }) {
                        Image(systemName: "plus")
                            .foregroundColor(.white)
                            .padding(8)
                            .font(.system(size: 16, weight: .bold))
                            .background(Circle().fill(Color.blue))
                    }
                }
            }
            .navigationBarBackButtonHidden(true)
        }
    }
}

// 根据实际API返回定义的数据模型示例
struct SymbolResponse: Identifiable {
    let id = UUID()
    let symbol: SymbolInfo
    let peer: [String]
}

struct SymbolInfo {
    let title: String
}
关键说明
  • 使用@Environment(\.navigationStack)需要项目部署目标为iOS 16+/macOS 13+,Xcode15默认支持该版本。
  • 每个ApiSymbolResults实例持有独立的symbolData,返回时无需重新请求API,直接展示已保存的内容。
  • 点击peer时通过Task异步处理API请求,确保数据获取完成后再跳转,避免状态混乱。
  • 根据导航栈路径长度动态切换返回按钮样式:第一个页面显示“Preview”,后续页面显示上一个symbol的标题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 11:12:03