Binding 将SwiftUI视图绑定到领域对象的意外行为

Binding 将SwiftUI视图绑定到领域对象的意外行为,binding,swiftui,property-wrapper,Binding,Swiftui,Property Wrapper,我正在为我的应用程序开发UI,它使用领域作为后端。文本由用户在自定义文本字段中输入,该字段使用“浮动”标签以有效利用屏幕。标签动画(由非常简单的逻辑支持)是通过检查领域对象和自定义视图的属性之间的绑定状态触发的 当我对视图进行原型设计时,我使用了由结构支持的@State或@Binding属性,并且视图的行为完全符合预期。然而,当在我的主应用程序中使用它时,@State或@Binding会包装一个类(一个领域对象),并且转换不会被触发,当然,这是因为视图没有在其内部状态中注册更改,因此它不会随着颜

我正在为我的应用程序开发UI,它使用领域作为后端。文本由用户在自定义文本字段中输入,该字段使用“浮动”标签以有效利用屏幕。标签动画(由非常简单的逻辑支持)是通过检查领域对象和自定义视图的属性之间的绑定状态触发的

当我对视图进行原型设计时,我使用了由结构支持的@State或@Binding属性,并且视图的行为完全符合预期。然而,当在我的主应用程序中使用它时,@State或@Binding会包装一个类(一个领域对象),并且转换不会被触发,当然,这是因为视图没有在其内部状态中注册更改,因此它不会随着颜色和偏移量的更改而重新渲染

当我意识到在处理类时应该使用@observeObject时,我认为我已经找到了解决方案,但这也不起作用。仔细想想,这似乎是有道理的,尽管我对各种属性包装器的工作方式有点困惑

我非常怀疑这有一个解决方法,我的第一个调用端口可能会挂接到willChange以修改必要的状态。但是,同时,有人能解释一下这里发生了什么,因为我有点迷糊。如果你手头碰巧有个解决方案,这可能会阻止我去做一些疯狂的切线

自定义视图:

struct FloatingLabelTextField: View {
let title: String
let text: Binding<String>
let keyboardType: UIKeyboardType?
let contentType: UITextContentType?

var body: some View {
    ZStack(alignment: .leading) {
        Text(title)
            .foregroundColor(text.wrappedValue.isEmpty ? Color(.placeholderText) : .accentColor)
            .offset(y:text.wrappedValue.isEmpty ? 0 : -25)
            .scaleEffect(text.wrappedValue.isEmpty ? 1 : 0.8, anchor: .leading)

        TextField("", text: text, onEditingChanged: { _ in print("Edit change") }, onCommit: { print("Commit") })
            .keyboardType(keyboardType ?? .default)
            .textContentType(contentType ?? .none)
        
    }
    .padding(15)
    .border(Color(.placeholderText), width: 1)
    .animation(.easeIn(duration: 0.3))
}
}
或者

对对象使用@State/@绑定:

class TestData: ObservableObject {
    var name: String = ""
}

struct ContentView: View {
    @ObservedObject private var test = TestData()
    
    var body: some View {
        VStack {
            FloatingLabelTextField(title: "Test", text: $test.name, keyboardType: nil, contentType: nil)
    }
}

添加了丢失的@Published并带回对象(发布原始代码以尽可能简单的方式显示问题)

以上崩溃,错误如下:

如上面的注释所述,在第一次创建对象时,标准做法是在客户端验证完成之前不保存到领域(从而使其处于管理状态)。看起来需要一种不同的方法

更新:

解决方案1-仅在填充数据字段后创建对象

我使用Realm的SwiftUI示例代码汇总了以下内容。这是非常混乱的,因为我没有跟上新的SwiftUI@App wrappers等的速度,但是UI按照预期工作(我不再绑定UI和对象,而是依赖@State属性),并且我已经确认域存储正在正确更新

@main
struct TestApp: App {
    var body: some Scene {
        let realm = try! Realm()
        var dogs = realm.object(ofType: Dogs.self, forPrimaryKey: 0)
        if dogs == nil {
            dogs = try! realm.write { realm.create(Dogs.self, value: []) }
        }
        
        return WindowGroup {
            ContentView(dogs: dogs!.allDogs)
        }
    }
}
领域对象:

final class Dog: Object {
    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var name: String = ""
}

final class Dogs: Object {
    @objc dynamic var id: Int = 0
    
    // all data objects stored in the realm
    let allDogs = RealmSwift.List<Dog>()
    
    override class func primaryKey() -> String? {
        "id"
    }
}
最终类狗:对象{
@objc动态变量id:String=UUID().UUIString
@objc动态变量名称:String=“”
}
最后一类狗:对象{
@objc动态变量id:Int=0
//存储在域中的所有数据对象
让allDogs=RealmSwift.List()
重写类func primaryKey()->字符串{
“id”
}
}
主要观点:

struct ContentView: View {
    let dogs: RealmSwift.List<Dog>
    
    // MARK: UI state
    @State private var name = ""
    
    var body: some View {
        Form {
            FloatingLabelTextField(title: "Name", text: $name, keyboardType: nil, contentType: nil)
            
            Button("Commit") {
                var dog = Dog()
                dog.name = name
                self.dogs.realm?.beginWrite()
                self.dogs.append(dog)
                try! self.dogs.realm?.commitWrite()
                self.name = ""
            }
        }
    }
}
struct ContentView:View{
放狗:RealmSwift.列表
//标记:UI状态
@国家私有变量名称=“”
var body:一些观点{
形式{
FloatingLabelTextField(标题:“名称”,文本:$Name,键盘类型:nil,内容类型:nil)
按钮(“提交”){
var dog=dog()
dog.name=名称
self.dogs.realm?.beginWrite()
self.dogs.append(dog)
试试!self.dogs.realm?.commitWrite()
self.name=“”
}
}
}
}
解决方案2(不成功):

我试图通过遵循错误消息(领域对象需要由领域管理以允许更改通知)来解决问题,但没有成功。从头开始创建新对象时,必须将属性上设置了临时值的“空白”版本写入域,然后写入用于编辑属性的文本字段,并在单独的事务中将更改写回域


因为在SwiftUI中使用领域比在UIKit中使用领域要复杂一些(新对象是通过主列表对象编写的,而不是独立编写的),这很快就变得复杂了,所以我决定不值得花费时间和精力,因为第一个解决方案似乎还可以。当然,我对其他人的观点(或解决方案)非常感兴趣,如果他们有…

我相信你只需要添加一些缺失的部分

class TestData: ObservableObject {
    @Published var name: String = ""
 }

这不是一个答案-用这些信息更新您的问题。添加了一个有效的解决方案-查看另一个简短的解决方案,以纠正原始问题(正如Asperi已经指出的那样)(但是@Published不能用于领域对象,除非对其进行管理(请参阅我的附加代码).就像通常的情况一样,我解决了一个问题,然后又直接遇到了另一个问题:-)
final class Dog: Object {
    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var name: String = ""
}

final class Dogs: Object {
    @objc dynamic var id: Int = 0
    
    // all data objects stored in the realm
    let allDogs = RealmSwift.List<Dog>()
    
    override class func primaryKey() -> String? {
        "id"
    }
}
struct ContentView: View {
    let dogs: RealmSwift.List<Dog>
    
    // MARK: UI state
    @State private var name = ""
    
    var body: some View {
        Form {
            FloatingLabelTextField(title: "Name", text: $name, keyboardType: nil, contentType: nil)
            
            Button("Commit") {
                var dog = Dog()
                dog.name = name
                self.dogs.realm?.beginWrite()
                self.dogs.append(dog)
                try! self.dogs.realm?.commitWrite()
                self.name = ""
            }
        }
    }
}
class TestData: ObservableObject {
    @Published var name: String = ""
 }