对于我视图中的每个按钮,它都有一个 QuizProgressView,它是一个小进度条。目前如果功能正常并在所需值更改时更新。但是,在按下更新所在的按钮之前,更新实际上并没有反映在 UI 上。
无论按钮是否被按下,我如何强制它更新?最好将它保留在按钮内,这样布局、间距等都是正确的。
struct QuizOption: Identifiable, Hashable {
var id = UUID()
var title: String
var symbolName: String
// var progress: Double
var loadSet: [Question]
struct MenuView: View {
let onQuizButtonTapped: ([Question]) -> Void
@State private var menuSelectedSet: [Question]?
@ObservedObject var scoreManager = QuizScoreManager(startingValues: ["Core": 0, "Behaviour": 0, "Parking": 0, "Emergencies": 0, "RoadPosition": 0, "Intersection": 0, "Theory": 0, "Signs": 0])
@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)
//Spacer()
Text(quizOption.title)
.fontWeight(.semibold)
.foregroundColor(.white)
.font(.title)
.multilineTextAlignment(.center)
.frame(height: 50)
QuizProgressView(quizTitle: quizOption.title) //update this on demand
Spacer()
}
)
.padding(5)
.shadow(color: Color.black.opacity(0.4), radius: 10, x: 0, y: 5)
}
}
}
}
.padding(.horizontal, 13)
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle("Learn")
}
}
}
...按需更新
QuizProgressView(quizTitle: quizOption.title)
,
使用@State var quizOptions
,这样每当
quizOptions
变化(如标题),UI会自动刷新,
因为它被声明为@State var
.
这是我的测试代码,显示了实际的原理。
测试
Button("change Core title")
已设置(与您的操作按钮分开),以便在单击时直接更改
quizOptions
第一个QuizOption
的标题,然后反映在用户界面中。
此外还提供了一个
.onAppear{...}
,这样第二个quizOptions
标题
3 秒后无需用户交互即可更改。
struct ContentView: View {
var body: some View {
MenuView(onQuizButtonTapped: onQuizButtonTapped)
}
// for testing
func onQuizButtonTapped(_ qSet: [Question]) {
print("---> onQuizButtonTapped: \(qSet)\n")
}
}
struct Question: Identifiable, Hashable {
let id = UUID()
var text: String
var options: [String]
var correctAnswer: String
var imageName: UIImage?
var originalOptions: [String]
// for testing
static var questionsCore = [
Question(text: "What is the capital of New Zealand?", options: ["What if one of the answer choices is a longer answer", "Christchurch", "Wellington", "Auckland"], correctAnswer: "Wellington", imageName: nil, originalOptions: ["What if one of the answer choices is a longer answer", "Christchurch", "Wellington", "Auckland"]),
Question(text: "What is the highest mountain in New Zealand?", options: ["Mount Cook", "Mount Aspiring", "Mount Taranaki"], correctAnswer: "Mount Cook", imageName: UIImage(named: "cherry"), originalOptions: ["Mount Cook", "Mount Aspiring", "Mount Taranaki"]),
]
}
struct QuizOption: Identifiable, Hashable {
var id = UUID()
var title: String
var symbolName: String
// var progress: Double
var loadSet: [Question]
}
// for testing
struct QuizProgressView: View {
@State var quizTitle: String
var body: some View {
VStack {
Text("QuizProgressView").foregroundColor(.red)
Text(quizTitle).foregroundColor(.red)
}
}
}
struct MenuView: View {
let onQuizButtonTapped: ([Question]) -> Void
@State private var menuSelectedSet: [Question]?
// --- here
@State var quizOptions = [
QuizOption(title: "Core", symbolName: "book", loadSet: Question.questionsCore),
QuizOption(title: "Behaviour", symbolName: "figure.seated.seatbelt", loadSet: Question.questionsCore),
QuizOption(title: "Parking", symbolName: "car.circle", loadSet: Question.questionsCore),
QuizOption(title: "Emergencies", symbolName: "light.beacon.max", loadSet: Question.questionsCore),
QuizOption(title: "RoadPosition", symbolName: "car.top.lane.dashed.arrowtriangle.inward", loadSet: Question.questionsCore),
QuizOption(title: "Intersection", symbolName: "arrow.triangle.2.circlepath", loadSet: Question.questionsCore),
QuizOption(title: "Theory", symbolName: "questionmark.circle", loadSet: Question.questionsCore),
QuizOption(title: "Signs", symbolName: "exclamationmark.octagon", loadSet: Question.questionsCore)
]
var body: some View {
NavigationView {
VStack {
Button("change Core title") { // <--- here testing
quizOptions[0].title = String(UUID().uuidString.prefix(5))
}
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)
//Spacer()
Text(quizOption.title)
.fontWeight(.semibold)
.foregroundColor(.white)
.font(.title)
.multilineTextAlignment(.center)
.frame(height: 50)
QuizProgressView(quizTitle: quizOption.title)
Spacer()
}
)
.padding(5)
.shadow(color: Color.black.opacity(0.4), radius: 10, x: 0, y: 5)
}
}
}
}
.padding(.horizontal, 13)
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle("Learn")
}
}.navigationViewStyle(.stack)
// --- here
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
quizOptions[1].title = String(UUID().uuidString.prefix(5))
}
}
}
}
编辑-1:
您当然可以为
QuizOption
progress
做完全相同的事情。
这是...a little progress bar
在QuizProgressView
中的代码
struct QuizOption: Identifiable, Hashable {
var id = UUID()
var title: String
var symbolName: String
var progress: Double // <-- here
var loadSet: [Question]
}
// for testing
struct QuizProgressView: View {
@State var progress: Double // <-- here
var body: some View {
ProgressView(value: progress)
.tint(.red)
.background(.yellow)
.frame(width: 111)
}
}
struct MenuView: View {
let onQuizButtonTapped: ([Question]) -> Void
@State private var menuSelectedSet: [Question]?
// --- here
@State var quizOptions = [
QuizOption(title: "Core", symbolName: "book", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "Behaviour", symbolName: "figure.seated.seatbelt", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "Parking", symbolName: "car.circle", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "Emergencies", symbolName: "light.beacon.max", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "RoadPosition", symbolName: "car.top.lane.dashed.arrowtriangle.inward", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "Intersection", symbolName: "arrow.triangle.2.circlepath", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "Theory", symbolName: "questionmark.circle", progress: 0.0, loadSet: Question.questionsCore),
QuizOption(title: "Signs", symbolName: "exclamationmark.octagon", progress: 0.0, loadSet: Question.questionsCore)
]
var body: some View {
NavigationView {
VStack {
Button("change Core progress") { // <--- here testing
quizOptions[0].progress = Double.random(in: 0.0..<1.0)
}
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)
//Spacer()
Text(quizOption.title)
.fontWeight(.semibold)
.foregroundColor(.white)
.font(.title)
.multilineTextAlignment(.center)
.frame(height: 50)
QuizProgressView(progress: quizOption.progress) // <-- here
Spacer()
}
)
.padding(5)
.shadow(color: Color.black.opacity(0.4), radius: 10, x: 0, y: 5)
}
}
}
}
.padding(.horizontal, 13)
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle("Learn")
}
}.navigationViewStyle(.stack)
// --- here
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
quizOptions[1].progress = Double.random(in: 0.0..<1.0)
}
}
}
}
根据我从您的帖子中收集到的信息,您正在这样的按钮内创建进度视图:
QuizProgressView(quizTitle: quizOption.title)
很难看出这个进度视图从哪里获取它应该显示的进度信息。也许它正在访问某种正在外部更新的全球数据?这行不通 - 它可能会在首次创建时正确显示进度状态,但不会在数据更改时更新。
让它跟踪进度(您的“真相来源”)的方法是使用
State
变量或 ObservedObject
建立对数据的依赖性。如果您使用 State 变量,它可能会是这样的:
@State private var progress = 0.0
:
:
QuizProgressView(quizTitle: quizOption.title, progress: progress)
您还可以使用
Binding
到 State
变量将进度传递给子视图。但由于进度视图只是数据的仅显示视图,因此在这种情况下没有必要。
您的 QuizProgressView 可能看起来像这样(至少):
struct QuizProgressView: View {
private var quizTitle: String
private var progress: Double
init(quizTitle:String, progress: Double) {
self.quizTitle = quizTitle
self.progress = progress
}
var body: some View {
ProgressView(value: progress)
}
}
总而言之,您需要记住
View
就是这样——它是一些数据的视图。如果数据可以更改并且您希望在更改时更新视图,那么您需要构建对数据的依赖性。这是使用 State
变量、Binding
或 ObservedObject
来完成的。
对于仅显示子视图,可以通过将值传递给 init 来将对简单值的更新“推送”到子视图,如上所示。使用这种技术,父视图需要使用
State
变量、Binding
或 ObservedObject
绑定到数据,否则不会发生更新。
根据我之前在上面的回答,另一种方法是让您的
QuizOption
可观察到。
QuizOption
更改为类并实现ObservableObject
:class QuizOption: Identifiable, Hashable, ObservableObject {
var id = UUID()
var title: String
var symbolName: String
@Published var progress = 0.0
var loadSet: [Question]
init(id: UUID = UUID(), title: String, symbolName: String, loadSet: [Question]) {
self.id = id
self.title = title
self.symbolName = symbolName
self.loadSet = loadSet
}
static func == (lhs: QuizOption, rhs: QuizOption) -> Bool {
lhs.title == rhs.title
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
}
}
QuizProgressView(quizOption: quizOption)
struct QuizProgressView: View {
@ObservedObject private var quizOption: QuizOption
init(quizOption: QuizOption) {
self.quizOption = quizOption
}
var body: some View {
ProgressView(value: quizOption.progress)
}
}
4 有一个复杂的问题,因为选项是在
MenuView
中创建的(而不是传入)。要保留视图化身之间的数组,将其包装为 State
变量可能就足够了:
@State var quizOptions = [...
当我们这样做时,
QuizScoreManager
也在MenuView
中创建,所以它应该被包装成一个StateObject
,而不是一个ObserveredObject
:
@StateObject var scoreManager = QuizScoreManager(startingValues: ["Core": 0, "Behaviour": 0, "Parking": 0, "Emergencies": 0, "RoadPosition": 0, "Intersection": 0, "Theory": 0, "Signs": 0])