I have used UICollectionView and I am populating it with results from API. There is a ID, Image, Name, Description and Liked true or false coming in API.
I have implemented all these things and now all the results loads inside this collection view.
extension VCResCourses : UICollectionViewDelegate {
}
extension VCResCourses: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let length = (UIScreen.main.bounds.width - 16)
return CGSize(width: length, height: 135);
}
}
extension VCResCourses : UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSources.numbeOfRowsInEachGroup(section)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let currentCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier,for:indexPath) as! CVCResCourse
let doCourses: [DOCourses] = dataSources.total()
let doCourse = doCourses[indexPath.row]
let courseId = doCourse.Id!
var courseName = doCourse.Name!
let courseAddress = doCourse.Address!
let courseImage = doCourse.Image!
let courseWish = doCourse.Wish!
courseName = courseName.replacingOccurrences(of: "\t", with: "", options: .literal, range: nil)
currentCell.imageView.kf.setImage(with: URL(string: courseImage))
currentCell.textPrimary.text = courseName.capitalized
currentCell.textSecondary.text = courseAddress.capitalized
currentCell.buttonOpen.addTarget(...)
currentCell.buttonFav.addTarget(...)
return currentCell
}
}
What I am trying now is there are two button on each CELL one opens details of the record and other set favourite to true or false.
Now I need to get id of the current record and toggle its favourite state to true or false or open the detail page using the id. I am not able to add action for buttons and perform the favourite or open detail action using ID.
In android I have done this by adding clickListener inside CustomBaseAdapter which perform actions for each row or record of the data. I need to do the same in iOS as well but no luck yet
I am new to iOS development please help
you need to set tag for each button for e,g
currentCell.buttonOpen.tag = indexPath.row
currentCell.buttonFav.tag = indexPath.row
currentCell.buttonOpen.addTarget(self, action: #selector(self.buttonClicked), for: .touchUpInside)
and you can get id via button.tag, for e.g
func buttonClicked(_ sender: UIButton) {
let doCourses: [DOCourses] = dataSources.total()
let doCourse = doCourses[sender.tag]
let courseId = doCourse.Id!
print (courseId)
}
Related
Update on July 8 2022 - Apple appears to have fixed the two finger scrolling bug, although the interaction is still a bit buggy.
Collection view + compositional layout + diffable data source + drag and drop does not seem to work together. This is on a completely vanilla example modeled after this (which works fine.)
Dragging an item with one finger works until you use a second finger to simultaneously scroll, at which point it crashes 100% of the time. I would love for this to be my problem and not an Apple oversight.
I tried using a flow layout and the bug disappears. Also it persists even if I don't use the list configuration of compositional layout, so that's not it.
Any ideas? Potential workarounds? Is this a known issue?
(The sample code below should run as-is on a blank project with a storyboard containing one view controller pointing to the view controller class.)
import UIKit
struct VideoGame: Hashable {
let id = UUID()
let name: String
}
extension VideoGame {
static var data = [VideoGame(name: "Mass Effect"),
VideoGame(name: "Mass Effect 2"),
VideoGame(name: "Mass Effect 3"),
VideoGame(name: "ME: Andromeda"),
VideoGame(name: "ME: Remaster")]
}
class CollectionViewDataSource: UICollectionViewDiffableDataSource<Int, VideoGame> {
// 1
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard let fromGame = itemIdentifier(for: sourceIndexPath),
sourceIndexPath != destinationIndexPath else { return }
var snap = snapshot()
snap.deleteItems([fromGame])
if let toGame = itemIdentifier(for: destinationIndexPath) {
let isAfter = destinationIndexPath.row > sourceIndexPath.row
if isAfter {
snap.insertItems([fromGame], afterItem: toGame)
} else {
snap.insertItems([fromGame], beforeItem: toGame)
}
} else {
snap.appendItems([fromGame], toSection: sourceIndexPath.section)
}
apply(snap, animatingDifferences: false)
}
}
class DragDropCollectionViewController: UIViewController {
var videogames: [VideoGame] = VideoGame.data
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .insetGrouped)))
lazy var dataSource: CollectionViewDataSource = {
let dataSource = CollectionViewDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, model) -> UICollectionViewListCell in
return collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: model)
})
return dataSource
}()
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, VideoGame> { (cell, indexPath, model) in
var configuration = cell.defaultContentConfiguration()
configuration.text = model.name
cell.contentConfiguration = configuration
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
var snapshot = dataSource.snapshot()
snapshot.appendSections([0])
snapshot.appendItems(videogames, toSection: 0)
dataSource.applySnapshotUsingReloadData(snapshot)
}
}
extension DragDropCollectionViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return []
}
let itemProvider = NSItemProvider(object: item.id.uuidString as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
// 4
extension DragDropCollectionViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
//Not needed
}
}
If you download the modern Collectionviews project from Apple, there is one that shows compositional layout, diffable datasource and reordering. However this is only for their new list cells, not a reg CollectionView cell.
You can find it here:
Modern CollectionViews
I am trying to change selected and unselected image on a tap in collection view, but if I select but from the first index it reflecting in other indices. I want only one selection at a time, but it's reflecting on other sections too.
This is my struct for collection view.
struct teamSelected {
var logoImage: String
var isImageSelected: Bool
}
I made a variable for currentIndex
var currentIndex : Int = 0
Here is how data for my collection view looks like:
var teamSelectionList: [teamSelected] = [
teamSelected(logoImage: "ic_team_yellow_big", isImageSelected: false),
teamSelected(logoImage: "ic_team_red_big", isImageSelected: false),
teamSelected(logoImage: "ic_team_purple_big", isImageSelected: false),
teamSelected(logoImage: "ic_team_blue_big", isImageSelected: false),
teamSelected(logoImage: "ic_team_green_big", isImageSelected: false),
teamSelected(logoImage: "ic_team_orange_big", isImageSelected: false)
]
Here is my collection view methods:
extension TeamViewController : UICollectionViewDelegate , UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return teamSelectionList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let teamSelection : TeamSelectionCollectionViewCell = self.teamCollectionView.dequeueReusableCell(withReuseIdentifier: "teamCell", for: indexPath) as! TeamSelectionCollectionViewCell
let row = teamSelectionList[indexPath.row]
teamSelection.logoImage.image = UIImage(named: row.logoImage)
teamSelection.logoButton.isSelected = row.isImageSelected
//teamSelection.logoButton.layer.setValue(row, forKey: "index")
teamSelection.logoButton.tag = indexPath.row
teamSelection.logoButton.addTarget(self, action: #selector(logoButtonTapped), for: .touchUpInside)
teamSelection.seperatorView.isHidden = indexPath.row == 2 || indexPath.row == self.teamSelectionList.count - 1 ? true : false
return teamSelection
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: (teamCollectionView.frame.width / 3), height: 110.0)
}
#objc func logoButtonTapped(sender: UIButton){
// self.teamSelectionList[sender.tag].isImageSelected = true
// self.teamSelectionList[self.currentIndex].isImageSelected = self.currentIndex != sender.tag ? false : true
self.currentIndex = sender.tag
if (teamSelectionList[self.currentIndex].isImageSelected == false){
teamSelectionList[self.currentIndex].isImageSelected = true
sender.setImage(UIImage(named: "ic_radio_selected"), for: UIControl.State.normal)
} else {
teamSelectionList[self.currentIndex].isImageSelected = false
sender.setImage(UIImage(named: "ic_radio_normal"), for: UIControl.State.normal)
}
self.teamCollectionView.reloadData()
}
}
This is the output I'm getting:
You're updating the selection image with sender.setImage after the tap. Collection view is reusing cells and there's no guarantee that the same cell will be used on the next layout.
I suggest you moving it from logoButtonTapped into cellForItemAt, for example like this:
teamSelection.logoButton.isSelected = row.isImageSelected
teamSelection.logoButton.setImage(
UIImage(named: row.isImageSelected ? "ic_radio_normal" : "ic_radio_selected"),
for: UIControl.State.normal
)
Also a couple of tips.
According to swift code style guide, names of classes and structs start with an uppercase letter and use camel case, e.g. struct TeamSelected instead of struct teamSelected. It's much easier to read when you understand wether you're calling a function or a class/struct constructor.
It's much easier to store selected indices instead of storing selection state for each item. It takes less code and decreases mistake probability. You code can be updated like this:
class TeamViewController: UIViewController {
let teamLogoList = [
"ic_team_yellow_big",
"ic_team_red_big",
"ic_team_purple_big",
"ic_team_blue_big",
"ic_team_green_big",
"ic_team_orange_big",
]
var selection = Set<Int>()
}
extension TeamViewController : UICollectionViewDelegate , UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return teamLogoList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let teamSelection : TeamSelectionCollectionViewCell = self.teamCollectionView.dequeueReusableCell(withReuseIdentifier: "teamCell", for: indexPath) as! TeamSelectionCollectionViewCell
let index = indexPath.row
teamSelection.logoImage.image = UIImage(named: teamLogoList[index])
let isImageSelected = selection.contains(index)
teamSelection.logoButton.isSelected = isImageSelected
teamSelection.logoButton.setImage(
UIImage(named: isImageSelected ? "ic_radio_normal" : "ic_radio_selected"),
for: UIControl.State.normal
)
//teamSelection.logoButton.layer.setValue(row, forKey: "index")
teamSelection.logoButton.tag = indexPath.row
teamSelection.logoButton.addTarget(self, action: #selector(logoButtonTapped), for: .touchUpInside)
teamSelection.seperatorView.isHidden = indexPath.row == 2 || indexPath.row == self.teamLogoList.count - 1 ? true : false
return teamSelection
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: (teamCollectionView.frame.width / 3), height: 110.0)
}
#objc func logoButtonTapped(sender: UIButton){
let index = sender.tag
if (selection.contains(index)){
selection.remove(index)
} else {
selection.insert(index)
}
self.teamCollectionView.reloadData()
}
}
I'm trying to load in some data in a feed based off of a user's data through Firebase, however, it isn't working. My application is currently organized so that the user enters on CustomTabBarController and is verified for login and that a profile has been created, retrieving it if needed. Then, I send the user to the feed by:
// Go to home feed
let navController = self.viewControllers![0] as? UINavigationController
let feedVC = navController?.topViewController as? FeedViewController
if feedVC != nil {
feedVC!.getProfilePhotos()
}
My first question - is this the correct way to load in the FeedViewController on a CustomTabBarController? I also make a call to get the profile data ahead of time.
The getProfilePhotos is a set of delegate and protocols, and returns the following way (I have verified that it correctly retrieves photoURLs). The debugger then thinks that there are no more methods to fire after this.
func feedProfilePhotosRetrieved(photoURLs: [String]) {
// Set photos array and reload the tableview
self.photoURLs = photoURLs
cardCollectionView.reloadData()
}
Here is my FeedViewController class, it's properties and viewDidLoad()/viewDidAppear()
var feedModel = FeedViewModel()
var associates = UserProfile.shared().associates
var photoURLs = [String]()
#IBOutlet weak var cardCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
associates = UserProfile.shared().associates
feedModel.delegate = self
cardCollectionView.delegate = self
cardCollectionView.dataSource = self
cardCollectionView.register(CardCollectionViewCell.self, forCellWithReuseIdentifier: "sectionCell")
cardCollectionView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
getProfilePhotos()
}
This is where I create the cells in the collection view. I put a breakpoint at the declaration of "cell", but it isn't firing.
extension FeedViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return associates.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "sectionCell", for: indexPath) as! CardCollectionViewCell
let card = UserProfile.shared().associates[indexPath.row]
cell.name.text = card.name
cell.poscomp.text = card.title + ", " + card.company
// Photo that we're trying to display
let p = photoURLs[indexPath.row]
// Display photo
cell.downloadPhoto(p)
cell.layer.transform = animateCell(cellFrame: cell.frame)
return cell
} }
Are there any blatantly visible errors that I'm missing? Do I have to call the above function when reloadingData() as well? Thanks for your help and let me know if you need additional information.
The method numberOfItemsInSection should return photoURLs.count.
extension FeedViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoURLs.count
}
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)
I have a collection in the ViewController1, if i click that it goes to ViewController2 where i can change a image; when i push back button on the navigation controller to go back in the ViewController1 i should see the image i changed in the ViewController2. My problem is that i need to reload the data of the CollectionView but i can't do it! I already tried to put CollectionView.reloaddata() in the **ViewWillAppear**, but nothing happened! How can i do this?
import UIKit
private let reuseIdentifier = "Cell2"
class CollectionViewControllerStatiVegetarian: UICollectionViewController {
let baza1 = Baza()
#IBOutlet var CollectionViewOut: UICollectionView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
CollectionViewOut.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
let backround = CAGradientLayer().turquoiseColor()
backround.frame = self.view.bounds
self.collectionView?.backgroundView = UIView()
self.collectionView?.backgroundView?.layer.insertSublayer(backround, at: 0)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let size2 = baza1.superTuples(name: "2")
let x = Mirror(reflecting: size2).children.count //
return Int(x+1)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell2", for: indexPath) as! CollectionViewCell_VegetarianStaty
if indexPath.row != 0 {
cell.shapka_stati_vegetarian.image = nil
let superTupl = baza1.superTuplesShapka(Nomer_tupl: (indexPath.row-1))
cell.label.text = superTupl.5
let tupl = baza1.superTuplesShapka(Nomer_tupl: (indexPath.row-1))
if (tupl.2 == 1) {
cell.shapka_stati_vegetarian.image = nil
cell.shapka_stati_vegetarian.image = UIImage(named: "fon_galochka.png")
} else {}
} else {
cell.shapka_stati_vegetarian.image = UIImage(named: "shapkastaty")
cell.label.text = ""
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let screenWidth = UIScreen.main.fixedCoordinateSpace.bounds.width
let height = screenWidth*550/900+20
var size = CGSize(width: screenWidth, height: 73)
if indexPath.row==0 {
size = CGSize(width: screenWidth, height: height)
}
return size
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row != 0 {
numb_cell = indexPath.row
let bazaSh = Baza()
let f = bazaSh.superTuplesShapka(Nomer_tupl: (indexPath.row-1) )
let vc = storyboard?.instantiateViewController(withIdentifier: "ViewStaty") as! ViewController
vc.obr = f.3
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
Views are loaded only once in the lifetime of a view controller, so viewDidLoad is only run once.
One way to do this is to reload the data in viewWillAppear which is fired when the view appears, but this might run many times.
Another way is to have a delegate method of vc2 that is implemented by vc1. This delegate method is run when the data is changed in vc2 and since vc1 implements the delegate, it can then choose to reload the view.
Yet another way, and one that I prefer, is to use something like Core Data as a model. That way when vc2 changes the data, vc1 can be observing the state of objects it is interested in and react to changes in the model through the NSFetchedResultsControllerDelegate methods.
You could choose to use Realm as a persistence mechanism, and I'm sure there is a similar way to observe the model and react to changes.
inn order to reloadData in background thread, you need to use
DispatchQueue.main.async { self.collectionView.reloadData() }
implement your vc from
UICollectionViewDelegate , UICollectionViewDataSource
then in
viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self