I've built an NSOutlineView that gets dynamically updated data from an NSTreeController and that all works fine. What I can't seem to do is work backwards from there based on a user selection in the NSOutlineView.
var deviceStore = [TreeNode]()
is my backing datastore that is updated in real-time it is an array of Device Objects, which may )or may not) contain an array of Service objects as children.
This all works. But when I select a row in the Outline View, I need to work my way back to the original object in the deviceStore -- or, at the very least, get the displayed data from the OutlineView so that I can walk the deviceStore to find the original item.
What I've got is func outlineViewSelectionDidChange(_ notification: Notification) {} which is called when a selection is made, and I can, from that, extract the NSTreeController TreeNode via treeController.selectedNodes but from there, I am in the weeds. The selectedNodes is the complete array of the selected Node, so if it's a child (leaf) node, it includes its parent node, and all its siblings.
Give then Table shown here:
The selectedNodes array looks like this:
<NSTreeControllerTreeNode: 0x6080000c4590>, child nodes {
0:<NSTreeControllerTreeNode: 0x6000000ca6b0>, child nodes {
0:<NSTreeControllerTreeNode: 0x6000000caf70>, child nodes {}
1:<NSTreeControllerTreeNode: 0x6000000cafe0>, child nodes {}
2:<NSTreeControllerTreeNode: 0x6000000cb050>, child nodes {}
}
1:<NSTreeControllerTreeNode: 0x6080000d1790>, child nodes {
0:<NSTreeControllerTreeNode: 0x6000000cce80>, child nodes {}
}
}
And the selectedIndex is 4.
I can't see how to get back to what, in my data model, would be deviceStore[0].serviceStore[2] from this information.
If I could retrieve the value in the Service ID column from the selected Row, I could simply walk the deviceStore tree to find it.
I'm sure there's a simply, elegant, easy way to do this that I just haven't found yet, but being new to NSTreeControllers and NSOutlineViews I'm lost.
You may try to access directly the associated object(s) like this:
let selectedService = treeController.selectedObjects.first as? Service
The docs are here.
also make sure your NSTreeController is correctly configured to use your class' objects:
Alternatively (if you want to work directly with your data source) you may want to get the index path of the selected object in the NSTreeController:
var selectionIndexPath: IndexPath? { get }
I use following with Core data,
func outlineViewSelectionDidChange(_ notification: Notification) {
let item = outlineView.item(atRow: outlineView.selectedRow) as! NSTreeNode
let fileItem = item.representedObject as! FileItemMO
// Do what you need to do with object fileItem
}
I was never able to actually get back to the TreeController backing-store data baed on where the user clicks in the TreeView. What I was able to do was to work backwards to the data though.
func outlineViewSelectionDidChange(_ notification: Notification) {
let selectedIndex = (notification.object as AnyObject).selectedRow!
let selCol1 = outlineView.view(atColumn: 0, row: selectedIndex, makeIfNecessary: false)?.subviews.last as! NSTextField
let selCol2 = outlineView.view(atColumn: 1, row: selectedIndex, makeIfNecessary: false)?.subviews.last as! NSTextField
let devName = selCol1.stringValue
let devID = selCol2.stringValue
...
}
I could then 'walk' the deviceStrore array until I found the devName and devID in it, and deal with it accordingly.
Probably not the most elegant solution, but at least it finally works.
Related
I've trying to reorder objects from at TableView in a Realm utility class. Many other Stack Overflow questions have said to uses List, however I can't seem to make it work. (Example, example). I'm able to successfully add objects to my list with:
public func addUserSong(song: Song) {
let songList = List<Song>()
songList.append(objectsIn: realm.objects(Song.self))
try! realm.write {
songList.append(song)
realm.add(songList)
}
}
However, I'm not able to preserve the updated order when trying:
public func reorder(from: Int, to: Int) {
let songs = List<Song>()
songs.append(objectsIn: realm.objects(Song.self))
songs.move(from: from, to: to)
try! realm.write {
realm.add(songs)
}
My models are:
class Song: Object {
#Persisted var name: String
#Persisted var key: String
}
class SongList: Object {
let songs = List<Song>()
}
Thanks!
Realm object order is not guaranteed. (unless you specify a sort order)
e.g. if you load 10 songs from Realm, they could come into your app an any order and the order could change between loads. The caveat to that is a Realm List object. Lists always maintain their order.
The problem in the question is you have Song objects stored in Realm but as mentioned above there is no ordering.
So the approach needs to be modified by leveraging a List object for each user to keep track of their songs:
class UserClass: Object {
#Persisted var name: String
#Persisted var songList = List<SongClass>()
}
When adding a song to a user, call it within a write transaction
try! realm.write {
someUser.songList.append(someSong)
}
suppose the user wants to switch the place of song 2 and song 3. Again, within a write transaction:
try! realm.write {
someUser.songList.move(from: 2, to: 3)
}
So then the UI bit - tableViews are backed by a tableView dataSource - this case it would be the songList property. When that dataSource is updated, the tableView should reflect that change.
In this case you would add an observer to the someUser.songList and as the underlying data changes, the observer will react to that change and can then update the UI.
You can do something simple like tableView.reloadData() to reflect the change or if you want fine-grained changes (like row animations for example) you can do that as well. In that same guide, see the code where tableView.deleteRows, .insertRows and .reload is handled. You know what rows were changed in the underlying data there so you can then animate the rows in the tableView.
I know SwiftUI uses state-driven rendering. So I was assuming, when I delete Core Data Entity entries, that my List with Core Data elements gets refreshed immediately.
I use this code, which gets my Entity cleaned succesfully:
func deleteAll()
{
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.execute(deleteRequest)
} catch let error as NSError {
print(error)
}
}
To get the List in my View visually empty I have to leave the View afterwards (for example with " self.presentationMode.wrappedValue.dismiss()") and open it again. As if the values are still stored somewhere in the memory or something.
This is of course not user-friendly and I am sure I just oversee something that refreshes the List immediately.
Maybe someone can help.
The reason is that execute (as described in details below - pay attention on first sentence) does not affect managed objects context, so all fetched objects remains in context and UI represents what is really presented by context.
So in general, after this bulk operation you need to inform back to that code (not provided here) force sync and refetch everything.
API interface declaration
// Method to pass a request to the store without affecting the contents of the managed object context.
// Will return an NSPersistentStoreResult which may contain additional information about the result of the action
// (ie a batch update result may contain the object IDs of the objects that were modified during the update).
// A request may succeed in some stores and fail in others. In this case, the error will contain information
// about each individual store failure.
// Will always reject NSSaveChangesRequests.
#available(iOS 8.0, *)
open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult
For example it might be the following approach (scratchy)
// somewhere in View declaration
#State private var refreshingID = UUID()
...
// somewhere in presenting fetch results
ForEach(fetchedResults) { item in
...
}.id(refreshingID) // < unique id of fetched results
...
// somewhere in bulk delete
try context.save() // < better to save everything pending
try context.execute(deleteRequest)
context.reset() // < reset context
self.refreshingID = UUID() // < force refresh
No need to force a refresh, this is IMO not a clean solution.
As you correctly mentioned in your question, there are still elements in memory. The solution is to update your in-memory objects after the execution with mergeChanges.
This blog post explains the solution in detail under "Updating in-memory objects".
There, the author provides an extension to NSBatchDeleteRequest as follows
extension NSManagedObjectContext {
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
}
Here is an update to your code on how to call it:
func deleteAll() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.executeAndMergeChanges(deleteRequest)
} catch let error as NSError {
print(error)
}
}
Some more info also here under this link: Core Data NSBatchDeleteRequest appears to leave objects in context.
Here is pseudocode for what I want to achieve:
let a = AClass()
let go = NSMutableArray()
go.add(a)
.
.
.
class AClass{
.
.
.
fileprivate func removeMyselfFromCollection() { //Called from within the class
//TODO: How to remove myself from the collection I am in.
}
}
It could be any collection, array or dictionary etc. Something similar to removeFromSuperview()
Update: Responding to a common question...
Question: Why I need this?
Answer: Scenario is a bit complex but bringing it down to most basic details. I am uploading files (audio, video, sound) to remote storage. Once files are uploaded, I have a mechanism (listeners) to know when my files are there, I don't need local objects to tell me the status. I add local objects to a global collection to keep them alive till the files are uploading (one object representing each file). Each object takes care of each file being uploaded. I wish that each of my objects removes itself silently from the global collection once upload is finished (reason explained above).
Not unless you:
a. Implement this method yourself.
b. Pass the collection in as a parameter.
c. Have the object's method forward the remove(self) message to the collection.
Given that you would have to just forward the message to the collection anyway, and you'd have to know the collection at the time you called the method, there's no value in such a method. It adds complexity without adding any value.
Another problem is that objects can be members of more than one collection. What should happen in that case? Should it be removed from all the collections it belongs to? The first one to which it was added? The last? And how would you track that?
All in all, this seems like a all-pain-no-gain proposition.
You can do this by typing your own custom insertion method into the extension of NSMutableArray, and in this method, you can assign the array to the object variable.
class SomeClass {
var containedBy: NSMutableArray?
var value: String
init(value: String) {
self.value = value
}
func removeFromCollection() -> Void {
guard let containedBy = self.containedBy else { return }
containedBy.remove(self)
}
}
extension NSMutableArray{
func addRemovable(value: SomeClass) {
value.containedBy = self
self.add(value)
}
}
I am new to reactive programming and I am experiencing difficulty in filtering and accessing object from a specific index. Below is my code snippet.
private var contacts: Observable<(AnyRealmCollection<Contact>, RealmChangeset?)>!
override func viewDidLoad() {
super.viewDidLoad()
contacts = Observable.changeset(from: contactViewModel.getDeviceContacts())
let dataSource = RxCollectionViewRealmDataSource<Contact>(cellIdentifier: "SendFeedContactCell", cellType: ContactCollectionCell.self) {cell, ip, contact in
cell.configure(contact)
}
contacts
.bindTo(collectionView.rx.realmChanges(dataSource))
.addDisposableTo(disposeBag)
searchBar
.rx.text
.orEmpty
.subscribe(onNext: { [unowned self] query in
// Filter query - doesn't work!
// self.contacts.filter({ (observable) -> Bool in
// observable.0.filter(NSPredicate(format: "name CONTAINS[c] '\(query)'"))
// })
})
.addDisposableTo(disposeBag)
collectionView.rx.itemSelected
.subscribe(onNext: { indexPath in
// TODO: How to access a specific object from the contacts object
})
.addDisposableTo(disposeBag)
}
I am receiving query in the search bar but filtering doesn't seems to work.
I am getting IndexPath when an item is selected in the collection view but I am wondering how I can access a specific model properties based on the index path.
I am using RxRealm and RxRealmDataSources as my data is stored in realm database in device.
1) Regarding your first question: you cannot imperatively filter an observable. You are trying to use it as an array, but an observable does not have a "value" you can filter at arbitrary times. To filter the collection you are binding to your table view, you need to filter the realm results that is the source of your contacts observable.
E.g. you need to re-create the contacts observable, and bind this new observable to your table view.
There is a somewhat similar (but not exact) example of how to filter a table view with Rx here: https://realm.io/news/marin-todorov-realm-rxswift/.
2) You are using rx.itemSelected which gives you an index path. As said above, an Observable isn't an array that you can access in arbitrary ways, so what you want to do is not possible. You can use rx.modelSelected instead, which will give you directly the corresponding object (instead of the index path). You can see more about modelSelected here: https://github.com/ReactiveX/RxSwift/blob/master/RxCocoa/iOS/UICollectionView%2BRx.swift#L231
Background:
I designed a TableViewDataSource class that provides an implementation for UITableViewDataSource and UITableViewDelegate. You instantiate TableViewSection objects, which are passed to the TableViewDataSource which are used to configure cells, section headers, handle selection, row insertion, etc.
The TableViewSection object has a property called dataSource: [AnyObject]?, which, when set, is used to calculate the number of rows in the section, and provide an object for the cell configuration block:
// get the section, dequeue a cell for that section, retrieve the item from the dataSource
// ...
tableSection.cellConfigurationBlock?(cell: AnyObject, item: AnyObject?, indexPath: NSIndexPath)
return cell
What I'd like to do is assign a reference to an array from my viewModel to my tableSection.dataSource, having my viewModel update the array, in turn updating the table view. In Swift, you cannot pass an array by reference. The workaround seems to be to use an NSMutableArray, but with that comes a loss of type safety, and greater cognitive load while translating objects back and forth from Swift to Foundation.
Working Example:
let kCellIdentifier = "SomeCellIdentifier"
class MyViewController: UITableViewController {
// Property declarations
#IBOutlet var tableDataSource: TableViewDataSource!
var viewModel: MyViewControllerViewModel = MyViewControllerViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.setupTableView()
self.refresh()
}
func setupTableView() {
var tableSection = TableViewSection(cellIdentifier: kCellIdentifier)
tableSection.dataSource = self.viewModel.collection
// tableSection configuration
// ...
self.tableDataSource.addSection(tableSection)
}
func refresh() {
self.viewModel
.refresh()
.subscribeNext({ result in
self.tableView.reloadData()
}, error: { error in
self.logger.error(error.localizedDescription)
})
}
}
The refresh() method on the viewModel hits my API service, updates it's collection property on response, and provides the result on the next event of an RACSignal (RACSignal is a class provided by Reactive Cocoa and really, besides the point).
I've found one workaround, which involves reassigning the data source each time a single update is made, or after a batch update.
func refresh() {
self.viewModel
.refresh()
.subscribeNext({ result in
self.updateDataSource()
self.tableView.reloadData()
}, error: { error in
self.logger.error(error.localizedDescription)
})
}
func updateDataSource() {
self.tableDataSource.tableSectionForIndex(0)?.dataSource = viewModel.collection
}
This approach works, but only temporarily as a workaround. As a TableViewDataSource grows and becomes more complex, this method becomes increasingly more complex with imperative, procedural code, the opposite of what I set out to achieve when writing the class.
Question
Is there any workaround to stick to native Swift Array's to achieve the equivalent of passing a Foundation NSArray or NSMutableArray by reference?
Bonus Question
Can someone provide me with some class/struct design tips to accomplish the desired goal in pure Swift?
The simple solution is to wrap the array in a class. The class instance is passed by reference so the problem is effectively solved: a change to the array through any reference to the class instance affects the array as seen through every reference to that class instance.
The class in question can be extremely lightweight - basically, it just serves as a thin wrapper that carries the array along with it, and a client accesses the array directly through the class instance - or, just the opposite, you can design the class to manage the array, i.e. the class deliberately presents an array-like API that shields clients from the underlying implementation. Either approach might be appropriate; I've certainly done both.
Here's an example of the first kind of situation. My model object is an array belonging to a UIDocument subclass. My view controller is a UITableViewController. The user is going to view, add, and edit model entities in the table. Thus, the UITableViewController needs access to the UIDocument's array (which happens to be called people).
In Objective-C, my UITableViewController simply held a reference to the array, self.people, which was an NSMutableArray. This was just a pointer, so changes to self.people were also changes to the UIDocument's people - they are one and the same object.
In Swift, my UITableViewController holds a reference to the UIDocument object, self.doc. The array, which is now a Swift array, is "inside" it, so I can refer to it as self.doc.people. However, that's too much rewriting! Instead, I've created a calculated variable property self.people which acts as a gateway to self.doc.people:
var doc : PeopleDocument!
var people : [Person] { // front end for the document's model object
get {
return self.doc.people
}
set (val) {
self.doc.people = val
}
}
Hey presto, problem solved. Whenever I say something like self.people.append(newPerson), I'm passed right through to the UIDocument's model object people and I'm actually appending to that. The code thus looks and works just like it did in Objective-C, with no fuss at all.