automerge / automerge-swift

Swift language bindings presenting Automerge

Home Page:https://automerge.org/automerge-swift/documentation/automerge/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

AutomergeText.textBinding() instances not bound to a document don't publish updates

gwbrown opened this issue · comments

In the setter for the Binding supplied by AutomergeText.textBinding():

guard let objId = self.objId, self.doc != nil else {
self._unboundStorage = newValue
return
}
do {
try self.updateText(newText: newValue)

updateText() calls sendObjectWillChange():

try doc.updateText(obj: objId, value: newText)
sendObjectWillChange()

But no change notification is sent on the code path that updates _unboundStorage. This confused the heck out of me for a while, trying to figure out why the text binding just wasn't working in an ultra-simple test app (literally just a TextField and an AutomergeText is what I was using).

Ah, yeah - I definitely see what you're talking about.

The idea of the bind/unbound concept for AutomergeText was specifically so you could use an instance of that class before you had an Automerge document, with my initial expectation being that you'd get instances of this class when you decoded a model using the AutomergeDecoder into your local model. At that point, they'd be bound, and dynamic updates would be reflected.

When you're first creating a new object, to add into a model, it won't be bound by default - but it captures the "initial value" you want to add, and then when you encode it into the Document, it should be written into place. Until it's bound, any updates you make to that initial value are stored locally, but not pushed into the document, which is what's triggering the updates there.

The way to make that connection outside of the encode/decode process I mentioned earlier is to call bind(doc:path:) on an instance of AutomergeText.

What would have made this easier or more obvious? I allowed textBinding() to provide binding that updated the local value specifically so you could create a new instance using SwiftUI, and then either encode it or bind it into a document to get the value stored. There's not a path that I could see to make a compiler-oriented warning that this "isn't bound" and won't trigger objectWillChange on the corresponding Document, but we could expand the documentation in textBinding to make it clear that until the instance is bound to a document, the document won't register changes.

Take a look at the updates to the documention I created in #146, and let me know if you think that would help.

Likewise, if something isn't working as you expected. For example, if you think the API is incorrect or should operate in some different pattern, I'd love to have that feedback as well.

@heckj Those doc changes would have been super helpful, thanks. I think the time folks are most likely to hit this is where I'm at now: Just messing with automerge for the first time, so maybe a note in the quick start would be good as well.

I'm not familiar enough with Swift idioms to really comment too much on what this should do instead - I'm mostly a Java dev just starting to mess with Swift. Here's a quick demo of what I mean - the "Unbound - Doesn't work" is basically what I wrote the first time and had to spend too long tracking down why it didn't work.

import SwiftUI
import Automerge

struct ContentView: View {
    @State var title: String
    @ObservedObject var note: AutomergeText
    var body: some View {
        VStack {
            Text(title)
            Text(note.value)
            TextField("textfield", text: note.textBinding())

        }
        .padding()
    }
}

struct TextWrapper: Codable {
    var text: AutomergeText
}

func previewData() -> AutomergeText {
    let doc = Document()
    let enc = AutomergeEncoder(doc: doc)
    let dec = AutomergeDecoder(doc: doc)
    var text = TextWrapper(text: AutomergeText(("spam spam")))
    try! enc.encode(text)
    text = try! dec.decode(TextWrapper.self)
    return text.text
}

#Preview {
    VStack {
        ContentView(title: "Unbound - doesn't work", note: AutomergeText("spam"))
        ContentView(title: "Bound - does work", note: previewData())
    }
}

My two big "aha" moments were:

  1. AutomergeText needs to be bound to a document before it will publish changes (what I filed this issue about because it looked like a bug at first even though I now understand why it does that), and
  2. enc.encode(text) encodes the object to the document but does not bind any component AutomergeTexts to that document - you need to decode it again to do that.

I admit I don't really understand this part:

I allowed textBinding() to provide binding that updated the local value specifically so you could create a new instance using SwiftUI, and then either encode it or bind it into a document to get the value stored.

and what workflow that's specifically talking about enabling. It sounds like that's the reason why it's not an error condition to try to set an unbound AutomergeText -that would be my naive solution to cases like the above just failing silently, make it fail loudly, but that sounds like it's precluded by support for another workflow.

This is wonderful feedback, thank you so much! I'll see if I can add just a bit more additional content to AutomergeText's description that while it conforms to ObservableObject, it only reports updates after it has been actively connected to an Automerge document.

And let me dig to see if I can find a way to update an instance of AutomergeText - it may be possible to cause it to be bound after an encode is finished. I may not be able to "reach back" from the encoders to do that, but I'll check - and it would be nice if it did!

I merged the documentation updates (#146), and when I dug around a bit, I did find a path to make this at least a little better:

  • create an instance of AutomergeText
  • add or update content to it
  • when you encode() is into an Automerge document, that updates the instance to mark it as bound, and there-after updates will flow as you originally expected.
  • This illustrated an issue with encoding AutomergeText as the top-level type into an Automerge document, so I fixed the special casing goodies there to accomodate it, and verified it in the tests.

So you'll still need to do something to bind the document, but now you can encode it or call bind() and not have to additionally decode() it again from the Document.

All that's included in PR #147

I think I've got all the updates that would solve most of this for you - if there's anything else you see along those lines, as you continue to explore (which I hope you will), I'd love to hear it!

Thanks so much! I think that's the quickest turnaround on an issue I've filed on an open source project, including the ones I work on professionally. And thanks for all y'all's work on Automerge, it's extremely cool and I very much plan to keep using it!