I have a collectionView and deleting cells in it seems wrong.
https://giphy.com/gifs/9VIPAbXq8GScmDNiZa
As you can see when i tried to delete first cell, image overlaid on image. it happens randomly
DataSource for Cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as? CollectionViewCell else { return UICollectionViewCell() }
if let image = ImageModel.images[indexPath.row].image {
cell.configureCell(image: image)
}
return cell
}
Delegate for deleting
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
UIView.animate(withDuration: 0.5) {
if let cell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell {
cell.image.center.x += 300 }
} completion: { Bool in
ImageModel.images.remove(at: indexPath.row)
collectionView.deleteItems(at: [indexPath])
collectionView.reloadData()
}
}
Delegate for downloading new Images
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.presenter?.fetchData()
}
Also i have cell config with reusable cell
override func prepareForReuse() {
super.prepareForReuse()
self.image.image = nil
self.image.center.x = 0
}
What can be wrong with cell?
Related
i have a collectionView with images in cells and by tapping on the item i am deleting it
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
UIView.animate(withDuration: 0.5) {
if let cell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell {
cell.image.center.x += 300 }
} completion: { Bool in
ImageModel.images.remove(at: indexPath.row)
collectionView.deleteItems(at: [indexPath])
}
}
I can delete 1,2,3 cells, but if i scroll it downside sometimes i see on cells that they already moved (like the method UIView.animate was used, but without completion of it) Why is it happens?
I'm trying to present a view controller whenever I tap on a collection view cellwith the zoom effect.
As far as I know Hero framework works using HeroID's, you set the same id in the fromView and in the toView and the frameworks does the hard work for you.
I have set It up this way:
in viewcontroller1 :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! CollectionViewCell
cell.heroID = "profheroid"
let viewcontroller2 = ViewController2()
viewcontroller2.configureController(with: arrayOfModels[indexPath.item])
viewcontroller2.heroModalAnimationType = .zoom
viewcontroller2.modalPresentationStyle = .fullScreen
self.navigationController?.present(viewcontroller2, animated: true, completion: nil)
}
and in viewcontroller2:
override func viewDidLoad() {
super.viewDidLoad()
view.heroID = "profheroid"
}
The problem happens whenever I tap on a collectionviewCell the presentation happens correctly but I see so many cells being presented at the same time.
I think this happens because cell.heroID = "profheroid" is applied to more cells at the same time.
How can I make sure that when the cell is presented the heroID's are only in the cell tapped and in the viewcontroller's view??
I think you must clear this heroID when cell is reused and after VC is presented.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
cell.heroID = nil // or empty
}
I think you must reset heroID to nil in cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: YourCollectionViewCell.description(), for: indexPath) as? YourCollectionViewCell else {
return UICollectionViewCell()
}
cell.heroID = nil
return cell
}
Reset all visible cell's heroID to nil, and only set heroID for cell selected with value before present:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.visibleCells.forEach { cell in
cell.heroID = nil //reset heroID
}
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
cell.heroID = "profheroid"
let viewcontroller2 = ViewController2()
viewcontroller2.configureController(with: arrayOfModels[indexPath.item])
viewcontroller2.heroModalAnimationType = .zoom
viewcontroller2.modalPresentationStyle = .fullScreen
self.navigationController?.present(viewcontroller2, animated: true, completion: nil)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! UsersCollectionViewCell
someRequest(username: self.usernameUrl) { (userInfo, error) in
guard let userInfo = userInfo else {
// present error
return
}
print("Running")
let user_image_url = userInfo.items.map{($0.avatarURL)}
cell.userCellLabel.text = user_name[indexPath.item]
}
return cell
}
Why the code inside someRequest( ... ) is not running when I call reloadData on viewDidAppear?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.collectionViewUsers.reloadData()
}
P.s. Somerequest just perform an Alamofire get
Async Alamofire request must be outside of the cellForItemAt handler. Because cellForItemAt is called every time the cell appeared on the screen, that means it will try to fetch same data for same cell for multiple times while scrolling, which is not intended. So viewDidLoad is a good place to start fetching, after async call completed don't forget to reload collectionview.
var user_image_url: String?
override func viewDidLoad(_ animated: Bool) {
someRequest(username: self.usernameUrl) { (userInfo, error) in
guard let userInfo = userInfo else {
// present error
return
}
print("Running")
user_image_url = userInfo.items.map{($0.avatarURL)}
self.collectionViewUsers.reloadData()
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! UsersCollectionViewCell
if let user_image_url = user_image_url {
//user_image_url is avaliable
}
return cell
}
I have a screen that contains a UITableView and inside some UICollectionViews.
I need to click on UICollectionViewCell and open the next screen, and send some information to this new screen. But I can not.
According to my structure "follows" does not work. I need some help to find another way to do this.
Code: - TableViewCell
class CategoriasTableViewCell: UITableViewCell {
var db: Firestore!
var categoriasArray = [Categorias]()
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var labelTitleCategorias: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
collectionView.dataSource = self
collectionView.delegate = self
/*Firebase*/
let autenticacao = Auth.auth()
autenticacao.addStateDidChangeListener { (autenticacao, usuario) in
if let usuarioLogado = usuario {
} else {
//self.performSegue(withIdentifier: "checkEntrarSegue", sender: nil)
}
}
db = Firestore.firestore()
loadData()
}
func loadData() {
db.collection("Categories")
.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
self.categoriasArray = querySnapshot!.documents.flatMap({Categorias(dictionary: $0.data())})
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
}
}
Code - TableView
class TabHomeViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
let autenticacao = Auth.auth()
autenticacao.addStateDidChangeListener { (autenticacao, usuario) in
if usuario == nil {
self.performSegue(withIdentifier: "logoutAutomatico", sender: nil)
//....
}
}
}
}
extension TabHomeViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellSlide", for: indexPath) as! SlideTableViewCell
return cell
} else if indexPath.row == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellCategorias", for: indexPath) as! CategoriasTableViewCell
//cell.collectionView.reloadData()
return cell
} else if indexPath.row == 2{
let cell = tableView.dequeueReusableCell(withIdentifier: "cellRecomendacoes", for: indexPath) as! RecomendacoesTableViewCell
return cell
} else if indexPath.row == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellPromocoes", for: indexPath) as! PromocoesTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellFamosos", for: indexPath) as! FamososTableViewCell
return cell
}
}
/*func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == 1 {
if let cell = cell as? CategoriasTableViewCell {
cell.collectionView.reloadData()
print("Atualizando Collection1")
}
}
}*/
}
extension TabHomeViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return 215
case 1:
return 200
case 2:
return 300
case 3:
return 400
case 4:
return 500
default:
return UITableViewAutomaticDimension
}
}
}
//COLLECTION CATEGORIAS
extension CategoriasTableViewCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return categoriasArray.count //Int(Constant.totalItem)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//set the image URL
let urlBase = categoriasArray[indexPath.row].foto_horizontal
let imageUrl = URL(string: urlBase)!
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BoxCollectionCategorias", for: indexPath) as! CellCategoriasCollectionViewCell
cell.labelNameCategoria.text = categoriasArray[indexPath.row].nome
cell.imageView.sd_setImage(with: imageUrl) { (image, erro, cache, url) in
// Here my code ...
}
return (cell)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Click... \(categoriasArray[indexPath.row].uid)")
// Here I detect the click on the UICollectionViewCell
}
}
extension CategoriasTableViewCell: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 130)
}
}
I'm using an extension CategoriasTableViewCell: UICollectionViewDataSource {}, to edit the UICollectionView data
You can just create delegate for your TableViewCell
protocol CategoriasTableViewCellDelegate : class {
func categoryTapped(_ cell: CategoriasTableViewCell, categoriasID:Int)
}
class CategoriasTableViewCell: UITableViewCell {
weak var delegate : CategoriasTableViewCellDelegate?
}
And In CategoriasTableViewCell Extention
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if delegate != nil {
delegate?.categoryTapped(self, categoriasID: categoriasArray[indexPath.row].uid)
}
print("Click... \(categoriasArray[indexPath.row].uid)")
// Here I detect the click on the UICollectionViewCell
}
At your TabHomeViewController set cell.delegate = self
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellSlide", for: indexPath) as! SlideTableViewCell
return cell
} else if indexPath.row == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellCategorias", for: indexPath) as! CategoriasTableViewCell
//cell.collectionView.reloadData()
cell.delegate = self
return cell
} else if indexPath.row == 2{
let cell = tableView.dequeueReusableCell(withIdentifier: "cellRecomendacoes", for: indexPath) as! RecomendacoesTableViewCell
return cell
} else if indexPath.row == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellPromocoes", for: indexPath) as! PromocoesTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellFamosos", for: indexPath) as! FamososTableViewCell
return cell
}
}
// now you can get Data in TabHomeViewController
extension TabHomeViewController:CategoriasTableViewCellDelegate {
func categoryTapped(_ cell: CategoriasTableViewCell, categoriasID:Int){
}
}
You need to trigger a segue to reach the other view, or present your new view on top of the current view (which I don't recommend unless you know what you are doing).
To pass your information from one view to another you have several options :
pass it through the segue (one to one)
use protocols & delegates (one to one)
use events & observers (one to many)
use a third class responsible for holding the current data (one to many)
I want to reorder my cells in my UICollectionView. But when I drop my item, the "cellForItemAt" method is called and this will cause the cell to flash (See image below).
What should I do to avoid this behavior ?
Thank you in advance for your help.
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let cellIdentifier = "cell"
private let cells = [""]
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
collectionView.addGestureRecognizer(longPressGesture)
}
//Selectors
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case .began:
guard let selectedIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
}
// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 100)
}
}
You need to call endInteractiveMovement in perfomBatchUpdates.
But whenever endInteractiveMovement triggered, cellForRow called. So cell will be refreshed and new cell will added(check with random color extension). To secure that, you need to save selectedCell in variable. And return that cell when endInteractiveMovement called.
Declare currentCell in ViewController
var isEnded: Bool = true
var currentCell: UICollectionViewCell? = nil
Store selected cell in variable when gesture began & call endInteractiveMovement in performBatchUpdates.
So, your handleLongGesture func look like below:
//Selectors
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case .began:
guard let selectedIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) else {
break
}
isEnded = false
//store selected cell in currentCell variable
currentCell = collectionView.cellForItem(at: selectedIndexPath)
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case .ended:
isEnded = true
collectionView.performBatchUpdates({
self.collectionView.endInteractiveMovement()
}) { (result) in
self.currentCell = nil
}
default:
isEnded = true
collectionView.cancelInteractiveMovement()
}
}
Also need to change cellForRow
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if currentCell != nil && isEnded {
return currentCell!
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
cell.backgroundColor = .random
return cell
}
}
TIP
Use random color extension for better testing
extension UIColor {
public class var random: UIColor {
return UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0)
}
}
EDIT
If you have multiple sections.
Lets take array of array
var data: [[String]] = [["1","2"],
["1","2","3","4","5","6","7"],
["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15"]]
Then you need to maintain data when reordering
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
print("\(sourceIndexPath) -> \(destinationIndexPath)")
let movedItem = data[sourceIndexPath.section][sourceIndexPath.item]
data[sourceIndexPath.section].remove(at: sourceIndexPath.item)
data[destinationIndexPath.section].insert(movedItem, at: destinationIndexPath.item)
}
You can try to call
collectionView.reloadItems(at: [sourceIndexPath, destinationIndexPath])
right after all your updates (drag and drop animation) are done.
For example call it after performBatchUpdates. It will remove blinking.