Load more activity cell CollectionViewCell - ios

I'm trying to create a activityCell, so when the user reach the button it will show an cell with an activity indicator. This seem to work fine however if moreDataAvailable is false it should remove this cell. However i keep getting following error?
'NSInternalInconsistencyException', reason: 'attempt to delete item 0 from section 1 which only contains 0 items before the update'
numberOfItemsInSection
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if section == 0 {
return organizationArray.count
} else {
if self.moreDataAvailable == true {
return 1
} else {
return 0
}
}
}
Hide Collection Cell
func hideCollectionViewFooter() {
self.collectionView!.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 1)])
}
numberOfSectionsInCollectionView
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 2
}
cellForItemAtIndexPath
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("OrganizationCell", forIndexPath: indexPath) as! OrganizationCollectionViewCell
cell.customerLabel?.text = organizationArray[indexPath.item].name.uppercaseString
cache.fetch(key: organizationArray[indexPath.item].coverPhoto).onSuccess { data in
cell.customerImageView?.image = UIImage(data: data)
}
return cell
} else {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ActivityCell", forIndexPath: indexPath) as UICollectionViewCell
return cell
}
}
Load More when reach bottom
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if !loadingData && indexPath.item == organizationArray.count - 1 && self.moreDataAvailable {
self.loadingData = true
proposeAccess(false, success: {
self.loadingData = false
})
}
}
Update Organization and check if more data is available
func updateOrganizations(refresh: Bool) {
let realm = try! Realm()
GetOrganization.request(String(self.lastLoadedPage), limit: String(limit), location: self.lastLocation!, radius: String(100), refresh: refresh,
success: { numberOfResults in
//Sort by distance
self.organizationArray = GetOrganization.sortOrganizationsByDistanceFromLocation(realm.objects(Organization), location: self.lastLocation!)
self.lastLoadedPage = self.lastLoadedPage + 1
if numberOfResults < self.limit {
//Hide FooterView
self.moreDataAvailable = false
self.hideCollectionViewFooter()
}
}, error: {
self.organizationArray = GetOrganization.sortOrganizationsByDistanceFromLocation(realm.objects(Organization), location: self.lastLocation!)
print("error")
})
}

This error means that you're trying to delete cell that not existed in current table view state. Probably moreDataAvailable already was false before request in updateOrganizations was finished.
I would recommend you using table footer view for displaying activity indicator. Also, after data is loaded you can display a number of loaded items.

Related

How to present UIAlertController when finished multiple selection in UICollectionView

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShiftCollectionViewCell.identifier, for: indexPath) as? ShiftCollectionViewCell else {
return UICollectionViewCell()
}
let model = shiftSection[indexPath.section].options[indexPath.row]
cell.configure(withModel: OptionsCollectionViewCellViewModel(id: 0, data: model.title))
return cell
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
collectionView.indexPathsForSelectedItems?.filter({ $0.section == indexPath.section }).forEach({ collectionView.deselectItem(at: $0, animated: false) })
return true
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = shiftSection[indexPath.section].options[indexPath.row]
print(model.title)
if indexPath.section == 2 {
showAlert()
}
}
my goal is to show alert when finished multiple selection in collectionview
Thankyou in advance :)
Your description is a bit thin so I am guessing/assuming that what you want is kind of; After user selects an item in every section an alert view should be shown.
To achieve this you could have a nullable property for each of possible selection and then check if all of them are set. For instance imagine having
private var timeMode: TimeMode?
private var shift: Shift?
private var startTime: StartTime?
now on didSelectItemAt you would try and fill these properties like:
if indexPath.section == 0 { // TODO: rather use switch statement
timeMode = allTimeModes[indexPath.row]
} else if indexPath.section == 1 {
shift = allShifts[indexPath.row]
} ...
then at the end of this method (preferably call a new method) execute a check like
guard let timeMode = self.timeMode else { return }
guard let shift = self.shift else { return }
guard let startTime = self.startTime else { return }
showAlert()
Alternatively
You can use a collection view property indexPathsForSelectedItems to determine what all is selected in a similar way every time user selects something:
guard let timeModeIndex = collectionView.indexPathsForSelectedItems?.first(where: { $0.section == 0 })?.row else { return }
guard let shiftIndex = collectionView.indexPathsForSelectedItems?.first(where: { $0.section == 1 })?.row else { return }
guard let startTimeIndex = collectionView.indexPathsForSelectedItems?.first(where: { $0.section == 2 })?.row else { return }
showAlert()
I hope this puts you on the right track.

Question about loading collectionView cell repeatedly in Swift

I want to load collectionView Cell repeatedly.
I want to make a dynamic cell, and when 20 cells are loaded, I want to make 40 cells by loading 20 underneath again.
API.requestBookCategory(bookCategory: 9, bookAddPoint: 0, completionHandler: handleBooksCategory(books:error:))
func handleBooksCategory(books: Books?, error: Error?) {
self.booksCategory = books
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
This code retrieves information from 20 books in bookCategory 9.
When importing 1 to 20 book information, insert bookAddPoint 0.
After that, we insert book information into the bookCatagory variable and use bookCategory to display the book information data in the cell.
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return booksCategory?.books.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: "MainBookCell", for: indexPath) as! MainBookCell
func handleImageResponse() {
guard let imageURL = URL(string: booksCategory?.books[indexPath.row].bookImage ?? "") else {
return
}
API.requestImageFile(url: imageURL, completionHandler: handleImageFileResponse(image:error:))
}
func handleImageFileResponse(image: UIImage?, error: Error?) {
DispatchQueue.main.async {
cell.bookImageView.image = image
}
}
cell.bookTitleLabel.text = booksCategory?.books[indexPath.row].bookTitle
cell.bookWriterLabel.text = booksCategory?.books[indexPath.row].authorName
handleImageResponse()
return cell
}
}
I go through the process up to here, and a cell displaying 20 book information is created.
If a cell showing the 20th book information is created,
API.requestBookCategory (bookCategory: 9, bookAddPoint: 20, completionHandler: handleBooksCategory (books: error :))
I want to run a code that modifies bookAddPoint from 0 to 20, like the code above, to create a cell that displays the 21st to 40th book information. What should I do?
in viewControoller create a variables
var bookAddPoint = 0
var pageLength = 20
func callNextPageData(){
bookAddPoint = booksCategory?.books.count + 1
API.requestBookCategory(bookCategory: 9, bookAddPoint: bookAddPoint, completionHandler: handleBooksCategory(books:error:))
}
change
func handleBooksCategory(books: Books?, error: Error?) {
for book in books{
self.booksCategory.appen(book)
}
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
Change cellforItem
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: "MainBookCell", for: indexPath) as! MainBookCell
func handleImageResponse() {
guard let imageURL = URL(string: booksCategory?.books[indexPath.row].bookImage ?? "") else {
return
}
API.requestImageFile(url: imageURL, completionHandler: handleImageFileResponse(image:error:))
}
func handleImageFileResponse(image: UIImage?, error: Error?) {
DispatchQueue.main.async {
cell.bookImageView.image = image
}
}
cell.bookTitleLabel.text = booksCategory?.books[indexPath.row].bookTitle
cell.bookWriterLabel.text = booksCategory?.books[indexPath.row].authorName
handleImageResponse()
if indexPath.row == self.booksCategory.count-3 // it will load records before you reach end point automatically you can increase or decrease number
{
callNextPageData()
}
return cell
}
NOTE: if you want to do this on load more button just call callNextPageData() in button action or in tableView footer actions.

Smooth animation in UICollectionView when changing data source

I have a UISegmentControl that I use to switch the datasource for a UICollectionView. The datasources are different types of objects.
For example the objects might look like this
struct Student {
let name: String
let year: String
...
}
struct Teacher {
let name: String
let department: String
...
}
And in the view that contains the CollectionView, there would be code like this:
var students = [Student]()
var teachers = [Teachers]()
... // populate these with data via an API
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(segmentControl.titleForSegment(at: segmentControl.selectedSegmentIndex) == "Students") {
return students?.count ?? 0
} else {
return teachers?.count ?? 0
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "personCell", for: indexPath) as! PersonCell
if(segmentControl.titleForSegment(at: segmentControl.selectedSegmentIndex)! == "Students") {
cell.title = students[indexPath.row].name
cell.subtitle = students[indexPath.row].year
} else {
cell.title = teachers[indexPath.row].name
cell.subtitle = teachers[indexPath.row].subject
}
return cell
}
#IBAction func segmentChanged(_ sender: AnyObject) {
collectionView.reloadData()
}
This correctly switches between the two datasources, however it does not animate the change. I tried this:
self.collectionView.performBatchUpdates({
let indexSet = IndexSet(integersIn: 0...0)
self.collectionView.reloadSections(indexSet)
}, completion: nil)
But this just crashes (I think this is because performBatchUpdates gets confused about what to remove and what to add).
Is there any easy way to make this work, without having a separate array storing the current items in the collectionView, or is that the only way to make this work smoothly?
Many thanks in advance!
If your Cell's UI just look the same from different datasource, you can abstract a ViewModel upon your datasource, like this:
struct CellViewModel {
let title: String
let subTitle: String
...
}
Then every time you got data from an API, generate ViewModel dynamically
var students = [Student]()
var teachers = [Teachers]()
... // populate these with data via an API
var viewModel = [CellViewModel]()
... // populate it from data above by checking currently selected segmentBarItem
if(segmentControl.titleForSegment(at: segmentControl.selectedSegmentIndex)! == "Students") {
viewModel = generateViewModelFrom(students)
} else {
viewModel = generateViewModelFrom(teachers)
}
So you always keep one datasource array with your UICollectionView.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "personCell", for: indexPath) as! PersonCell
cell.title = viewModel[indexPath.row].title
cell.subtitle = viewModel[indexPath.row].subTitle
return cell
}
#IBAction func segmentChanged(_ sender: AnyObject) {
collectionView.reloadData()
}
Then try your performBatchUpdates:
self.collectionView.performBatchUpdates({
let indexSet = IndexSet(integersIn: 0...0)
self.collectionView.reloadSections(indexSet)
}, completion: nil)

Fatal error: Index out of range when downloading data from the Internet

There is a function that updates my UICollectionView:
#objc func refreshFeed(sender:AnyObject)
{
fetchWeatherData()
}
private func fetchWeatherData() {
PostArray = [PostModel]()
offset = 0
self.fetchPost()
self.refreshControl.endRefreshing()
}
This function causes the PostArray collection to be nullified and calls the fetchPost ribbon update function:
func fetchPost(URL: String){
ApiService.sharedInstance.fetchPost(url: URL, completion: { (Posts: [PostModel]) in
self.PostArray.append(contentsOf: Posts)
self.collectionView.reloadData()
})
}
After updating the collection, the function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! PostCell
cell.PostArray = PostArray[indexPath.item]
return cell
}
Gives an error message: Thread 1: Fatal error: Index out of range in line: cell.PostArray = PostArray[indexPath.item]
Why is this happening? The data falls into the collection.
Sorry for my English
At first, check that func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int returns valid count of items.
Than you can check if PostArray contains item at given index:
if PostArray.indices ~= indexPath.item {
cell.postArray = PostArray[indexPath.item]
}
And you need to make sure that ApiService.sharedInstance.fetchPost runs completion on main queue. If not, try this:
func fetchPost(URL: String){
ApiService.sharedInstance.fetchPost(url: URL, completion: { (Posts: [PostModel]) in
self.PostArray.append(contentsOf: Posts)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
})
}

UICollectionView Crash on reloadData

UICollectionview is loaded with simple cells and there is a simple int variable for calculating the number of items in the section.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewCount
}
When the value of viewCount (initially at 45) is changed to a lower number (say 12) the app crashes.
This is error message to update the item count:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x17402fbe0> {length = 2, path = 0 - 12}'
I tried reloading data and invalidating the cache as well as said here. This didn't help. I also tried reloading the indexSet before invalidating the cache. That didn't help either.
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == countTextField {
viewCount = Int(textField.text!)!
textField.resignFirstResponder()
let indexSet = IndexSet(integer: 0)
// collectionView.reloadSections(indexSet)
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
}
return true
}
I use 2 custom UICollectinViewLayout and I also set shouldInvalidateLayout to be true.
Any help?
Update
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DemoCell", for: indexPath) as! DemoCollectionViewCell
cell.indexLabel.text = "\(indexPath.item + 1)"
return cell
}
#IBAction func showLayout1(_ sender: Any) {
currentLayout = DemoACollectionViewLayout()
animateLayout()
}
#IBAction func showLayout2(_ sender: Any) {
currentLayout = DemoBCollectionViewLayout()
animateLayout()
}
func animateLayout() {
UIView.animate(withDuration: 0.3) { [weak self] value in
guard let `self` = self else { return }
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.collectionViewLayout = self.currentLayout!
}
}
Here is the sample project.
Update:
I have updated to use an dataObject to reload the UICollectionView but the cache in UICollectionViewLayout is not being cleared when I invalidate.
The cache must be cleared when the number of items for the collectionView has changed.
guard let views = collectionView?.numberOfItems(inSection: 0)
else {
cache.removeAll()
return
}
if views != cache.count {
cache.removeAll()
}
if cache.isEmpty {
//Layout attributes
}

Resources