Closed tgoyne closed 4 years ago
To support combine, right now I'm using property wrappers. The following snippet is not a general solution, just something I think worth sharing here.
@propertyWrapper
public class RealmCollectionProxy<Source: Object> {
// the realm here can be injected or shared globally
// Query is a custom enum in my project to type possible queryes
public var query: Query? {
didSet {
results = realm.objects(Source.self).filter(query?.realmQuery)
}
}
private var results: Results<Source> {
didSet {
notificationToken?.invalidate()
notificationToken = results.observe({ [weak self] _ in
guard let welf = self else { return }
welf.subject.send(welf.wrappedValue)
})
}
}
public typealias Output = Results<Source>
public typealias Failure = Never
private let subject = PassthroughSubject<Results<Source>, Never>()
private var notificationToken: NotificationToken?
public init(_ query: Query? = nil) {
self.query = query
results = realm.objects(Source.self).filter(query?.realmQuery)
notificationToken = results.observe({ [weak self] _ in
guard let welf = self else { return }
welf.subject.send(welf.wrappedValue)
})
}
public var wrappedValue: Results<Source> { results }
public var projectedValue: AnyPublisher<Results<Source>, Never> { self.subject.eraseToAnyPublisher() }
public func receive<S>(subscriber: S) where S : Subscriber, RealmCollectionProxy.Failure == S.Failure, RealmCollectionProxy.Output == S.Input {
projectedValue.receive(subscriber: subscriber)
}
deinit {
notificationToken?.invalidate()
}
}
With this, the example solution is like the following.
final class AlbumsViewModel: ObservableObject {
@RealmCollectionProxy<Album>() var albums
var objectWillChange: ObservableObjectPublisher = ObservableObjectPublisher()
private var subscriptions: Set<AnyCancellable> = []
init() {
$albums.receive(on: RunLoop.main).sink { [weak self] _ in self?.objectWillChange.send() }.store(in: &subscriptions)
}
deinit {
subscriptions.forEach { $0.cancel() }
}
}
I'm still looking forward to implement something like @Published
does, which would eliminate 90% percent of this code, leaving my class really clean.
Hi,
We will be adding support for Combine/SwiftUI next week. I'll inform the thread once the feature is merged with the master branch.
@jsflax Can't wait!
@jsflax Just curious how far along the SwiftUI support is? Will there be or are there any good examples of how one would utilize Realm when building an app with SwiftUI? My current app uses Realm, and I've used realm for quite sometime. I've started a fresh rewrite with SwiftUI and started CoreData, but am really not feeling it. :) Id like to stick with Realm !
Hey @ismyhc . The PR is here and in review: https://github.com/realm/realm-cocoa/pull/6231. An example is included as well if you'd like to take a look.
Question related to this: is there any ongoing effort to bring support for property wrappers as a whole to Realm? ie: instead of the current @objc dynamic var
way of defining things, to have something more elegant and similar to @Gujci's example the above?
I put together a prototype of using property wrappers for model definitions a while back and we're planning to spend some time this quarter trying to finish it.
@tgoyne would any contributing help, or you have already made your tools?
Thanks for the prompt update @tgoyne. And incredible timing on the 5.0 release! 🎉
Excited to get adopting this - this officially unblocks us on moving forward with a Combine re-arch in a major way =)
Hi @tgoyne could we get a documentation update on this? The docs don't mention anything about it and it's hard to find the exact way that this is supposed to work (ie: how do you observe a single property rather than a collection?)
Any luck @marchy ?
+1 for docs.
We will have a blog post this week - if you'd like to see a working app example see here: https://github.com/realm/realm-cocoa/tree/master/examples/ios/swift/ListSwiftUI
@ianpward I'm currently working on a SwiftUI project making use of Realm. Did that blog get posted? Could you provide a link?
@ptliddle It sure did - https://developer.mongodb.com/article/realm-cocoa-swiftui-combine
Any way to use combine and still write to realm without notifying that publisher?
@traviskaufman Depends on how you are trying to use it. Could you give us a sample? There are ways to use both Realm and/or Combine to filter certain results.
Yeah for sure! Thanks so much for getting back to me 😄
So my primary use-case is that I want to be able to perform ui-driven updates using Combine API.
So for example, say I have this example table view controller
import UIKit
import Combine
import RealmSwift
class ExampleTableViewController: UITableViewController {
private let realm = try! Realm()
private var items: Results<Item>!
private var c1: AnyCancellable? = nil
override func viewDidLoad() {
super.viewDidLoad()
items = realm.objects(Item.self).filter("complete = 0")
c1 = items
.collectionPublisher
.subscribe(on: DispatchQueue(label: "background queue"))
.freeze()
.receive(on: DispatchQueue.main)
.assertNoFailure()
.sink {[weak self] _ in
self?.tableView.reloadData()
}
}
}
The items
collection powers all of the UITableViewDataSource
methods, etc.
Now say I want to add a "swipe right to complete" option:
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let complete = UIContextualAction(style: .normal, title: "Complete") {_, _, completionHandler in
let itemId = self.items[indexPath.row].id
let realm = try! Realm()
guard let item = realm.object(ofType: Item.self, forPrimaryKey: itemId) else {
return
}
// How do I access a token that I can use via commitWrite() without notifying the combine publisher
try! realm.write {
item.complete = true
}
self.tableView.deleteRows(at: [indexPath], with: .automatic)
completionHandler(true)
}
complete.backgroundColor = .systemGreen
complete.image = UIImage(systemName: "checkmark")
let config = UISwipeActionsConfiguration(actions: [complete])
config.performsFirstActionWithFullSwipe = true
return config
}
So in this case, I try to complete the item using a UI-driven update, but the logic within sink
will cause the animation to be interrupted by a data reload on the table view. The only workaround I've found for this is to introduce an additional variable e.g. isUiUpdate
and use that within my sink
callback to return early without doing a reload, however that seems a bit inelegant.
Let me know if any of that is unclear and thanks for your help!
Hey @traviskaufman, just to chime in at a high level you should not use a full reload .reloadData() on table-view updates.
Look up diffable data sources that were introduced in iOS 13 (link), which is the de facto way any such updates should be handled. These have the benefit of automatically animating item removals, so your animations should work out right.
If you Realm collection is set up to filter out items that are not completed, the Combine publisher will fire, your snapshot diff will detect the exact row that was removed, and your list will update perfectly (ie: in a reactive fashion, which is presumably why you are using Combine in the first place).
NOTE: You could also do this using the old, manual UITableView beginUpdates()/endUpdates() API, but in short this is basically deprecated and should not manage this yourself.
A second note, if your table view has different sections with multiple data sources each – the updates to diffable data sources in iOS 14 are needed to properly manage those (the V1 of the API didn't have proper support for section diffs)
@traviskaufman I've created a new feature request for that. I don't see any straightforward way to do it currently.
Hey @traviskaufman, just to chime in at a high level you should not use a full reload .reloadData() on table-view updates.
Look up diffable data sources that were introduced in iOS 13 (link), which is the de facto way any such updates should be handled. These have the benefit of automatically animating item removals, so your animations should work out right.
If you Realm collection is set up to filter out items that are not completed, the Combine publisher will fire, your snapshot diff will detect the exact row that was removed, and your list will update perfectly (ie: in a reactive fashion, which is presumably why you are using Combine in the first place).
NOTE: You could also do this using the old, manual UITableView beginUpdates()/endUpdates() API, but in short this is basically deprecated and should not manage this yourself.
A second note, if your table view has different sections with multiple data sources each – the updates to diffable data sources in iOS 14 are needed to properly manage those (the V1 of the API didn't have proper support for section diffs)
Hello @marchy I'm new to realm and kinda having tough time to update UI with diffableDataSource and realm. Can you please explain a bit more about updating snapshot when there is a change in realm objects? (specially when inserting new items). Thanks!
https://developer.apple.com/documentation/combine