How to present UIAlertController when finished multiple selection in UICollectionView - ios

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.

Related

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)

UICollectionViewCell reuse causing incorrect UISwitch state

I am having trouble finding a solution for this issue.
I am using UISwitch inside UICollectionViewCell and I am passing a boolean variable to set switch on or off.
The condition is only one switch has to be ON at a time from all cells.
But When I turn one switch on another random switch's tint color changes that means its state changed.
By default switch status is ON in storyboard and even if I set it OFF nothing changes.
I couldn't figure out why is this happening.
Here is my code for cellForItemAtIndexPath
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddEditItemPopupView.cellId, for: indexPath) as! DiscountCollectionViewCell
cell.delegate = self
let currentDiscount = allDiscounts[indexPath.item]
let shouldApplyDiscount = updatedDiscountId == currentDiscount.id
cell.updateCellWith(data: currentDiscount, applyDiscount: shouldApplyDiscount)
return cell
}
And code for cell class
func updateCellWith(data: DiscountModel, applyDiscount: Bool) {
let name = data.title.replacingOccurrences(of: "Discount ", with: "")
self.titleLabel.text = String(format: "%# (%.2f%%)", name, data.value)
self.switchApply.isOn = applyDiscount
self.switchApply.tag = data.id
}
Data source contains objects of DiscountModel which look like this:
{
id: Int!
title: String!
value: Double!
}
Switch value changed method inside cell class:
#IBAction func switchValueChanged(_ sender: UISwitch) {
if sender.isOn {
self.delegate?.switchValueDidChangeAt(index: sender.tag)
}
else{
self.delegate?.switchValueDidChangeAt(index: 0)
}
}
Delegate method inside view controller class:
func switchValueDidChangeAt(index: Int) {
self.updatedDiscountId = index
self.discountCollectionView.reloadData()
}
There are a few improvements I would suggest to your code;
Reloading the entire collection view is a bit of a shotgun
Since it is possible for there to be no discount applied, you should probably use an optional for your selected discount, rather than "0"
Using Tag is often problematic
I would use something like:
var currentDiscount: DiscountModel? = nil
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddEditItemPopupView.cellId, for: indexPath) as! DiscountCollectionViewCell
cell.delegate = self
let item = allDiscounts[indexPath.item]
self.configure(cell, forItem: item)
return cell
}
func configure(_ cell: DiscountCollectionViewCell, forItem item: DiscountModel) {
cell.switchApply.isOn = false
let name = item.title.replacingOccurrences(of: "Discount ", with: "")
self.titleLabel.text = String(format: "%# (%.2f%%)", name, item.value)
guard let selectedDiscount = self.currentDiscount else {
return
}
cell.switchApply.isOn = selectedDiscount.id == item.id
}
func switchValueDidChangeIn(cell: DiscountCollectionViewCell, to value: Bool) {
if value {
if let indexPath = collectionView.indexPath(for: cell) {
self.currentDiscount = self.allDiscounts[indexPath.item]
}
} else {
self.currentDiscount = nil
}
for indexPath in collectionView.indexPathsForVisibleItems {
if let cell = collectionView.cellForItem(at: indexPath) {
self.configure(cell, forItem: self.allDiscounts[indexPath.item])
}
}
}
In your cell:
#IBAction func switchValueChanged(_ sender: UISwitch) {
self.delegate?.switchValueDidChangeIn(cell:self, to: sender.isOn)
}

Reload Collection View in a Collection View Cell through delegation

I have a controller (A) with a Collection View that features 2 cell classes. One of them (B) contains another Collection View. After doing some research, I still cannot figure out how to update the cells in (B) from (A) or elsewhere to get what I want.
Issues
(B) does not reload properly when its button is pressed: the cell with whom the button was tied is still visible even though it is deleted from the userFriendRequests array in (A) in its delegate method. As a bonus I get a crash when I scroll to a new cell in (B) stating that "index is out of range" on the line cell.user = userFriendRequests[indexPath.row].
What I Have
Controller (A)
protocol UserFriendRequestsDelegate: class {
func didPressConfirmFriendButton(_ friendId: String?)
}
/...
fileprivate var userFriendRequests = [User]()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if userFriendRequests.isEmpty == false {
switch indexPath.section {
case 0:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: friendRequestCellId, for: indexPath) as! UserFriendRequests
cell.userFriendRequests = userFriendRequests
cell.delegate = self
return cell
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! UserFriendCell
let user = users[indexPath.row]
cell.user = user
return cell
default:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! UserFriendCell
return cell
}
}
/...
extension AddFriendsController: UserFriendRequestsDelegate {
internal func didPressConfirmFriendButton(_ friendId: String?) {
guard let uid = FIRAuth.auth()?.currentUser?.uid, let friendId = friendId else {
return
}
let userRef = FIRDatabase.database().reference().child("users_friends").child(uid).child(friendId)
let friendRef = FIRDatabase.database().reference().child("users_friends").child(friendId).child(uid)
let value = ["status": "friend"]
userRef.updateChildValues(value) { (error, ref) in
if error != nil {
return
}
friendRef.updateChildValues(value, withCompletionBlock: { (error, ref) in
if error != nil {
return
}
self.setUpRequestsStatusesToConfirmed(uid, friendId: friendId)
DispatchQueue.main.async(execute: {
let index = self.currentUserFriendRequests.index(of: friendId)
self.currentUserFriendRequests.remove(at: index!)
for user in self.userFriendRequests {
if user.id == friendId {
self.userFriendRequests.remove(at: self.userFriendRequests.index(of: user)!)
}
}
self.attemptReloadOfCollectionView()
})
})
}
}
PS: self.attemptReloadOfCollectionView() is a func that simply invalidates a timer, sets it to 0.1 sec and then calls reloadData() on (A)'s Collection View.
CollectionViewCell (B)
weak var delegate: UserFriendRequestsDelegate?
var userFriendRequests = [User]()
/...
#objc fileprivate func confirmFriendButtonPressed(_ sender: UIButton) {
delegate?.didPressConfirmFriendButton(friendId)
}
/...
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return userFriendRequests.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: friendRequestCellId, for: indexPath) as! FriendRequestCell
cell.user = userFriendRequests[indexPath.row]
return cell
}
/...
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let firstName = userFriendRequests[indexPath.row].first_name, let lastName = userFriendRequests[indexPath.row].last_name, let id = userFriendRequests[indexPath.row].id else {
return
}
nameLabel.text = firstName + " " + lastName
friendId = id
confirmButton.addTarget(self, action: #selector(confirmFriendButtonPressed(_:)), for: .touchUpInside)
}
What I want to achieve
Update (B) when a User is removed from the userFriendRequests array in (A), this User being identified by his id passed by (B) through delegation.
Any good soul that might have an idea on how to tackle this issue ?
Thanks in advance for your help !

Deselect all other selections if one cell selected from other section in UICollectionView

How to deselect all other selected cells in UICollectionView in section 0 if any cell selected from section 1.
Below is what i tried so far:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath){
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! SignupStep3CollectionViewCell
cell.layer.borderWidth = 4
cell.layer.masksToBounds = false
cell.layer.borderColor = UIColor.init(red: 46.0/255.0, green: 234.0/255.0, blue: 219.0/255.0, alpha: 1.0).CGColor
cell.layer.cornerRadius = cell.frame.height/2
cell.clipsToBounds = true
if indexPath.section == 1
{
collectionView.selectItemAtIndexPath(nil, animated: true, scrollPosition: .None)
}
}
this is not working please guide.
Try with this.
Swift 3
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
//multiple selection in section 0 otherwise single selection in other section
if indexPath.section == 0 {
return true
}
let indexPaths = collectionView.indexPathsForSelectedItems
if (indexPaths?.count) ?? 0 > 0 {
/// If you need simple way
for index in indexPaths! {
if index.section == indexPath.section {
self.collectionView.deselectItem(at: index, animated: true) // if want deselect previous selection
//return false //if you do not want further selection
}
}
/// If you need some optimization and don't want to run loop each time
/*
let arrIndexPaths = NSArray(array: indexPaths!)
let sectionPrediate = NSPredicate(format: "section == %d", indexPath.section)
let arrSelections = arrIndexPaths.filtered(using: sectionPrediate) as? [IndexPath]
if arrSelections?.count ?? 0 > 0 {
self.collectionView.deselectItem(at: arrSelections![0], animated: true) // if want deselect previous selection
//return false //if you do not want further selection
}*/
}
return true
}
Swift 2.3
func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
//multiple selection in section 0 otherwise single selection in other section
if indexPath.section == 0 {
return true
}
let indexPaths = collectionView.indexPathsForSelectedItems()
if (indexPaths?.count) ?? 0 > 0 {
/// If you need simple way
for index in indexPaths! {
if index.section == indexPath.section {
self.colletionView.deselectItemAtIndexPath(index, animated: true) // if want deselect previous selection
//return false //if you do not want further selection
}
}
/// If you need some optimization and don't want to run loop each time
/*let arrIndexPaths = NSArray(array: indexPaths!)
let sectionPrediate = NSPredicate(format: "section == %d", indexPath.section)
let arrSelections = arrIndexPaths.filteredArrayUsingPredicate(sectionPrediate) as? [NSIndexPath]
if arrSelections?.count ?? 0 > 0 {
self.colletionView.deselectItemAtIndexPath(arrSelections![0], animated: true) // if want deselect previous selection
//return false //if you do not want further selection
}*/
}
return true
}
You can implement following delegate method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let section = indexPath.section
let selectedRows = collectionView.indexPathsForSelectedItems
for i in 0...(selectedRows!.count - 1) {
let path = selectedRows![i] as IndexPath
if(path.section != section) {
collectionView.deselectItem(at: path, animated: false)
}
}
}
On selecting any particular item, you can get all the selected items. And then deselect all the items which are from different section.
Based off Mrugesh's answer, but also taking into consideration the single selection in section 1 requirement:
Swift 2:
func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let indexPaths = collectionView.indexPathsForSelectedItems()
if indexPaths?.count ?? 0 > 0 {
if indexPaths![0].section != indexPath.section || indexPath.section == 1 {
for index in indexPaths! {
collectionView.deselectItemAtIndexPath(index, animated: true)
}
}
}
return true
}
Swift 3:
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
let indexPaths = collectionView.indexPathsForSelectedItems
if (indexPaths?.count) ?? 0 > 0 {
if indexPaths![0].section != indexPath.section || indexPath.section == 1 {
for index in indexPaths! {
collectionView.deselectItem(at: index, animated: true)
}
}
}
return true
}
Depending whether your view controller is a subclass of UICollectionViewController or just implements its delegate and datasource protocols, you may need to override this function.
Used below code to fix the issue:- clear section 0 selections on tap cell of section 1.
// didSelect Method
let indexSet = NSIndexSet(index: 1)
self.collectionViewBrands.reloadSections(indexSet)

Load more activity cell CollectionViewCell

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.

Resources