Collection view cell scrollToItem cast fails [Swift] - swift

In my swift app I've a collection view with a lot of items, I want to scroll to an item and then set a propriety of the collectionview cell custom class, but the cast fails, why?
Here's my code, I've omitted some obviously steps, but the result is printing of "Cast fails":
class ViewController: UIViewController {
lazy var collectionView: UICollectionView = {
let cv = UICollectionView(frame: self.view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
cv.backgroundColor = .gray
cv.register(Cell.self, forCellWithReuseIdentifier: Cell.id)
cv.delegate = self
cv.dataSource = self
return cv
}()
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.id, for: indexPath) as! Cell
cell.backgroundColor = .purple
cell.label.text = "\(indexPath)"
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item == 14 {
collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: .top, animated: true)
guard let cell = collectionView.cellForItem(at: IndexPath(item: 1, section: 0)) as? Cell else {
print("Cast fails")
return
}
cell.label.text = "Something"
}
}
}
Here's the solution I found:
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at position: UICollectionView.ScrollPosition, completion: #escaping () -> ()) {
scrollToItem(at: indexPath, at: .top, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion()
}
}
}

The cast fails as the cell ( of that specific index ) is not visible at the moment you query for it
guard let cell = collectionView.cellForItem(at: IndexPath(item: 1, section: 0)) as? Cell else {
print("Cast fails")
return
}
instead you can try to set , at: .top, animated: false) or wrap the above code snippet inside a Dispatch after block
Btw you should better change the dataSource array and reload that index and that way you don't need a wait
// change source array at that index
// scroll to that row with/without animation
// make sure you set value for the cell label in cellForRowAt table's datasource method

Related

Hero ViewController animation with collectionView - swift

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)
}

How to use a UISwitch to add and remove data from an Array of Data

I have a tableview with a UISwitch in each of the cells. What I am trying to do is that whenever the UISwitch is Toggled On, it adds that cell into an array and when the switch is Toggled Off it removes it. Right now it only adds and doesn't remove.
Once this is complete I need the CollectionView that is also within this ViewController to update and visually show the newStringArray Cells based on the number in that array and that also is able to appear and disappear based on the cells that have their UISwitch toggled on.
import UIKit
class NewMoveViewController: UIViewController {
private var stringSource = ["String 1,", "String 2", "String 3"]
var newStringArray = Array<String>()
private let tableView: UITableView = {
let tableView = UITableView()
tableView.rowHeight = 100
return tableView
}()
private var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 50, height: 50)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.register(NewMoveCollectionViewCell.self, forCellWithReuseIdentifier: NewMoveCollectionViewCell.identifier)
collectionView?.showsHorizontalScrollIndicator = false
title = "Add to Group"
tableView.register(NewMoveTableViewCell.self, forCellReuseIdentifier: NewMoveTableViewCell.identifier)
tableView.delegate = self
tableView.dataSource = self
collectionView?.backgroundColor = .systemBlue
collectionView?.dataSource = self
collectionView?.delegate = self
guard let myCollection = collectionView else {
return
}
view.addSubview(myCollection)
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = CGRect(x: 0, y: 100, width: view.frame.size.width, height: 50)
tableView.frame = CGRect(x: 0, y: 200, width: view.frame.size.width, height: view.frame.size.height)
}
}
extension NewMoveViewController : UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: NewMoveTableViewCell.identifier, for: indexPath) as! NewMoveTableViewCell
let switchView = UISwitch(frame: .zero)
switchView.setOn(false, animated: true)
switchView.tag = indexPath.row
switchView.addTarget(self, action: #selector(self.switchDidChange(_:)), for: .valueChanged)
cell.accessoryView = switchView
cell.configure(with: "", label: "test")
return cell
}
#objc func switchDidChange(_ sender: UISwitch) {
newStringArray.append(stringSource[sender.tag])
// newStringArray.remove(stringSource.remove(at: [s]))
print(newStringArray)
}
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
return false
}
}
extension NewMoveViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return newStringArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewMoveCollectionViewCell.identifier, for: indexPath) as! NewMoveCollectionViewCell
return cell
}
}
the hardest part is to remove an object from an array, my approach on these situations is to transform my array in a NSMutableArray because it have a function to remove an specific object, then make a delegate in the cell that informs the viewController to remove the object from the list and reload the tableView.
the delegate wil be something like this:
protocol RemoveObjectFromCell {
func removeObjectIncell(object: MyObject)
}
class myCell: UITableViewCell {
//your outlets and variables
var object: MyObject?
var delegate: removeObjectIncell?
func setupCell(object: myObject) {
//configure your cell with the specific object
}
}
make sure of calling the delegate on the switch action inside you cell class like this:
#IBAction func switchPressed(sender: UISwitch) {
if !sender.isOn {
self.delegate?.removeObjectIncell(object: self.object)
}
in the view controller implement your protocol and use the required function like this:
class myViewController: UIViewController, RemoveObjectFromCell {
//everything that goes in your class
func removeObjectIncell(object: MyObject) {
self.myArray.remove(object)
DispatchQueue.main.async {
self.myTableView.reloadData()
}
}
}
In order to get changes you want you have to set property which is gonna indicate whether switch is on or off
Something like: var switchIsActive = false
and simply change it in function and if it is turned on you perform one action when it is off you perform another one. Also after function you have to reload your tableView tableView.reloadData()
You can remove elements in your array by their tag by calling Array.remove(at: Int). It can be done by the cells [indexPath.row]

Invisible cells of UICollectionView are not deselected - Swift

I have in my swift app two collection views. One which is a functions categories and another one which is the functions. The first one works as a filter to the second one. If I select "Cat1" then only functions with tag "Cat1" are displayed. This works great.
The functions categories collectionview is horizontal and I need to scroll to see all the cells. My issue/problem is already mentioned in another topics but I can not find the right anwser or technique.
Issue: If I select a category, the cell's background changes, fine. If now I scroll completely to the end of the collectionview and select the last cell, this one change as selected the the first one (previously selected) is not deselected.. I know that is a problem with reused cell but no idea how to manage that. Below my code :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Working with functions categories collection view
if collectionView == self.functionsCategoriesCollectionView {
let cell = collectionView.cellForItem(at: indexPath) as! DevicePageFunctionsCategoriesCVCell
cell.isHidden = false
cell.cellView.isHidden = false
cell.isSelected = true
cell.cellView.clipsToBounds = true
cell.cellView.layer.cornerRadius = 25
cell.cellView.addGradiant(colors: [UIColor(red: 127.0/255.0, green: 127.0/255.0, blue: 127.0/255.0, alpha: 1.0).cgColor, UIColor(red: 47.0/255.0, green: 47.0/255.0, blue: 47.0/255.0, alpha: 1.0).cgColor], angle: 45)
if cellSelectionIndexPath == indexPath {
// it was already selected
cellSelectionIndexPath = nil
collectionView.deselectItem(at: indexPath, animated: true)
cell.cellView.addGradiant(colors: [UIColor.clear.cgColor, UIColor.clear.cgColor], angle: 0)
self.filtered = GlobalVariables.currentProduct.functions.filter { _ in
return true
}
self.functionsCollectionView.reloadData()
} else {
// wasn't yet selected, so let's remember it
cellSelectionIndexPath = indexPath
// Filter with seletec category name
let cellCategoryName = ICDatabase.objects(FunctionCategory.self)[indexPath.row]
self.filtered = GlobalVariables.currentProduct.functions.filter { function in
return function.functionCategory.contains(cellCategoryName)
}
self.functionsCollectionView.reloadData()
}
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if collectionView == self.functionsCategoriesCollectionView {
if let cellToDeselect = collectionView.cellForItem(at: self.cellSelectionIndexPath) as? DevicePageFunctionsCategoriesCVCell {
cellToDeselect.isSelected = false
collectionView.deselectItem(at: self.cellSelectionIndexPath, animated: true)
cellToDeselect.cellView.addGradiant(colors: [UIColor.clear.cgColor, UIColor.clear.cgColor], angle: 0)
self.cellSelectionIndexPath = nil
// Get all functions
self.filtered = GlobalVariables.currentProduct.functions.filter { _ in
return true
}
self.functionsCollectionView.reloadData()
}
}
}
Thanks for your help!
Try this -
var selectedIndexPath: IndexPath?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell.layer.backgroundColor = UIColor.black
self.selectedIndexPath = indexPath
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell.layer.backgroundColor = UIColor.white
self.selectedIndexPath = nil
}
Collection cells reuse memory. So you have to manually manage data and UI. If your requirement is to select only one category cell in collection view at any time, keep one variable in collection view and update it in didSelect method.
var selectedIndexPath: IndexPath?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndexPath = indexPath
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = UICollectionCell()
....
cell.backgroundColor = UIColor.black
if indexPath == selectedIndexPath {
cell.backgroundColor = UIColor.red
}
}

UICollectionview - blink when move item

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.

Reusing cells with hidden / scaled components

I reuse collection view cell as a custom radio button. As a radio button selection I use two views:
border view
mark view
Mark view is hidden and scaled. It is basically simple selection.
In RadioButtonCollectionViewCell :
var checkMark: UIView = {
let view = UIView()
view.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
view.backgroundColor = .darkGreyFts
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override var isSelected: Bool {
get {
return super.isSelected
} set {
super.isSelected = newValue
isSelected ? checkMark.scaleAnimate(1) : checkMark.scaleAnimate(-1)
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.infoLabel.text = nil
self.checkMark.isHidden = false
}
UIView extension :
func scaleAnimate(_ state: Int) {
switch state {
case 1:
self.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
self.transform = CGAffineTransform(scaleX: 1, y: 1)
})
case -1:
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
}, completion: { _ in
self.isHidden = true
})
default:
break
}
}
// MARK: - UICollectionView delegate.
extension DriverFormsViewController : UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? RadioButtonCollectionViewCell {
cell.isSelected = true
collectionCellSelectedAtIndexPath = indexPath
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? RadioButtonCollectionViewCell {
cell.isSelected = false
}
}
}
// MARK: - UICollectionView data source.
extension DriverFormsViewController : UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return optionsDictionary.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as! RadioButtonCollectionViewCell
let key = optionsKeys[indexPath.row]
cell.infoLabel.text = optionsDictionary[key]
cell.checkMark.isHidden = true
if let collectionCellIsSelectedAtIndexPath = collectionCellSelectedAtIndexPath {
if collectionCellIsSelectedAtIndexPath == indexPath {
cell.checkMark.scaleAnimate(1)
}
}
return cell
}
}
How I would be able to reuse view which is hidden/not scaled/not ? Whenever I scroll down and back to the collection selected cell is not selected - mean, check mark is hidden and whenever I click on this cell it goes without animations. Any ideas what might be wrong ?
Thanks in advance!
You have to make sure to deselect the current cell before selecting a new one
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let currentSelectedIndexPath = collectionCellSelectedAtIndexPath,
let currentCell = cellForItem(at: currentSelectedIndexPath) as? RadioButtonCollectionViewCell {
currentCell.isSelected = false
}
if let cell = collectionView.cellForItem(at: indexPath) as? RadioButtonCollectionViewCell {
cell.isSelected = true
collectionCellSelectedAtIndexPath = indexPath
}
}
And instead of using prepareForReuse I would add an else clause in cellForItemAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as! RadioButtonCollectionViewCell
let key = optionsKeys[indexPath.row]
cell.infoLabel.text = optionsDictionary[key]
cell.checkMark.isHidden = true
if let collectionCellIsSelectedAtIndexPath = collectionCellSelectedAtIndexPath,
collectionCellIsSelectedAtIndexPath == indexPath {
cell.checkMark.scaleAnimate(1)
} else {
cell.checkMark.scaleAnimate(-1)
}
return cell
}