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?
Related
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)
Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.
I am using Swift, Cocoa Bindings and Core Data in an OSX Xcode project to display an outline view which is bound to my entity "Series" and displays the attribute "name", which has a to-many relationship with itself.
I want my users to be able to enter only three levels: one root, a child and another child of that child.
Here is my subclass for Series:
#objc(Series)
class Series: NSManagedObject {
#NSManaged var isLeaf: NSNumber
#NSManaged var name: String
#NSManaged var parent: Series
#NSManaged var subGroups: NSSet
}
Effectively when the data is entered and saved, "isLeaf" should be changed to true for the third name entry. In this way, it would be impossible for the user to create a fourth entry.
I added this function to the Series subclass, so I could validate each new name entry:
func validateName(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>,
error: NSErrorPointer) -> Bool {
if let test = ioValue.memory as? String {
if test != "" {
println("Name is \(test)")
// This is where we need to test for indentation level
}
} else {
}
return true
}
I'm pretty sure that what I need to do is test for the indentation level of the third entry and if it returns 2, then I need to change isLeaf from false to true.
Unfortunately, I do not know how to do this in practice. Does anyone have any suggestions, please?
I am querying data from our IBM i and displaying it in a grid. The purpose of displaying all records is for a couple reasons:
The existing software isn't used properly and people aren't closing out the items. (user/training issue yes, but see other items). So narrowing down the list to just open items isn't accurate.
It allows a user to query all history (this is property based and history can be important)
However, there currently is 28,000 items and will ever increase. Right now, I am using MvcContrib grid. Here is my code:
public ActionResult Index(GridSortOptions gridSortOptions, int? page, int? filterPropertyUniqueKey, int? filterPermitNumber)
{
#region Filter and Sort
var permits = buildingPermitRepository.GetOpenPermits();
// Set default sort and apply filters
if (filterPermitNumber.HasValue)
{
permits = permits.Where(w => w.PermitId == filterPermitNumber.Value);
}
// TODO add more filters
if (String.IsNullOrEmpty(gridSortOptions.Column))
{
gridSortOptions.Column = "DateApplied";
gridSortOptions.Direction = SortDirection.Descending;
}
var permitsPagedList = permits.OrderBy(gridSortOptions.Column, gridSortOptions.Direction).AsPagination(page ?? 1, 20);
#endregion
var viewModel = new PermitIndexViewModel
{
BuildingPermits = permitsPagedList,
GridSortOptions = gridSortOptions
};
return View(viewModel);
}
What would you suggest I do differently to improve the display speed? At least for subsequent views.
I don't know how AsPagination method works, but we use Skip and Take methods.
So after all filtering is done your code could look like this:
var permitsPagedList = permits.OrderBy(gridSortOptions.Column, gridSortOptions.Direction).Skip(pageSize * (page -1)).Take(pageSize).ToList();
This simple method returns only those rows which we actualy needs.
I have a couple of tables with similar relationship structure to the standard Order, OrderLine tables.
When creating a data context, it gives the Order class an OrderLines property that should be populated with OrderLine objects for that particular Order object.
Sure, by default it will delay load the stuff in the OrderLine property but that should be fairly transparent right?
Ok, here is the problem I have: I'm getting an empty list when I go MyOrder.OrderLines but when I go myDataContext.OrderLines.Where(line => line.OrderId == 1) I get the right list.
public void B()
{
var dbContext = new Adis.CA.Repository.Database.CaDataContext(
"<connectionString>");
dbContext.Connection.Open();
dbContext.Transaction = dbContext.Connection.BeginTransaction();
try
{
//!!!Edit: Imortant to note that the order with orderID=1 already exists
//!!!in the database
//just add some new order lines to make sure there are some
var NewOrderLines = new List<OrderLines>()
{
new OrderLine() { OrderID=1, LineID=300 },
new OrderLine() { OrderID=1, LineID=301 },
new OrderLine() { OrderID=1, LineID=302 },
new OrderLine() { OrderID=1, LineID=303 }
};
dbContext.OrderLines.InsertAllOnSubmit(NewOrderLines);
dbContext.SubmitChanges();
//this will give me the 4 rows I just inserted
var orderLinesDirect = dbContext.OrderLines
.Where(orderLine => orderLine.OrderID == 1);
var order = dbContext.Orders.Where(order => order.OrderID == 1);
//this will be an empty list
var orderLinesThroughOrder = order.OrderLines;
}
catch (System.Data.SqlClient.SqlException e)
{
dbContext.Transaction.Rollback();
throw;
}
finally
{
dbContext.Transaction.Rollback();
dbContext.Dispose();
dbContext = null;
}
}
So as far as I can see, I'm not doing anything particularly strange but I would think that orderLinesDirect and orderLinesThroughOrder would give me the same result set.
Can anyone tell me why it doesn't?
You're just adding OrderLines; not any actual Orders. So the Where on dbContext.Orders returns an empty list.
How you can still find the property OrderLines on order I don't understand, so I may be goofing up here.
[Edit]
Could you update the example to show actual types, especially of the order variable? Imo, it shoud be an IQueryable<Order>, but it's strange that you can .OrderLines into that. Try adding a First() or FirstOrDefault() after the Where.