Updating UICollectionView based on Array Comparison - ios

I currently have a collectionView: UICollectionView that displays contents based on an array of objects called projects. At times I will receive an updated array called newProjects from a server. newProjects should replace projects as the data source for the collectionView.
I have figured out a way to update the collectionView when objects have been deleted from the array with the following code:
var indexPaths = [IndexPath]()
for project in projects {
if(!newProjects.contains(project)) {
let index = projects.firstIndex(of: project)
indexPaths.append(IndexPath(item: index!, section: 0))
}
}
collectionView.deleteItems(at: indexPaths)
projects = newProjects
collectionView.reloadData()
This works for deleted projects. However, I am also trying to react to added projects. My code however seems to fail, since I keep getting the exception:
Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (65) must be equal to the number of items contained in that section before the update (64), plus or minus the number of items inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).
My code for added project comparison is:
// First, the deletion from the code above is called.
// project = newProjects and data reload doesn't happen after deletion.
var addIndexPaths = [IndexPath]()
for project in newProjects {
if(!projects.contains(project)) {
projects.append(content)
addIndexPaths.append(IndexPath(item: projects.count - 1, section: 0))
}
}
collectionView.insertItems(at: addIndexPaths)
projects = newProject
collectionView.reloadData()
I'm sure this code is somewhat of a hack job (since it doesn't function), but I honestly can't figure out how else to reliably update a UICollectionView. Does anyone have suggestions or helpful links? Cheers!

As Wez pointed out, just taking out all the insertion and deletion of data did end up working. Now the entirety of the call is just reduced to:
projects = newProjects
collectionView.reloadData()
I initially thought that didn't work, but that was due to a problem with the server. I hadn't tried it yet, thinking I needed to manually insert all changed projects.
As rmaddy pointed out, taking reloadData() out of the original code also worked! My initial thought for some reason was that reloadData() would be necessary after adding and deleting items, probably because I also thought UITableViews required you to call endUpdates()... I overcomplicated some essential concepts.
Thanks everyone!

Related

Is there a way to pull the last CoreData Entry only?

I have a dashboard view that I'd like to display a few variables from my last entered CoreData entry. However, I can't figure out how to fetch only the last data entered into a variable so I can display it. Any ideas?
EDIT: I'm trying to setup a NSFetchRequest inside of a called function that is called only onappear. However, I'm getting errors and am lost.
func singleEntryPull() -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "todaysDate", ascending: false)]
request.fetchLimit = 1
let singleEntry = FetchRequest(fetchRequest: request)
return singleEntry
}
And then the return from the function should only show 1 result and I can then use the returned value to display the variables I need?
Well, not sure if this is the cleanest or best way to do this but I got it working like I want for now until a better solution comes up. I'm still using #FetchRequest which im now aware is pulling data live and updating it, but that might work as if someone keeps the app open overnight and updates it in the morning, I'd want it to display that latest entry. I used this:
ForEach(singleEntry.prefix(1)) { item in
A fetch limit of 1 on a fetch request will return you a single value. However, when you're setting up a #FetchRequest, you're doing more than this - you're making the initial fetch and then continuing to monitor the context for changes, so it live updates. This monitoring only uses the predicate of your fetch request.
Depending on your order of operations, you could be seeing the latest data, and then any new data inserted since you started that view. My experiments with the SwiftUI core data template project prove this out - on initial run you get the a single latest entry, but as you add newer ones, the fetch-limited screen picks up the new entries.
Depending on how this view is actually used, you have two choices - you can do an actual fetch request on appear of the view and store the result as an observable object, or you can make sure you only ever use the first record from the fetch request's results array, which will always be the latest record because of your sort ordering:
var body: some View {
if let latest = singleEntry.first {
// Some view describing the latest entry
} else {
Text("No record")
}
}

Invalid parameter not satisfying: initialSnapshot.numberOfSections == initialSections.count

Working on a new project, our team decided to use UICollectionViewDiffableDataSource to handle our collection views. It works fine, but we're getting (infrequent) crashes logged via an online tool with the message Invalid parameter not satisfying: initialSnapshot.numberOfSections == initialSections.count. We can't seem to reproduce this locally.
The crash occurs at the point where we update the data source with new data, specifically at dataSource.apply(snapshot). We're unsure as to how this could happen, as the data is always created the same.
Specifically, the unit working on this one view decided to forgo the creation of a section model and instead decided to use an Int as section identifiers, because they didn't want to use sections, just display items. That's one thing I hadn't seen before, but an Int satisfies the requirements for identifiers, so the code does compile correctly.
Here is the code:
Collection View and Data Source creation
These variables are inside the class of a UIView which is programmatically created.
var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
lazy var dataSource = UICollectionViewDiffableDataSource<Int, URL>(collectionView: collectionView, cellProvider: provideCell(_:indexPath:item:))
Updating the Datasource
func updateUI() {
var snapshot = dataSource.snapshot()
snapshot.deleteAllItems()
snapshot.appendSections([0])
if let urls = viewModel?.imageUrls {
snapshot.appendItems(urls)
}
dataSource.apply(snapshot)
}
In all (production) cases I have tested, viewModel?.imageUrls is empty on the first call, then contains items on the second call and all calls after that. The number of items usually doesn't change.
I have thought about not using dataSource.snapshot() and instead creating a new one, then I also wouldn't have to call deleteAllItems() every time. But I don't want to just push this as the solution when I can't be sure whether this really fixes the issue.
Has anyone encountered an issue like this before? Is it correct to use an Int as the section identifier? What could be other causes of the crash?
My guess is that you delete only items and each time you add an additional section to your snapshot, which causes an error.
So if your Snapshot already has a "0" section, you do not need to add a new one each time.
In my projects I create a new snapshot every time:
var snapshot = NSDiffableDataSourceSnapshot<Int, URL>()
snapshot.appendSections([0])
if let urls = viewModel?.imageUrls {
snapshot.appendItems(urls, toSection: 0)
}
customDataSource.apply(snapshot, animatingDifferences: animate)
The same approach is used in WWDC videos.

how to delete data in tableview, coredata, firebase database, the order of deleting?

i am using table view which store data, and i upload these data in firebase and then save them in coredata
i want to delete a row in table view, here is the code:
let x = tableViewUsers![indexPath.row].permUser!
refDelete.child(emailReceived).child("Permission").child(x).removeValue()
self.tableViewUsers?.remove(at: indexPath.row)
self.TrackMeTable.deleteRows(at: [indexPath], with: .fade)
//self.TrackMeTable.reloadData()
DataController.deletePermUser(emailAddress: tableViewUsers![indexPath.row])
what are the right order of these line codes, as i get error when i delete for the second time in first line of code with index out of range
After you delete and item the number of items changes, that is why sooner or later you will get an error when deleting rows, because index will be out of bounds. What I suggest is after you delete row, update the tableview. self.TrackMeTable.reloadData().
The error occurs because after
self.tableViewUsers?.remove(at: indexPath.row)
the item at this particular index path does not exist anymore and the next call of indexPath.row raises an out-of-range exception if it was the last item.
Apart from the issue declare tableViewUsers as non-optional to get rid of all the exclamation and question marks
To solve the issue keep a reference to the user before removing it from the array
let x = tableViewUsers[indexPath.row].permUser!
refDelete.child(emailReceived).child("Permission").child(x).removeValue()
let user = tableViewUsers[indexPath.row]
self.tableViewUsers.remove(at: indexPath.row)
self.TrackMeTable.deleteRows(at: [indexPath], with: .fade)
DataController.deletePermUser(emailAddress: user)

Files saved to NSUserDefaults disappear over time after a new file is created

Overview:
I am developing my first application. It collects user location information for tracking a bicycle ride. I save the contents to a .txt file (coordinate information and attribute information) The user can then export their data out of the iOS application for further analysis. I have successfully added a save text file function to store the information.
App while in use:
A user enters a custom title and then that name is appended to a tableview that lists all recorded rides. In the ride creation I call NSUserDefaults, this is critical, without this call to NSUserDefaults, the ride name does not get appended to the tableview listing all the rides.
let saveAction = UIAlertAction(title: "Save", style: .Default,
handler: { (action:UIAlertAction) -> Void in
// Allow for text to be added and appended into the RideTableViewController
let textField = alert.textFields!.first
rideContent.append(textField!.text!)
// Update permanent storage after deleting a ride
NSUserDefaults.standardUserDefaults().setObject(rideContent, forKey: "rideContent")
Now when a user is viewing past rides they can select an old ride and this loads information, everything is good there!
Problem:
Somehow what happens after a while, is a ride will get overwritten/lost? When testing on an iPhone, I can record a ride, then several hours later I check past rides in the tableview and the ride is there, but if I record a new ride and then check past rides the ride recorded several hours earlier is gone, but the new ride is there.
The only other times I call NSUserDefaults is in the tableview. The following instances where I call it are here in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// Set up an if statement so that if there is no saved content it does not throw an error, if there is, populate the information with rideContent, if not leave the list as empty
if NSUserDefaults.standardUserDefaults().objectForKey("rideContent") != nil {
// Save content of rideContent as permanent storage so the ride is saved even when the app has been closed, this restores the old data
rideContent = NSUserDefaults.standardUserDefaults().objectForKey("rideContent") as! [String]
}
}
And further down when I call it if a user swipes to delete a file:
// This method will be called when a user tries to delete an item in the table by swiping to the left
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
// create an if statement to select the delete function after a user swipes to the left on a cell in the table
if editingStyle == UITableViewCellEditingStyle.Delete {
//Delete an item at the row index level
rideContent.removeAtIndex(indexPath.row)
// Update permanent storage after deleting a ride
NSUserDefaults.standardUserDefaults().setObject(rideContent, forKey: "rideContent")
// This is called so that when a file has been deleted, it will reload the view to reflect this change
savedRideTable.reloadData()
}
}
I guess if the following code seems normal, does this just mean that saving to the user documents does not offer true permanent storage of files? Is there a better place to store these files? I hope I am not too vague, I have a hard time tracking down why the app does this, or what exactly is triggering this to happen. Thank you stackExchange for your help!
This is the normal behaviour of NSUserDefaults. You aren't saving a txt file, you are storing a String in NSUserDefaults, these are two different things even if the final result can be similar. When you call:
NSUserDefaults.standardUserDefaults().setObject(rideContent, forKey: "rideContent")
a second time the previous value is replaced with the new one.
You have three options:
Keep using the NSUserDefaults class to store these strings: you can save an array of strings with all the ride data under the rideContent key.
Use real txt files saved inside the NSDocumentDirectory: Read and write data from text file
https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/Introduction/Introduction.html
Use CoreData:
https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/CoreDataSnippets/Introduction/Introduction.html
https://www.raywenderlich.com/115695/getting-started-with-core-data-tutorial

Reloading UITableView shows error?

I am in a strange situation . when i try to reload the tableview using reloadData() it shows the following error . . .
fatal error: unexpectedly found nil while unwrapping an Optional value
Here is the web service method that retrieves output
func didRecieveOutput(results:NSArray) {
if results.count != 0
{
userOrders = results as! NSMutableArray
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.orderList.reloadData() })
}
}
Edit : I had checked my connection as well as delegate & datasource . It works fine with static data . But problem came when I called reloadData(). I had the same problem with static data as well as dynamic (data from server).
There could be n number of reasons for this error. Some of the common causes are:
Your #IBOutlet for your UITableView is not properly connected.
Missing Delegate/Datasource could also be a reason.
Your model that feeds data to table views is being modified just before reloadData() call.
You are not properly checking for nil before using some objects.
Post getting server response, you are creating a new instance instead of using the one that was already loaded.
Another reason could be if your view structure is like this: UITableViewController ---> UIView ---> UITableView, then 'tableView' goes nil and you need to call out [[self.view.subviews objectAtIndex:0] reloadData];. Reference: Apple Discussion Forum.
You can try above cases but for us to pin point the error you would need to share your table view rendering code and flow.
Solution found !!!!
:)
Thanks to Abhinav for giving me the thread even though his suggestion failed.
var array : NSArray = self.view.subviews
array.objectAtIndex(0).reloadData()
I got the same error for this also.
So I tried this and worked
var array : NSArray = self.view.subviews
array.objectAtIndex(2).reloadData()
Here in this array my tableview is at 2nd index. So my suggestion is to check the array to identify the tableview object first and use the index.

Resources