SwiftUI:无需点击按钮即可更新按钮内的QuizProgressView
问题解决:按钮内嵌进度条实时更新方案
问题根源
你的QuizProgressView内部创建了独立的QuizScoreManager实例,和MenuView里的管理器不是同一个对象。当分数更新时,进度条的管理器没有同步数据,只有点击按钮重建视图时才会重新加载分数,导致UI延迟更新。
解决步骤
- 共享状态管理器:让所有视图使用同一个
QuizScoreManager实例,避免数据孤岛。 - 传递依赖:将
MenuView的管理器传递给内嵌的QuizProgressView,而非让进度条自行创建。 - 全局状态提升:把管理器放到顶层的
Qmodel中,确保答题后的分数更新能同步到所有相关视图。
修改后的代码
1. 调整QuizProgressView,接收外部管理器
struct QuizProgressView: View { var quizTitle: String @ObservedObject var scoreManager: QuizScoreManager // 移除内部创建,改为外部传入 let progressViewHeight: CGFloat = 6 let cornerRadius: CGFloat = 15 var questions: [Question] { switch quizTitle { case "Core": return questionsCore case "Behaviour": return questionsBehaviour case "Parking": return questionsParking case "Emergencies": return questionsEmergencies case "RoadPosition": return questionsRoadPosition case "Intersection": return questionsIntersection case "Theory": return questionsTheory case "Signs": return questionsSigns default: return [] } } private var lastScore: Int { scoreManager.getLastScore(forQuizTitle: quizTitle) ?? 0 } var body: some View { GeometryReader { geo in VStack(spacing: 0) { let progress = Double(lastScore) / Double(questions.count) let progressBarWidth = geo.size.width - cornerRadius * 2 ZStack(alignment: .leading) { Capsule() .foregroundColor(Color.gray.opacity(0.4)) .frame(height: progressViewHeight) Capsule() .foregroundColor(.green) .frame(width: CGFloat(progress) * progressBarWidth, height: progressViewHeight) } .frame(height: progressViewHeight) .padding(.horizontal, cornerRadius) } } } }
2. 改造MenuView,接收并传递管理器
struct MenuView: View { let onQuizButtonTapped: ([Question]) -> Void @ObservedObject var scoreManager: QuizScoreManager // 接收顶层传入的管理器 @State private var menuSelectedSet: [Question]? @State private var quizOptionIndex: Int? var quizOptions = [ QuizOption(title: "Core", symbolName: "book", loadSet: questionsCore), QuizOption(title: "Behaviour", symbolName: "figure.seated.seatbelt", loadSet: questionsBehaviour), QuizOption(title: "Parking", symbolName: "car.circle", loadSet: questionsParking), QuizOption(title: "Emergencies", symbolName: "light.beacon.max", loadSet: questionsEmergencies), QuizOption(title: "RoadPosition", symbolName: "car.top.lane.dashed.arrowtriangle.inward", loadSet: questionsRoadPosition), QuizOption(title: "Intersection", symbolName: "arrow.triangle.2.circlepath", loadSet: questionsIntersection), QuizOption(title: "Theory", symbolName: "questionmark.circle", loadSet: questionsTheory), QuizOption(title: "Signs", symbolName: "exclamationmark.octagon", loadSet: questionsSigns), ] var body: some View { NavigationView { ScrollView { VStack(spacing: 20) { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) { ForEach(quizOptions, id: \.self) { quizOption in Button(action: { menuSelectedSet = quizOption.loadSet onQuizButtonTapped(menuSelectedSet ?? []) }) { RoundedRectangle(cornerRadius: 25) .frame(height: 200) .foregroundColor(.blue) .overlay( VStack { Spacer() Image(systemName: quizOption.symbolName) .font(.system(size: 60)) .foregroundColor(.white) .frame(height: 90) Text(quizOption.title) .fontWeight(.semibold) .foregroundColor(.white) .font(.title) .multilineTextAlignment(.center) .frame(height: 50) // 传入共享的scoreManager QuizProgressView(quizTitle: quizOption.title, scoreManager: scoreManager) Spacer() } ) .padding(5) .shadow(color: Color.black.opacity(0.4), radius: 10, x: 0, y: 5) } } } .padding(.horizontal, 13) Spacer() } } .navigationBarBackButtonHidden(true) .navigationTitle("Learn") } } }
3. 顶层ContentView和Qmodel统一管理状态
class Qmodel: ObservableObject { @Published var contentSelectedSet: [Question] = [] // 将scoreManager提升到全局模型,确保所有视图共享 @Published var scoreManager = QuizScoreManager(startingValues: [ "Core": 0, "Behaviour": 0, "Parking": 0, "Emergencies": 0, "RoadPosition": 0, "Intersection": 0, "Theory": 0, "Signs": 0 ]) } struct ContentView: View { @State private var isQuizViewShowing = false @StateObject var model = Qmodel() @State var hasSelectedSet = false var body: some View { TabView { MenuView(onQuizButtonTapped: { menuSelectedSet in model.contentSelectedSet = menuSelectedSet hasSelectedSet = true isQuizViewShowing = true }, scoreManager: model.scoreManager) // 传递管理器给MenuView .tabItem { Image(systemName: "book.fill") Text("Learn") } PracticeView() .tabItem { Image(systemName: "chart.bar.fill") Text("Practice") } Text("SettingsView") .tabItem { Image(systemName: "gearshape.fill") Text("Settings") } } .fullScreenCover(isPresented: $isQuizViewShowing) { if hasSelectedSet { // 传递管理器给QuizView,答题后更新分数时能同步到进度条 QuizView(isPresented: $isQuizViewShowing, selectedSet: model.contentSelectedSet, scoreManager: model.scoreManager) } else { Text("Selected set is nil") } } } }
4. 确保QuizView使用同一管理器更新分数
(假设你的QuizView有更新分数的逻辑,需要接收scoreManager并调用addScore)
// 示例QuizView结构 struct QuizView: View { @Binding var isPresented: Bool var selectedSet: [Question] @ObservedObject var scoreManager: QuizScoreManager // ... 其他代码 // 答题结束后调用此方法更新分数 func submitScore(_ score: Int) { // 假设当前quiz的标题可以从selectedSet推断,或者传入 let quizTitle = "Core" // 替换为实际的quiz标题 scoreManager.addScore(score: score, quizTitle: quizTitle) isPresented = false } }
原理说明
通过共享同一个QuizScoreManager实例,所有视图都观察同一个@Published的scores数组。当分数更新时,管理器的scores属性触发通知,所有依赖它的视图(包括每个按钮内的QuizProgressView)会自动刷新UI,进度条就能实时显示最新分数,无需点击按钮。
内容的提问来源于stack exchange,提问作者Flynn
相关产品推荐
相关产品推荐

