Skip to content
This repository has been archived by the owner on Aug 11, 2024. It is now read-only.

Load and persist block editor from actual data #987

Closed
Tracked by #214
gordonbrander opened this issue Nov 3, 2023 · 4 comments · Fixed by #1000
Closed
Tracked by #214

Load and persist block editor from actual data #987

gordonbrander opened this issue Nov 3, 2023 · 4 comments · Fixed by #1000

Comments

@gordonbrander
Copy link
Collaborator

gordonbrander commented Nov 3, 2023

Part of #214

@gordonbrander gordonbrander mentioned this issue Nov 3, 2023
12 tasks
@gordonbrander
Copy link
Collaborator Author

Here's my current plan.

  • We currently use a bespoke store-like class I wrote called ControllerStore to drive the block editor
    • It's similar to ObservableStore, but holds a reference to a controller. It is the responsibility of the update method of the model to create a new state, and also create a thunk to mutate the controller.
    • Why? Because UIKit is based on a mutable "DOM", rather than views which are a function of state. You need not just the state, but the motivating action to correctly play state changes from model to controllers/views.
  • However, the current approach leave open the question of how to interface with SwiftUI land, where we use ObservableObject stores.

We could play changes from one to the other. But I have a simpler idea in mind...

  • I've created an experimental fork of ObservableStore that holds a publisher which publishes transactions for every action sent to the store.
    • The transaction contains the new state and the action that caused the state change
  • We can then hold an ObservableStore as an ObservedObject on the editor's UIViewControllerRepresentable. This will allow SwiftUI land to send actions to the store and read store state.
  • Meanwhile, the controller will subscribe to transactions
  • it will apply the resulting state changes to the controller by switching on the action that caused the transaction.

What we get out of this:

  • Share the same store code across SwiftUI and UIKit
  • Shared store between UIKit and SwiftUI
  • Switching on action in both the state update code, and in the controller code is more verbose. However, it gives us independently testable update functions. You can test model update logic separately from controller update logic.

@gordonbrander
Copy link
Collaborator Author

gordonbrander commented Nov 16, 2023

Upon further reflection:

  • Current state + action is not enough to replay changes on controller/views. State changes are lossy and you can't always work out what changed. If you
  • We could use UICollectionViewDiffableDataSource, but this would diff the entire list on every keystroke, since blocks would change on every keystroke. Alternatively, we could diff only IDs, but then we're left with having to figure out which blocks must be reconfigured based on state change.
  • We could use CollectionDifference (https://developer.apple.com/documentation/swift/collectiondifference) but this similarly leaves us diffing every keystroke.

The most efficient thing for us to do is update model and controller together.

So I think this still leaves us needing a second store class for UIKit. Ideas:

  • Implement the closure-based mutation approach from before
  • Allow returning an Update that contains detail or directive enum which is essentially an action for the controller.
    • We could change Update to include a detail. This would allow us to share ObservableStore
    • I think this may also work with a mutable @Observable macro style. Perform mutations, then return an update with fx and detail.
    • This does have some ergonomic shortcomings. Details can't be batched, like effects. So we can't do batch updates. Additionally, subcomponents would not work without an additional tagDetail.
  • Allow the store class to hold both an ObservableObject and a controller.
    • This might work best if it were a wrapper type around UIViewControllerRepresentable?
  • Use nested ObservableObjects
    • Each block has its own ObservableObject
    • The list is also an ObservableObject
    • Only list changes diff the list
    • This may get us closer to a store based on the @Observable macro (see https://github.com/subconsciousnetwork/arboreal), in which case we could eventually share the same store between SwiftUI and UIKit.

@gordonbrander
Copy link
Collaborator Author

gordonbrander commented Nov 20, 2023

Attempting a stateless implementation with NSDiffableDataSource so we can share ObservableStore. Findings so far:

  • Attribute cycle problem with focus/selection/text change
  • NSDiffableDataSource with hashable will re-create cells for models that change. Ordinarily this would be ok, but in our case, it causes UITextView to lose focus between re-renders.
  • You can diff on ID instead of model, but then you have to figure out separately how to update the cells that changed. This turns out to be very difficult due to the ergonomics of UIDiffableDataSource.

I wrote a custom diff that diffed list by ID and updated cells by model hash. Findings:

  • Lots of attribute cycle warnings
    • some leading to a crash
  • could not figure out how to work around

Additional challenges with stateless approach to UICollectionView:

  • All changes must either animate or not animate.

@gordonbrander
Copy link
Collaborator Author

gordonbrander commented Nov 21, 2023

Promising: https://github.com/ra1028/DifferenceKit.

It implements what we want — ID based collection diffing inferring insertions, removes, and moves by identifier, and inferring changes by equatable.

However, it reloads, rather than reconfigures changed items. https://github.com/ra1028/DifferenceKit/blob/02ca1968b10305d4d6771f4005a7ce2c507a8539/Sources/Extensions/UIKitExtension.swift#L105

We need updated items to reconfigure rather than reload so that the cell is not discarded, losing focus. Using this would require us to implement our own reload function.

gordonbrander added a commit that referenced this issue Dec 1, 2023
Fixes #987. 

Depends on
subconsciousnetwork/ObservableStore#43

This PR implements basic loading and saving of real data from database,
including blocks and related notes. I've kept the scope to *just* saving
and loading, and there is more work to be done wrt transcludes and
loading states.

## Approach

- New changes to ObservableStore allow creating custom Update types for
models. subconsciousnetwork/ObservableStore#43
- Also introduces an `updates` publisher on stores
- Custom update type lets us include a change description along with
state changes, supplying extra information that the ViewController might
need to perform these changes.
- Root UIViewController subscribes to `updates` publisher and performs
updates to controller based on state transactions.
- ObservableStore instance is stored as a `@StateObject` on the
enclosing SwiftUIView and passed down as an `@ObservedObject` to the
UIViewControllerRepresentable.

The result is that we can share the same store between SwiftUI and UIKit
land, as well as the same environment and side effects management.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant