将 SwiftUI 视图绑定到 Realm 对象的意外行为

Unexpected behaviour binding SwiftUI views to Realm Object

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

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

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

我非常怀疑有解决方法,我的第一个停靠点很可能挂接到 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))
}
}

功能实现:

struct MyView: View {
@State private var test = ""

var body: some View {
    VStack {
        FloatingLabelTextField(title: "Test", text: $test, keyboardType: nil, contentType: nil)
    }
}
}

或...

struct TestData {
    var name: String = ""
}

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

对对象使用@State/@Binding:

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 并将 Object 带回(发布的原始代码以尽可能最简单的方式显示问题)。

@objcMembers
class TestData: Object {
    @Published dynamic 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)
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
}

以上崩溃并出现以下错误:

如上面的评论所述,在第一次创建对象时,标准做法是在客户端验证完成之前不保存到领域(并因此对其进行管理)。看起来需要不同的方法。

更新:

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

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

@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"
    }
}

主视图:

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 = ""
            }
        }
    }
}

解决方案 2(不成功):

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

因为在 Swift 中使用 RealmUI 比使用 UIKit 的情况要复杂一些(新对象是通过主 List 对象而不是独立编写的)这很快就变得复杂了所以我认为不值得花时间和精力,因为第一个解决方案似乎没问题。当然,我会对其他人的观点(或解决方案)非常感兴趣,如果他们有...

我相信你只需要补充一些缺失的部分。

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