Swift UICollectionView Show/Hide Items on Tap - ios

Within a UICollectionView, I would like to show/hide more content when an item is tapped.
Currently, I am doing this by designing a larger cell on a Storyboard, with just the UILabel I want to always show at the top. When an item is tapped, the didSelectItemAt() and sizeForItemAt() calls are coded as:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndex = indexPath.item
print("didSelectItemAt: \(selectedIndex)")
collectionView.reloadItems(at: [indexPath])
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var size = CGSize()
size.width = collectionView.frame.width
size.height = 50
if let index = selectedIndex {
print("SizeForItemAt item:\(indexPath.item), \(index)")
if index == indexPath.item {
size.height = 150
} else {
size.height = 50
}
}
return size
}
Which has this output (taken from an iPhone Simulator screen capture, converted to GIF). Note, when hiding/reducing the height on the higher item, the blue box animates behind the lower item.
Is there a better way to implement this?

You can achieve this by adding UILabel as collectionview Section and expandad/collapsed view as row. When user select label , you can change rows height
Declare section number :
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
Declare row count in section:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if indexPath.section == 0{
return 1
}
else indexPath.section == 1{
return 1
}
return 0
}
Then fill your section and rows in cellForRowAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath) as! CellViewController
if indexPath.section == 0{
cell.yourLabel.text = "bla bla"
if indexPath.row == 0{
// You can declare another cell in here
let blueViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "blueViewCellID", for: indexPath) as! BlueViewController
}
}
}
And in sizeForItemAt function
func collectionView(_ collectionView : UICollectionView,layout collectionViewLayout : UICollectionViewLayout,sizeForItemAt indexPath : IndexPath) -> CGSize{
if indexPath.section == 0{
if indexPath.row == 0{
if firstSectionOpened == true {
return 50
}else{ return 0 }
}
let width = collectionView.frame.width / CGFloat(4)
return CGSize(width: width, height: width)
}
}
Then in didSelect
var firstSectionOpened : Bool = false
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.section == 0 {
if firstSectionOpened == true{
firstSectionOpened = false
}
else{
firstSectionOpened = true
}
self.myCollection.reloadData()
}
}

Related

multiple cells selected on scrolling [reuse cells problem]

I have a collectionViewCell which contains an image and a label. When I select a cell and scroll ahead I find other cell being selected. Also when I scroll back I find the other cell and not the cell I selected is selected.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return followedUsers.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = attachProfilesCollectionView.dequeueReusableCell(withReuseIdentifier: "attachCells", for: indexPath) as? attachUsersCell
cell!.subscribedUserId = self.followedUsers[indexPath.row].userId
cell?.profileNameToAttah.text = self.followedUsers[indexPath.row].fullName
cell?.profileImageToAttch.loadImagesWithUrl(from: self.followedUsers[indexPath.row].ImagePath)
if mLastSelectedIndex == indexPath.row {
cell?.isSelected = true}
else{cell?.isSelected = false}
return cell!
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 80, height: 110)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = attachProfilesCollectionView.cellForItem(at: indexPath) as! attachUsersCell
cell.subscribedUserId = following[indexPath.item]
usersToAttach.append(cell.subscribedUserId)
attachedCounter += 1
attachCounterFun()
print(usersToAttach)
cell.checkMarkImage.isHidden = false
attachCounterFun()
guard mLastSelectedIndex != indexPath.row else{return}
mLastSelectedIndex = indexPath.row
print("this is addition \(usersToAttach)")
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell_ = attachProfilesCollectionView.cellForItem(at: indexPath) as! attachUsersCell
let toRemove = usersToAttach.index(of: following[indexPath.row])
usersToAttach.remove(at: toRemove!)
attachedCounter -= 1
attachCounterFun()
print(usersToAttach)
cell_.checkMarkImage.isHidden = true
print("this is removal \(usersToAttach)")
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if isSelected {
cell.isSelected = true}
else { isSelected = false }
}
At present I have enabled paging and almost there but occasionally the selected cells change selection upon scroll
Maintain global variable in your controller
var mLastSelectedIndex = -1
Whenever cellForRowAtIndexPath method called check if current cell is selected or not .If this is selected One update its UI. I suggest you to change selected cell UI on cell class only. This way you can easily manage cell tapping and helps you writing clean code.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = attachProfilesCollectionView.dequeueReusableCell(withReuseIdentifier: "attachCells", for: indexPath) as? attachUsersCell
cell!.subscribedUserId = self.followedUsers[indexPath.row].userId
cell?.profileNameToAttah.text = self.followedUsers[indexPath.row].fullName
cell?.profileImageToAttch.loadImagesWithUrl(from: self.followedUsers[indexPath.row].ImagePath)
// here update selected index path
if mLastSelectedIndex == indexPath.row {
cell?.isSelected = true
}
else{
cell?.isSelected = false
}
return cell!
}
Update last selected index on cell tapped.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard mLastSelectedIndex != IndexPath.row else{return}
let indexpath = IndexPath(row:mLastSelectedIndex, section: 0)
mColllectionView.cellForItem(at: indexpath)?.isSelected = !mColllectionView.cellForItem(at: indexpath)!.isSelected
mLastSelectedIndex = IndexPath.row
}
In your attachUsersCell.swift declare isSelected property of cell . The default value of this property is false, which indicates that the cell is not selected.
override var isSelected: Bool{
didSet{
if isSelected {
setSelectedUI()
}
else{
setUnSelectedUI()
}
}
}
func setSelectedUI(){
borderColor = UIColor.systemOrange
borderWidth = 2
cornerRadius = 8
clipsToBounds = true
}
func setUnSelectedUI(){
// reset to default ui
}
Hope this helps!

ios - How to merge two different sections in collection view

I want to merge two different sections that uses two different cells respectively(cellA and cellB). Using the following code I could get a collection view with two sections.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
#IBOutlet var testCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.testCollectionView.delegate = self
self.testCollectionView.dataSource = self
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 3
} else {
return 1
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "taskCell", for: indexPath) as! CellA
cell.layer.cornerRadius = 2
cell.layer.shadowColor = UIColor(red:0.82, green:0.82, blue:0.82, alpha:1.0).cgColor
cell.layer.shadowOffset = CGSize.zero
cell.layer.shadowOpacity = 1
cell.layer.shadowRadius = 4
cell.layer.masksToBounds = false
return cell
} else {
let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: "addNewCell", for: indexPath) as! CellB
return cell2
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = testCollectionView.bounds.width/2 - 30
let height = width
return CGSize(width: width, height: height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(30, 20, 10, 20)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath.row)
}
}
And the output looks like this.
Now, I want the section 1(CellB) to be merged with section 0(CellA). Like this.
I couldn't find the solution to achieve this. Any help will be appreciated.
You can show both cell types in the same section with these changes:
Add properties for the number of aCells and bCells. This is better than putting the magic numbers 3 and 1 in your code because it documents what the numbers represent. If you ever need to change them, you can change them in one spot.
Change numberOfSections to return 1.
Change numberOfItemsInSection to return the sum of the number of A cells and the number of B cells: return aCells + bCells.
In cellForItemAt, compare indexPath.item to aCells to figure out when to switch over to B cells.
var aCells = 3
var bCells = 1
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return aCells + bCells
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item < aCells {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "taskCell", for: indexPath) as! CellA
cell.layer.cornerRadius = 2
cell.layer.shadowColor = UIColor(red:0.82, green:0.82, blue:0.82, alpha:1.0).cgColor
cell.layer.shadowOffset = CGSize.zero
cell.layer.shadowOpacity = 1
cell.layer.shadowRadius = 4
cell.layer.masksToBounds = false
return cell
} else {
let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: "addNewCell", for: indexPath) as! CellB
return cell2
}
}

Adding new Item to CollectionView programmatically, via the last Item

I'm facing an issue with CollectionView, I want to see a collection of images and have an add button as the last item so first of all I've made an array of NSData as I'm gonna save those images to Core Data
var photoArray = [NSData]()
then I implement UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item == photoArray.count + 1 {
present(imagePicker, animated: true, completion: nil)
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoArray.count + 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoItem", for: indexPath) as? PhotoCell else {return UICollectionViewCell()}
cell.btn.tag = indexPath.item
if indexPath.item == photoArray.count + 1 {
cell.thumImage.image = UIImage(named: "add_button")
cell.btn.isHidden = true
print("first")
return cell
} else {
let img = photoArray[indexPath.item]
cell.configureAddingImage(img: img)
print("second")
return cell
}
}
Actually I'm facing with the problem in "cellForItemAt indexPath" like "fatal error: index is out of range" I tried to make array look like var photoArray: [NSData]! but it caused other problems, please give me any advice or help, thank you!
Index is out of range because items or rows are indexed from 0
Should be like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoItem", for: indexPath) as? PhotoCell else {return UICollectionViewCell()}
cell.btn.tag = indexPath.row
if indexPath.row == photoArray.count {
cell.thumImage.image = UIImage(named: "add_button")
cell.btn.isHidden = true
print("first")
return cell
} else {
let img = photoArray[indexPath.row]
cell.configureAddingImage(img: img)
print("second")
return cell
}
}

How to update tableView data when a CollectionViewCell is clicked ( tableview.reloadData() not working inside CollectionView's didSelectItemAt )

Image 1:
I have a collection view and a table view in the same view controller. I want to update the data of my table view, based on the selected item in the collection view. So every time I click on any item of the collectionView, my tableView data should update.
My code:
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
for superSkill in skills!{
if superSkill.name == (skills?[indexPath.row].name)! {
childSkills = superSkill.skills!
}
}
DispatchQueue.main.async{
self.childSkillTableView.reloadData()
}
}
Is there any way i can achieve it
First thing is to make sure your childSkillTableView.dataSource = self and your superSkillsCollectionView.delegate = self
The second thing, there's no reason to use DispatchQueue.main.async({})
The third thing, though less important, instead of a for loop, you might use something like:
childSkills = skills?.first(where { superSkill in superSkill.name == (skills?[indexPath.row].name)! }
Though you should use some if let or guard let statements to check for optionals instead of force unwrapping.
extension PerformanceVC: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return skills.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "superSkillCell", for: indexPath) as! SuperSkillCell
cell.superSkillName.text = skills[indexPath.row].name
//loading image async
ImageAsyncLoader.loadImageAsync(url: (skills[indexPath.row].imageURL)!, imgView: cell.superSkillImage)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: skillCollections.frame.height * 0.9, height: skillCollections.frame.height) //use height whatever you wants.
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print((skills[indexPath.row].name)!)
for skill in skills{
if skill.name == (skills[indexPath.row].name)! {
childSkills = skill.skills!
}
}
self.subSkillTableView.reloadData()
}
}
extension PerformanceVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let openViewHeight: Int = 55
if (indexPath.row == selectedRowIndex.row && isExpanded == false){
isExpanded = true
return CGFloat(openViewHeight + 36 * (childSkills[indexPath.row].skills?.count)!)
} else if (indexPath.row == selectedRowIndex.row && isExpanded == true){
isExpanded = false
return CGFloat(openViewHeight)
} else {
isExpanded = false
return CGFloat(openViewHeight)
}
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return childSkills.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(childSkills[indexPath.row].name)
selectedRowIndex = indexPath
tableView.beginUpdates()
//let cell = tableView.dequeueReusableCell(withIdentifier: "SubSkillTableCell", for: indexPath) as! SubSkillTableCell
let cell = tableView.cellForRow(at: indexPath) as! SubSkillTableCell
for subview in cell.grandSkillStack.subviews {
subview.removeFromSuperview()
}
var grandSkillView: GrandChildSkillItem
grandChildSkills = (childSkills[indexPath.row].skills)!
for grandchildskill in grandChildSkills {
grandSkillView = GrandChildSkillItem(frame: CGRect(x: 0, y: 0, width: 300, height: 30))
grandSkillView.grandChildSkillNameLabel.text = grandchildskill.name
grandSkillView.grandChildSkillProgress.progress = Float(grandchildskill.percentage!)
cell.grandSkillStack.addArrangedSubview(grandSkillView)
}
tableView.endUpdates()
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
let cell = tableView.dequeueReusableCell(withIdentifier: "SubSkillTableCell", for: indexPath) as! SubSkillTableCell
cell.subSkillName.text = childSkills[indexPath.row].name
cell.subSkillProgress.progress = Float(childSkills[indexPath.row].percentage!)
if let uPoints = childSkills[indexPath.row].userPoints {
if let tPoints = childSkills[indexPath.row].totalPoints {
if let count = childSkills[indexPath.row].skills?.count {
cell.subSkillDetail.text = "\(uPoints)" + "/" + "\(tPoints)" + "XP \u{2022} " + "\(count)" + " subskills"
}
}
}
return cell
}
}

How to add an extra static cell to my UICollectionView?

I have an array of photos that I currently display in a UICollectionView. The only thing I still want to add is an extra static cell that should give the user the possibility to open the camera. I used an if-else statement to detect the index. Unfortunately, the console gives me an out of index error.
To be precise: I want this static cell to be in the top left corner, followed by my array of images. Do I have to add two sections, or should I register another custom cell to accomplish this? As of now I can see my extra cell, but it's not working when tapped (out of index).
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count + 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoId, for: indexPath) as! PhotosCollectionViewCell
if indexPath.row == imageArray.count {
cell.backgroundColor = UIColor.lightGray
cell.addGestureRecognizer(UIGestureRecognizer(target: self, action: #selector(tappedCamera)))
} else {
cell.imageView.image = imageArray[indexPath.item]
cell.imageView.addGestureRecognizer(UIGestureRecognizer(target: self, action: #selector(tappedPhoto)))
}
return cell
}
Updated code (solution)
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count + 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == 0 {
let cameraCell = collectionView.dequeueReusableCell(withReuseIdentifier: cameraId, for: indexPath) as! CameraCollectionViewCell
return cameraCell
}
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tappedPhoto))
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoId, for: indexPath) as! PhotoCollectionViewCell
cell.imageView.image = imageArray[indexPath.row - 1]
cell.imageView.addGestureRecognizer(tapGesture)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row == 0 {
print("Camera")
}
}
var startingFrame: CGRect?
var blackBackGroundView: UIView?
var selectedImageFromPicker: UIImage?
var selectedImageCompressed: UIImage?
func tappedPhoto(sender: UIGestureRecognizer) {
if let indexPath = self.collectionView?.indexPathForItem(at: sender.location(in: self.collectionView)) {
let imageView = self.collectionView?.cellForItem(at: indexPath)
startingFrame = imageView?.superview?.convert((imageView?.frame)!, to: nil)
let zoomingImageView = UIImageView(frame: startingFrame!)
zoomingImageView.image = imageArray[indexPath.row - 1]
zoomingImageView.isUserInteractionEnabled = true
zoomingImageView.contentMode = .scaleAspectFill
zoomingImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleZoomOut)))
if let keyWindow = UIApplication.shared.keyWindow {
blackBackGroundView = UIView(frame: keyWindow.frame)
blackBackGroundView?.backgroundColor = UIColor.black
blackBackGroundView?.alpha = 0
keyWindow.addSubview(blackBackGroundView!)
keyWindow.addSubview(chooseLabel)
keyWindow.addSubview(zoomingImageView)
// Set selected image and compress
selectedImageFromPicker = imageArray[indexPath.row - 1]
selectedImageCompressed = selectedImageFromPicker?.resized(withPercentage: 0.1)
chooseLabel.rightAnchor.constraint(equalTo: keyWindow.rightAnchor, constant: -25).isActive = true
chooseLabel.bottomAnchor.constraint(equalTo: keyWindow.bottomAnchor, constant: -25).isActive = true
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackBackGroundView?.alpha = 1
self.chooseLabel.alpha = 1
let height = self.startingFrame!.height / self.startingFrame!.width * keyWindow.frame.width
zoomingImageView.frame = CGRect(x: 0, y: 0, width: keyWindow.frame.width, height: height)
zoomingImageView.center = keyWindow.center
}, completion: {(completed) in
// Do nothing
})
}
}
}
Do I have to add two sections, or should I register another custom
cell to accomplish this?
In your case, just adding one cell at the beginning of the collection should be fair enough, there is no need to multi-section it.
Your methods should be implemented as follows:
1- numberOfItemsInSection method: should be as is:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count + 1
}
2- cellForItemAt method: depends on the first cell, if it should be a different cell:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// first row
if indexPath.row == 0 {
let cameraCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cameraCell-ID", for: indexPath)
// setup the cell...
return cameraCell
}
let defaultCell = collectionView.dequeueReusableCell(withReuseIdentifier: "defaultCell-ID", for: indexPath)
// setup default cell...
return defaultCell
}
Or, if you want it to be the same cell, but with some editions:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell-ID", for: indexPath)
// first row
if indexPath.row == 0 {
// setup the cell as cemera cell...
} else {
// setup the cell as default cell...
}
return cell
}
Actually, there is no need to add UITapGestureRecognizer for each cell, all you have to do is to implement collection​View(_:​did​Select​Item​At:​) delegate method:
Tells the delegate that the item at the specified index path was
selected.
3- didSelectItemAt method:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row == 0 { // camera cell
// handle tapping the camera cell
} else { // default cells
// handle tapping the default cell
// don't forget that:
// getting the first element in 'imageArray' should be imageArray[indexPath.row - 1]
}
}
Hope this helped.

Resources