SwiftUI List .onDelete not working with filtered Core Data - xcode

I have a SwiftUI application that is basically a master/detail app with data persisted in Core Data. The basic application works perfectly, however I have added a search bar (through UIViewRepresentable) and I had difficulty with the ForEach.onDelete functionality when the list is filtered.
If there is text in the search bar, I execute a fetch request on the Core Data entity which includes a predicate to search all of the text fields that I want to include. Then the filtered list is presented. Again, this works as expected. However, attempting to delete a record causes a crash. I believe this is because the IndexSet becomes invalid but I don't understand why that should be. At any rate, if I re-issue the call
to the filtered list in the .onDelete code (see the first .onDelete below), then the deletions work as expected. Even with this secondary search however, changes made on the detail screen are not shown when returning to the filtered list, though the changes are saved in Core Data.
While I guess it is not a big penalty to have to re-fetch, I am not confident that this is the proper procedure, and changes do not cause the List to update. Is there a way to use the #FetchRequest wrapper and dynamically provide a predicate? I have seen several SO posts on dynamic filters in Core Data but none of them have worked for me.
Any guidance would be appreciated.
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Patient.getAllPatients()) var patients: FetchedResults<Patient>
#State private var searchTerm: String = ""
#State var myFilteredPatients = Patient.filterForSearchPatients(searchText: "")
//more properties
var body: some View {
NavigationView {
List {
MySearchBar(sText: $searchTerm)
if !searchTerm.isEmpty {
ForEach(Patient.filterForSearchPatients(searchText: searchTerm)) { patient in
NavigationLink(destination: EditPatient(patient: patient, photoStore: self.photoStore, myTextViews: MyTextViews())) {
//configure the cell etc.
}//NavigationLink
}//for each
.onDelete { indexSet in
//This recreation was necessary:
self.myFilteredPatients = Patient.filterForSearchPatients(searchText: self.searchTerm)
let deleteItem = self.myFilteredPatients[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)//put up an alert here
}
}
} else {
ForEach(self.patients) { patient in
NavigationLink(destination: EditPatient(patient: patient, photoStore: self.photoStore, myTextViews: MyTextViews())) {
//configure the cell etc
}//nav link
}//for each
.onDelete { indexSet in
let deleteItem = self.patients[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)//put up and alert here
}
}
}//if else
}//List
//buttons and setup
}//NavigationView
}//body
Xcode Version 11.2 (11B52)

Related

SwiftUI's Table: can you disable content diffing to improve performance?

I'm using SwiftUIs 'Table' to display large arrays (> 100.000) but experiencing nearly O(n²) performance penalty on updates of the array. In the following example (macOS) the table is drawn instantly on app start, but replacing the contents of the 'items' array takes several seconds, even if the new array is just empty:
struct TableTest: View {
#State private var items: [Person] = (0..<500000).map { _ in Person() }
var body: some View {
Table(items) {
TableColumn("Name", value: \.name)
TableColumn("Age") { person in Text(String(person.age)) }
}
.toolbar {
ToolbarItem() {
Button("Clear") {
items = []
}
}
}
}
struct Person: Identifiable {
let id = UUID()
let name: String = ["Olivia", "Elijah", "Leilani", "Gabriel", "Eleanor", "Sebastian", "Zoey", "Muhammad"].randomElement()!
let age: Int = Int.random(in: 18...110)
}
}
An array size of 1.000.000 items takes >30s to update.
Profiling the app suggests that the heavy lifting happens in
v AppKitTableCoordinator.updateTableView(_:from:to:)
> ListBatchUpdates.computeMoves<A>(from:to:)
> ListBatchUpdates.computeRemovesAndInserts<A>(from:to:)
so I assume the delay is caused by automatic diffing of the old and new array. I can see that Table accesses each and every of my 500.000 'Person' structs individually asking their 'id' which also suggests that it's diffing the arrays.
It would be great to switch that off. I tried using the .id(UUID()) modifier on the Table to force SwiftUI into treating each Table struct as brand new, but that didn't change anything.
The only workaround I see is using 'NSViewRepresentable' to display an 'NSTableView' but I'd prefer to use 'Table'.
So my question: is there a way to convince SwiftUI to refrain from diffing?

TornadoFX - how to fix ListView with a custom cell factory not updating properly when items are deleted?

I have a ListView displaying custom objects from my domain model, and if I use a custom cell factory to display the objects' properties in each row of the list, I get strange behaviour when I delete items. If the item is not the last in the list, the deleted item remains visible and the last item disappears. However, the item has been removed from the backing list as expected, and attempting to delete the phantom object has no further effect.
The display seems not to be refreshing properly, because after some arbitrary resizing of the window, the list eventually refreshes to its expected values. I've tried calling refresh() on the ListView manually but it has no noticeable effect.
Removing my custom cell factory fixes the problem, and I've seen other posts that have had a similar problem using standard JavaFX (ListView using custom cell factory doesn't update after items deleted) where the problem is fixed by changing the implementation of updateItem(Object item, boolean empty), but I can't work out how to do that in TornadoFX.
Here's an example that demonstrates the update issue (but not the phantom item, that only happens if the delete button is part of the custom cell):
package example
import javafx.scene.control.ListView
import tornadofx.*
data class DomainClass(val name: String, val flag1: Boolean, val flag2: Boolean, val info: String)
class UpdateIssue : App(UpdateIssueView::class)
class UpdateIssueView : View() {
val listSource = mutableListOf(
DomainClass("object1", true, false, "more info"),
DomainClass("object2", false, true, "even more info"),
DomainClass("object3", false, false, "all the info")
).observable()
var lst: ListView<DomainClass> by singleAssign()
override val root = vbox {
lst = listview(listSource) {
cellFormat {
graphic = cache {
hbox {
textfield(it.name)
combobox<Boolean> {
selectionModel.select(it.flag1)
}
combobox<Boolean> {
selectionModel.select(it.flag2)
}
textfield(it.info)
}
}
}
}
button("delete") {
action {
listSource.remove(lst.selectedItem)
}
}
}
}
Any help greatly appreciated!
The suggestion from #Edvin Syse to remove the cache block fixed this for me (although note that he also said a more performant fix would be to implement a ListCellFragment, which I haven't done here):
....
lst = listview(listSource) {
cellFormat {
graphic = hbox {
textfield(it.name)
combobox<Boolean> {
selectionModel.select(it.flag1)
}
combobox<Boolean> {
selectionModel.select(it.flag2)
}
textfield(it.info)
}
}
}
I noticed that the ComboBoxes don't show any other selectable values besides it.flag1 and flag2. You'll want to set the values property to true/false or true/false/null. You can then set the value item directly.
lst = listview(listSource) {
cellFormat {
graphic = hbox {
textfield(it.name)
combobox(values=listOf(true, false)) {
value = it.flag1
}
combobox(values=listOf(true, false)) {
value = it.flag2
}
textfield(it.info)
}
}
}

Getting lightswitch HTML client to load related entities

I am trying to load an entity based on a Query and allow the user to edit it. The entity loads without issues from the query, however it does not load its related entities, leaving detail pickers unfilled when loading the edit screen.
This is the code that I have:
myapp.BrowseCOAMissingHoldingCompanies.VW_ChartOfAccountsWithMissingHoldingCompanies_ItemTap_execute = function (screen) {
var accountName = screen.VW_ChartOfAccountsWithMissingHoldingCompanies.selectedItem.AccountFullName;
return myapp.activeDataWorkspace.Accounting360Data.FindChartOfAccountsMappingByAccountName(accountName)
.execute().then(function (query) {
var coa = query.results[0];
return myapp.showAddEditChartOfAccountsMapping(coa, {
beforeShown: function (addEditScreen) {
addEditScreen.ChartOfAccountsMapping = coa;
},
afterClosed: function () {
screen.VW_ChartOfAccountsWithMissingHoldingCompanies.refresh();
}
});
});
};
Interestingly if I open the browse screen (and nothing else) of that entity type first (which does retrieve the entity), then the related entities load correctly and everything works, but I can't figure out how to make that level of load happen in this code.
One method of tackling this (and to avoid the extra query execution of a follow on refresh) is to use the expand method to include any additional navigation properties as follows:
myapp.BrowseCOAMissingHoldingCompanies.VW_ChartOfAccountsWithMissingHoldingCompanies_ItemTap_execute = function (screen) {
var accountName = screen.VW_ChartOfAccountsWithMissingHoldingCompanies.selectedItem.AccountFullName;
return myapp.activeDataWorkspace.Accounting360Data.FindChartOfAccountsMappingByAccountName(
accountName
).expand(
"RelatedEntity," +
"AnotherRelatedEntity," +
"AnotherRelatedEntity/SubEntity"
).execute().then(function (query) {
var coa = query.results[0];
return myapp.showAddEditChartOfAccountsMapping(coa, {
beforeShown: function (addEditScreen) {
addEditScreen.ChartOfAccountsMapping = coa;
},
afterClosed: function () {
screen.VW_ChartOfAccountsWithMissingHoldingCompanies.refresh();
}
});
});
}
As you've not mentioned the name of your entity's navigational properties, I've used coa.RelatedEntity, coa.AnotherRelatedEntity and coa.AnotherRelatedEntity.SubEntity in the above example.
As covered by LightSwitch's intellisense (in msls-?.?.?-vsdoc.js) this method 'Expands results by including additional navigation properties using an expression defined by the OData $expand system query option' and it accepts a single parameter of 'An OData expand expression (a comma-separated list of names of navigation properties)'.
The reason your forced refresh of coa also populates the navigational properties is that LightSwitch's refresh method implicitly expands all navigation properties (provided you don't specify the navigationPropertyNames parameter when calling the refresh). The following shows the internal implementation of the LightSwitch refresh method (with the implicit expand behaviour executing if the navigationPropertyNames parameter is null):
function refresh(navigationPropertyNames) {
var details = this,
properties = details.properties.all(),
i, l = properties.length,
property,
propertyEntry,
query;
if (details.entityState !== _EntityState.unchanged) {
return WinJS.Promise.as();
}
if (!navigationPropertyNames) {
navigationPropertyNames = [];
for (i = 0; i < l; i++) {
property = properties[i];
propertyEntry = property._entry;
if (isReferenceNavigationProperty(propertyEntry) &&
!isVirtualNavigationProperty(propertyEntry)) {
navigationPropertyNames.push(propertyEntry.serviceName);
}
}
}
query = new _DataServiceQuery(
{
_entitySet: details.entitySet
},
details._.__metadata.uri);
if (navigationPropertyNames.length > 0) {
query = query.expand(navigationPropertyNames.join(","));
}
return query.merge(msls.MergeOption.unchangedOnly).execute();
}
However, if you take the refresh approach, you'll be performing an additional unnecessary query operation.
Entity Framework uses lazy loading by default, so related data will be loaded on demand, but in your case that's too late because the entity is already client-side a that point.
Try using the Include method in your query if you want eager loading.
Calling refresh on the details of the entity seems to do it:
return coa.details.refresh().then(function() {
return myapp.showAddEditChartOfAccountsMapping(coa, {
beforeShown: function (addEditScreen) {
addEditScreen.ChartOfAccountsMapping = coa;
},
afterClosed: function () {
screen.VW_ChartOfAccountsWithMissingHoldingCompanies.refresh();
}
});
});
You should use load method to fetch related data from Server. At this time we don't have any ways to force msls load related data.

How can I full text search response documents and have the parent show in the view/data table

I'm at a bit of an impasse. I am working in Domino with xPages and I am trying to allow full text searching through a view including response documents but including the parent document for any responses that match the query in the view or data table. Currently I'm just using the search term in a view datasource, and then using that datasource in a view control, but any workable solution would be welcome. There may be additional search criteria on the parent document.
Any ideas?
Richard,
you can't directly use the view as data source, so you won't use the view control. You can use the data table or (probably better, since it gives you full layout control) the repeat control.
Run the search against the view in code:
var v = database.getView("yourView")
//var result = database.FTSearch(...)
var result = v.FTSearchSorted(...) // or FTSearch
var datasource = [];
var parent;
for (var doc in result) {
addResult(doc, datasource);
if (doc.isResponseDoc()) {
parent = doc.getParentDocument();
addResult(parent, datasource);
// Careful here - if the parent is part of the resultset on its own
parent.recycle();
}
doc.recycle();
}
try {
result.recycle();
v.recycle();
} catch (e) {
// We suffer silently
}
return datasource;
function addResult(doc, datasource) {
var oneResult = {};
//Adjust that to your needs
oneResult.subject = doc.getItemValueString("Subject");
oneResult.unid = doc.getUniversalId();
datasource.push(oneResult);
}
See the FTSearchSorted documentation. I typed the code off my head, so there might be little syntax snafus, ut you get the idea Don't return documents or Notes objects to the XPage and use recycle() wisely.

Mark ItemViewModel as dirty explicitly

I have address editor fragment, after user edit address I save changed model values to a database
fun saveAddressTable(tableViewEditModel: TableViewEditModel<Client>) {
tableViewEditModel.items.asSequence()
.filter { it.value.isDirty }
.forEach {
//do stuff }
}
Some adressess have obvious mistakes I can fix programmatically, however model is not marked as dirty after I change it.
For example,
fun autoEditAddressTable(tableViewEditModel: TableViewEditModel<Client>) {
tableViewEditModel.items.asSequence()
.forEach {
val client = it.value.item
client.localProperty.value = client.local.replace(",{2,}".toRegex(), ",")
}
}
Changes reflected in the UI, but model itself is not dirty. I've found markDirty() property method, but it doesn't help much. As model is not marked dirty, it fails filter criteria on save.
center = tableview ( controller.clients ) {
column("Local Address", Client::localProperty)
.prefWidth(400.0)
.makeEditable()
bindSelected(controller.clientModel)
enableDirtyTracking()
tableViewEditModel = editModel
}
Isn't TableViewEditModel expected to trace all model alteration? If client edits Local Address column manually, model becomes dirty.

Resources