Swift Diffable Snapshot - Add new Section to Snapshot and Reload Collection View - swift

I have a collection view that has sections and the beginning. I want to add a new section after the user creates data and display data in the new section. Here is the code I have tried but failed. The new section does not appear after running the code.
var snapshot = self.collectionView.diffableDataSource.snapshot()
let recentlySection = Section(title: "Recently Colored",
cells: [userPicture],
sectionType: .recentlyColored)
snapshot.appendSections([recentlySection])
snapshot.appendItems([userPicture], toSection: recentlySection)
self.collectionView.diffableDataSource.apply(snapshot, animatingDifferences: true)
self.collectionView.reloadData()
Thank you!

I have solved my problem. I am leaving answer here in case of anyone encounters the same problem. I forgot to add new section to my collection view sections list.
Adding this line solved my problem.
self.collectionView.sections.insert(recentlySection, at: 2)

Related

Getting an 'NSInternalInconsistencyException', reason: 'Invalid section 0.' when doing a search on multiple sections

I am getting an exception when doing a search on multiple section. It occurs when applying a snapshot on the datasource.
Background: I have (pre-defined) sections, and each section has a collection of items. Sections won't appear in the viewController if there are no items in section. Items are added by a function of the app. Once an item is added in one of the section, datasource update is called and will show the section with the item added.
Problem: Encountering this issue when trying to search for a non-existent item twice. To reproduce, you can enter a non-existent item, then delete the search string via a backspace, then input a non-existing item again, then error will be thrown on the dataSource.apply().
Hoping someone can help. TIA!
Here is the code:
func updateData(on searchItem: String = "") {
//create a snapshot that will be used by the datasource to apply the diff changes
snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
Manager.shared.getAllSections().forEach { section in
let items = section.items
//if search string is empty, we just assign the items of the section,
//else we filter it based on the searchItem
var filteredItems = searchItem.isEmpty ? items :
items.filter { $0.itemName.lowercased().contains(searchItem.lowercased()) }
//if theres no items filtered, we ain't appending any section and items
if filteredItems.count > 0 {
snapshot.appendSections([section])
snapshot.appendItems(filteredItems)
}
}
//when calling apply, i get the exception when calling apply on dataSource
dataSource.apply(snapshot, animatingDifferences: false)
}
//Here is the updateSearchResults delegate method triggered when typing something in the search bar
func updateSearchResults(for searchController: UISearchController) {
guard let searchedItem = searchController.searchBar.text, !searchedItem.isEmpty else {
updateData()
return
}
updateData(on: searchedItem)
}
ok so I think this is some type of internal bug in diffable data source where it doesn't like when you have 0 sections in your collection view, but the workaround I figured out was to just add a dummy section and hide the section header (if you have one).
in your updateData() method you could add:
if snapshot.numberOfItems == 0 {
snapshot.appendSections([YourSection(name: "dummy")])
}
Then if you're using a section header, give that dummy section some identifiable variable that you can use to hide the header.
When dequeuing a supplementary view (header), check if the name == "dummy" then hide the header if so.
It's a hack, but it ends up looking the exact same and you don't have to deal with ugly empty sections being displayed.
My understanding is that that happens when the compositional layout is trying to resolve layout for a section that doesn't exist in the data source.
In my case I was using UICollectionViewCompositionalLayout(sectionProvider:) which was returning old sections when my data source was returning the correct ones but different.
How I fixed it was invalidating the layout: collectionView.collectionViewLayout.invalidateLayout()

Collection view loaded in reverse order

I have a collection view which is loaded based on the API(Get method).The number of entries increases when new entry is added and it is reloaded with new entry at the end.
How to reload the collection view with last showing first, second last as second and so on.
If you want to display array in reverse then you can do it like this:-
array.reversed()
OR
if you want to display last added item on top, then insert item at first index:-
yourArray.append(item)
let indexPath = IndexPath(row: 0, section: 0)
collectionView.insertItems(at: [indexPath])

Looping through entities which describe what action should be taken on screen after keypresses

Please forgive me if I don't describe this question too well, I am new to programming MacOS apps using Swift. I know the way I'm going about this is probably wrong and I just need someone to tell me the right way.
My main app screen
I have a Core Data application that stores an ordered list of entities called Items. These Items are intended to describe a single step in an activity that describes what should happen on screen. If you know the Mac application QLab each Item is like a single cue in QLab.
I have created an Activity class that is designed to read through each Item to determine the Item type and it's related information. Once the Item type has been determined the Activity class needs to present a View with information related to that particular Item and then wait until the user presses the right arrow key to then proceed to the next Item in the Core Data store where the process repeats until all Items have been read. Each time a new Item is read in the loop, the information on the screen should change after the user presses the right arrow each time.
The problem is that I don't know exactly how the best way of going about this should be programatically speaking. I have the code that retrieves the array of Items as an NSFetchRequest:
let moc = (NSApplication.shared.mainWindow?.contentViewController?.representedObject as! NSPersistentDocument).managedObjectContext!
let fetchRequest : NSFetchRequest = Item.fetchRequest()
do {
let items = try moc.fetch(fetchRequest)
print("Found " + String(items.count) + " items to use in the activity.")
for item in items {
print(item.itemType)
// How do I pause this loop for a user keypress after using data from this Item to display?
}
} catch {
print("Error retrieving Items")
}
I can retrieve the keydown event using NSEvent.addLocalMonitorForEvents(matching: .keyDown) and I'm also able to create View Controllers to display the information on a second screen. I just don't know how I should create the 'main loop', so to speak, so that information is displayed and then the app waits until the user presses a key to proceed...
I can share my project code if more information is needed and many thanks to anyone who can enlighten me... :)
You could try using a NSPageController. In your NSPageController you add a ContainerView which will display the ViewControllers that display information for each item. Each ViewController will need a storyboard identifier, e.g. ViewControllerItem1.
Your NSPageController class must conform to the NSPageControllerDelegate protocol and contains an array of ViewControllers to display.
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
arrangedObjects = ["ViewControllerItem1", "ViewControllerItem2", "...","ViewControllerItemN" ]
}
Note about arrangedObjects from the NSPageController documentation: An array containing the objects displayed in the page controller’s view.
Then you implement NSPageControllers viewControllerForIdentifier to return the ViewController that you currently want to display in the ContainerView.
func pageController(_ pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
switch identifier {
case "ViewControllerItem1":
return mainStoryboard().instantiateController(withIdentifier:"ViewControllerItem1") as? ViewControllerItem1
case "...":
default:
}
}
In your action handler for the key down event you implement.
self.navigateForward(sender) or self.navigateBack(sender)
I also implemented this method but I don't remember whether it was required.
func pageControllerDidEndLiveTransition(_ pageController: NSPageController) {
self.completeTransition()
}

Move UITableView row to new position programatically Swift 3

I'm looking to move an item in a tableview to a new position. I have models acting as my data source.
var models = [Model]
var dataTableView = UITableView!
A model class looks like this
class Model {
timestamp: Int64
...
}
The models data source is sorted by timestamp, any item inserted should be in it's right position.
When an item's timestamp changes, I want to move it to the corrected sorted position.
What's the best way to do this?
I'm considering just sorting the array to get the item to it's correct position then find it's index and do the following
beginUpdates()
reloadRows(at: [INSERTED_ITEM_INDEX, OLD_INDEX], with: .bottom)
endUpdates()
So that by reloading both index paths, both cells will display correctly.
Another option would be to find which index the item will occupy if inserted into the list (without actually touching the list) and then do the following:
beginUpdates()
moveRow(at: ORIGINAL_INDEX, to: NEW_INDEX)
reloadRows(at: [ORIGINAL_INDEX, NEW_INDEX], with: .bottom)
endUpdates()
Which is the better approach?
None? would be happy to learn new approaches, thanks :)

Swipe Cell in UICollectionView - Swift

Is there an elegant and short way to progrematiclly swipe between two cells (assuming we have the desired NSIndexPath of the two cells)?
I see few possibilities here, having the information you provide.
1) You can use standard UICollectionView method: - moveItemAtIndexPath:toIndexPath:, but you must update your data source first. For example, assume you already updated data source (note that this example code is useless until you figure out index changes after moving items.):
collectionView.performBatchUpdates({ () -> Void in
collectionView.moveItemAtIndexPath(firstIndexPath,toIndexPath:secondIndexPath)
//note: proper indexes might be changed after moveItem.. method. Play with it and you'll find the proper one for your item.
collectionView.moveItemAtIndexPath(secondIndexPath,toIndexPath:firstIndexPath)
}, completion: { (finish) -> Void in
})
2) You can recalculate your layout if you use custom layout
3) You can just reload collection view with reloadData or reloadItemsAtIndexPaths E.g.:
var dataSourceArray = [1,2,3,4,5]
// some event occurred -> dataSourceArray changed
dataSourceArray = [1,2,5,4,3]
// call collectionView.reloadData() or reloadItemsAtIndexPaths(_:).
If you'll use 1st or 3rd way, in both cases data source must be up to date.