【问题标题】:Unexpected behaviour binding SwiftUI views to Realm Object将 SwiftUI 视图绑定到领域对象的意外行为
【发布时间】:2020-10-14 18:33:26
【问题描述】:

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

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

当我意识到在处理类时我应该使用@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)
    }
}

【问题讨论】:

    标签: binding swiftui property-wrapper


    【解决方案1】:

    添加了缺少的@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()
            }
        }
    }
    

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

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

    更新:

    解决方案 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 Object 需要由 Realm 管理以允许更改通知)来解决问题的尝试并不顺利。从头开始创建新对象时,必须将在属性上设置了临时值的“空白”版本写入领域,然后用于编辑属性的文本字段,在单独的事务中将更改写回领域。

    因为在 SwiftUI 中使用 Realm 比使用 UIKit 的情况要复杂一些(新对象是通过主 List 对象而不是独立编写的),所以这很快变得复杂,所以我认为不值得花时间和努力作为第一个解决方案似乎还可以。当然,如果其他人有意见(或解决方案),我会非常感兴趣......

    【讨论】:

    • 这不是答案 - 使用此信息更新您的问题。
    • 添加了一个可行的解决方案 - 很快就会看到另一个
    【解决方案2】:

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

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

    【讨论】:

    • 这纠正了原始问题(正如 Asperi 已经指出的那样(但 @Published 不能与领域对象一起使用,除非它们是托管的(请参阅我的附加代码)。通常情况下,我已经解决了一个问题,然后直接遇到了另一个问题:-)
    猜你喜欢
    • 2019-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多