Swift/SwiftUI 无需按下按钮即可在按钮内更新视图

问题描述 投票:0回答:3

对于我视图中的每个按钮,它都有一个 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")
        }

    }

}
swift button swiftui view
3个回答
0
投票

...按需更新

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)
                }
            }
    }
}

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
绑定到数据,否则不会发生更新。


0
投票

根据我之前在上面的回答,另一种方法是让您的

QuizOption
可观察到。

  1. 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)
    }
}

  1. 将测验选项传递给进度视图:
QuizProgressView(quizOption: quizOption)
  1. 观察进度视图中的测验选项:
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])
© www.soinside.com 2019 - 2024. All rights reserved.