swift - 两种方式绑定到多个实例
问题描述
我有一个带有 ForEach 的视图,其中包含另一个视图的多个实例。我希望能够:
- 单击主视图中的按钮并在嵌套视图上触发验证,然后
- 在回来的路上,用验证的结果填充一个数组
我已经简化了项目,以便可以复制它。这是我所拥有的:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
@Published var item : String
@Published var isValid : Bool
@Published var doValidate: Bool {
didSet{
print(doValidate) // This is never called
validate()
}
}
init(item : String, isValid : Bool, validate: Bool) {
self.item = item
self.isValid = isValid
self.doValidate = validate
}
func validate() { // This is never called
isValid = Int(item) != nil
}
}
struct AddEditItemView: View {
@ObservedObject var viewModel : AddEditItemViewModel
var body: some View {
Text(viewModel.item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3", "nope"]
@Published var countersValidationResults = [Bool]()
@Published var performValidation = false
init() {
for _ in array {
countersValidationResults.append(false)
}
}
}
struct ContentView: View {
@ObservedObject var viewModel : AddEditProjectViewModel
@State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(
item: viewModel.array[i],
isValid: viewModel.countersValidationResults[i],
validate: viewModel.performValidation
)
)
}
Button(action: {
viewModel.performValidation = true
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: AddEditProjectViewModel())
}
}
当我在主视图中更改属性时,嵌套视图中的属性不会更改,即使它是 @Published 属性。
由于这第一步不起作用,我什至无法测试第二部分(使用验证结果更新书籍数组)
我需要这样的设置,因为如果某个项目无效,该视图将显示一条错误消息,因此嵌入式视图需要知道它们是否有效。
更新:
我的问题是您似乎无法将绑定对象存储在视图模型中,只能存储在视图中,因此我将属性移到了视图中,并且它可以工作:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
@Published var item : String
init(item : String) {
self.item = item
print("item",item)
}
func validate() -> Bool{
return Int(item) != nil
}
}
struct AddEditItemView: View {
@ObservedObject var viewModel : AddEditItemViewModel
@Binding var doValidate: Bool
@Binding var isValid : Bool
init(viewModel: AddEditItemViewModel, doValidate:Binding<Bool>, isValid : Binding<Bool>) {
self.viewModel = viewModel
self._doValidate = doValidate
self._isValid = isValid
}
var body: some View {
Text("\(viewModel.item): \(isValid.description)").onChange(of: doValidate) { _ in isValid = viewModel.validate() }
}
}
struct ContentView: View {
@State var performValidation = false
@State var countersValidationResults = [false,false,false,false] // had to hard code this here
@State var result : Bool = false
let array = ["1", "2", "3", "nope"]
// init() {
// for _ in array {
// countersValidationResults.append(false) // For some weird reason this appending doesn't happen!
// }
// }
var body: some View {
VStack {
ForEach(array.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(item: array[i]), doValidate: $performValidation, isValid: $countersValidationResults[i])
}
Button(action: {
performValidation.toggle()
result = countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
Text(countersValidationResults.description)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
解决方案
I'm having trouble reconciling the question with the example code and figuring out what's supposed to be happening. Think that there are a few issues going on.
didSet
will not get called on@Published
properties. You can (SwiftUI - is it possible to get didSet to fire when changing a @Published struct?) but the gist is that it's not a normal property, because of the @propertyWrapper around itYou say in your question that you want a "binding", but you never in fact us a
Binding
. If you did want to bind the properties together, you should look into using either @Binding or creating a binding without the property wrapper. Here's some additional reading on that: https://swiftwithmajid.com/2020/04/08/binding-in-swiftui/You have some circular logic in your example code. Like I said, it's a little hard to figure out what's a symptom of the code and what you're really trying to achieve. Here's an example that strips away a lot of the extraneous stuff going on and functions:
struct AddEditItemView: View {
var item : String
var isValid : Bool
var body: some View {
Text(item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3"]// "nope"]
@Published var countersValidationResults = [Bool]()
init() {
for _ in array {
countersValidationResults.append(false)
}
}
func validate(index: Int) { // This is never called
countersValidationResults[index] = Int(array[index]) != nil
}
}
struct ContentView: View {
@ObservedObject var viewModel : AddEditProjectViewModel
@State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(item: viewModel.array[i], isValid: viewModel.countersValidationResults[i])
}
Button(action: {
viewModel.array.enumerated().forEach { (index,_) in
viewModel.validate(index: index)
}
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
Note that it your array
, if you include the "nope"
item, not everything validates, since there's a non-number, and if you omit it, everything validates.
In your case, there really wasn't the need for that second view model on the detail view. And, if you did have it, at least the way you had things written, it would have gotten you into a recursive loop, as it would've validated, then refreshed the @Published property on the parent view, which would've triggered the list to be refreshed, etc.
If you did get in a situation where you needed to communicate between two view models, you can do that by passing a Binding
to the parent's @Published property by using the $
operator:
class ViewModel : ObservableObject {
@Published var isValid = false
}
struct ContentView : View {
@ObservedObject var viewModel : ViewModel
var body: some View {
VStack {
ChildView(viewModel: ChildViewModel(isValid: $viewModel.isValid))
}
}
}
class ChildViewModel : ObservableObject {
var isValid : Binding<Bool>
init(isValid: Binding<Bool>) {
self.isValid = isValid
}
func toggle() {
isValid.wrappedValue.toggle()
}
}
struct ChildView : View {
@ObservedObject var viewModel : ChildViewModel
var body: some View {
VStack {
Text("Valid: \(viewModel.isValid.wrappedValue ? "true" : "false")")
Button(action: {
viewModel.toggle()
}) {
Text("Toggle")
}
}
}
}
推荐阅读
- excel - 更新单元格中的值
- c# - 如何在 C# 中执行任务
- android - 同步 gradle 时 registerResGeneratingTask,registerResGeneratingTask - onesignal 无法在 android 设备上接收推送通知
- reactjs - 如何在功能组件渲染过程中正确产生副作用?
- types - VHDL 将自定义类型有符号整数转换为 std_logic_vector
- azure-devops - 任务名称的@+数字是什么意思
- java - 用 Java 绘制具有亚像素精度的图像
- c++ - 命名(或引用)一个专门的模板参数
- c - 如何从文件中获取枚举值?
- android - 如果应用程序已打开,则深层链接不会正确重定向