For some reason my collection view code works as expected when I Launch the emulator but when I hit the home button to close the app and then reopen it the 3 columns collapse on the right to one column. The only thing I have in Storyboard is a view. Everything is done via code. Xcode 11.1 / Swift 5.
Here is what it looks like on the first open:
And on the reopen:
Here is all of the code.
class Online: UIViewController {
weak var collectionView: UICollectionView!
var onlineArray = [[String:AnyObject]]()
override func viewDidLoad() {
super.viewDidLoad()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(collectionView)
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: collectionView.topAnchor),
self.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
self.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
self.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
self.collectionView = collectionView
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.identifier)
self.collectionView.alwaysBounceVertical = true
}
}
extension Online: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell
cell.textLabel1.text = "A"
cell.textLabel2.text = "B"
cell.textLabel3.text = "C"
cell.textLabel1.backgroundColor = .orange
cell.textLabel2.backgroundColor = .blue
cell.textLabel3.backgroundColor = .green
return cell
}
}
extension Online: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: 30)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) //.zero
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
class Cell: UICollectionViewCell {
static var identifier: String = "Cell"
weak var textLabel1: UILabel!
weak var textLabel2: UILabel!
weak var textLabel3: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
let width = self.contentView.frame.size.width
let textLabel1 = UILabel(frame: CGRect(x: 10, y: 0, width: 50, height: 20))
let textLabel2 = UILabel(frame: CGRect(x: width/3, y: 0, width: 50, height: 20))
let textLabel3 = UILabel(frame: CGRect(x: (width/3)*2, y: 0, width: 50, height: 20))
textLabel1.translatesAutoresizingMaskIntoConstraints = false
textLabel2.translatesAutoresizingMaskIntoConstraints = false
textLabel3.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(textLabel1)
self.contentView.addSubview(textLabel2)
self.contentView.addSubview(textLabel3)
self.textLabel1 = textLabel1
self.textLabel2 = textLabel2
self.textLabel3 = textLabel3
self.reset()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
self.reset()
}
func reset() {
self.textLabel1.textAlignment = .left
self.textLabel2.textAlignment = .center
self.textLabel3.textAlignment = .right
}
}
So I guess ill answer my own question.
translatesAutoresizingMaskIntoConstraints should be true not false.
textLabel1.translatesAutoresizingMaskIntoConstraints = true
textLabel2.translatesAutoresizingMaskIntoConstraints = true
textLabel3.translatesAutoresizingMaskIntoConstraints = true
I'm making a project with UICollectionView at swift, I need to execute drag-and-drop with each boxes and swap their position. Each boxes has different data and different size, therefore user need to manipulate their layout, for that i need to enable users can drag-n-drop these Views. I wrote a code but when i start drag-n-drop, event will transfer their data instead of boxes itself. How can i solve this?
import UIKit
class ViewController: UIViewController {
var cellIds = ["1","2","3"]
#IBOutlet weak var collectionView: UICollectionView!
fileprivate var longPressGesture = UILongPressGestureRecognizer();
let cellSizes = [
CGSize(width:190, height:200),
CGSize(width:190, height:200),
CGSize(width:190, height:200),
]
#objc func longTap(_ gesture: UIGestureRecognizer){
switch(gesture.state) {
case .began:
guard let selectedIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) else {
return
}
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case .ended:
collectionView.endInteractiveMovement()
//doneBtn.isHidden = false
//longPressedEnabled = true
self.collectionView.reloadData()
default:
collectionView.cancelInteractiveMovement()
}
}
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longTap(_:)))
collectionView.addGestureRecognizer(longPressGesture)
collectionView!.register(UICustomCollectionViewCell.self, forCellWithReuseIdentifier: "MenuCell")
self.automaticallyAdjustsScrollViewInsets = false
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
//top, left, bottom, right
return UIEdgeInsets(top: 130, left: 1, bottom: 0, right: 10)
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// print("User tapped on \(cellIds[indexPath.row])")
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func userDidEndDragging(_ cell: UICollectionViewCell?) {
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = cellIds.remove(at: sourceIndexPath.item)
cellIds.insert(item, at: destinationIndexPath.item)
print(cellIds);
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cellIds.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MenuCell", for: indexPath) as? UICustomCollectionViewCell
cell!.layer.cornerRadius=10
cell!.layer.masksToBounds=true
var id = cellIds[indexPath.row];
if (id == "1")
{
cell?.lblTest1.text = "amsterdam";
cell?.lblTest1.tag = 100
}
if (id == "2")
{
cell?.lblTest2.text = "madrid";
cell?.lblTest2.tag = 101
}
if (id == "3")
{
cell?.lblTest3.text = "istanbul";
cell?.lblTest3.tag = 102
}
return cell ?? UICollectionViewCell()
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return cellSizes[indexPath.item]
}
}
import UIKit
class UICustomCollectionViewCell: UICollectionViewCell {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(lblTest1)
self.addSubview(lblTest2)
self.addSubview(lblTest3)
}
var lblTest1:UILabel = {
let label1 = UILabel(frame: CGRect(x:10, y: 20, width: 90 , height: 40))
label1.lineBreakMode = .byWordWrapping
label1.numberOfLines = 0
label1.tag = 100;
label1.tintColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
return label1
}()
var lblTest2:UILabel = {
let label2 = UILabel(frame: CGRect(x:10, y: 20, width: 90 , height: 40))
label2.lineBreakMode = .byWordWrapping
label2.numberOfLines = 0
label2.tag = 101;
label2.tintColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
return label2
}()
var lblTest3:UILabel = {
let label3 = UILabel(frame: CGRect(x:10, y: 20, width: 90 , height: 40))
label3.lineBreakMode = .byWordWrapping
label3.numberOfLines = 0
label3.tag = 102;
label3.tintColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
return label3
}()
override func layoutSubviews() {
super.layoutSubviews()
}
}
Here is how I achieved an elegant drag & drop collectionView:
My class where I have the collectionView
class FavoritesVC: UIViewController {
//MARK: IBOutlets
#IBOutlet weak var collectionView: UICollectionView!
//MARK: View Model
let viewModel = FavoritesViewModel()
//MARK: Properties
lazy var dragDropFlowLayout: UICollectionViewLayout = {
let flow = DragDropFlowLayout()
flow.scrollDirection = .vertical
flow.headerReferenceSize = CGSize(width: collectionView.frame.width, height: 0.5)
return flow
}()
//MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
viewModel.competitionsUpdateCallback = {
self.collectionView.reloadData()
}
}
//MARK: Private functions
private func setupCollectionView() {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.collectionViewLayout = dragDropFlowLayout
}
}
//MARK: CollectionView DataSource
extension FavoritesVC: UICollectionViewDataSource {
///...methods for the collectionViewDatasource which aren't relevant here
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destination: IndexPath) {
//save the new order of the objects in the data
...
}
}
//MARK: CollectionView Flow Layout Delegate
extension FavoritesVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = CGSize(width: collectionView.frame.width / 2 - 15, height: 150)
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 10
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 10
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10)
}
}
The Drag&Drop Custom Flow Layout
class DragDropFlowLayout: UICollectionViewFlowLayout {
var longPress: UILongPressGestureRecognizer!
var originalIndexPath: IndexPath?
var draggingIndexPath: IndexPath?
var draggingView: UIView?
var dragOffset = CGPoint.zero
override func prepare() {
super.prepare()
installGestureRecognizer()
}
func applyDraggingAttributes(attributes: UICollectionViewLayoutAttributes) {
attributes.alpha = 0
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
attributes?.forEach { a in
if a.indexPath == draggingIndexPath {
if a.representedElementCategory == .cell {
self.applyDraggingAttributes(attributes: a)
}
}
}
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: indexPath)
if let attributes = attributes, indexPath == draggingIndexPath {
if attributes.representedElementCategory == .cell {
applyDraggingAttributes(attributes: attributes)
}
}
return attributes
}
func installGestureRecognizer() {
if longPress == nil {
longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
longPress.minimumPressDuration = 0.2
collectionView?.addGestureRecognizer(longPress)
}
}
#objc func handleLongPress(_ longPress: UILongPressGestureRecognizer) {
let location = longPress.location(in: collectionView!)
switch longPress.state {
case .began: startDragAtLocation(location: location)
case .changed: updateDragAtLocation(location: location)
case .ended: endDragAtLocation(location: location)
default:
break
}
}
func startDragAtLocation(location: CGPoint) {
guard let cv = collectionView else { return }
guard let indexPath = cv.indexPathForItem(at: location) else { return }
guard cv.dataSource?.collectionView?(cv, canMoveItemAt: indexPath) == true else { return }
guard let cell = cv.cellForItem(at: indexPath) else { return }
originalIndexPath = indexPath
draggingIndexPath = indexPath
draggingView = cell.snapshotView(afterScreenUpdates: true)
draggingView!.frame = cell.frame
cv.addSubview(draggingView!)
dragOffset = CGPoint(x: draggingView!.center.x - location.x, y: draggingView!.center.y - location.y)
draggingView?.layer.shadowPath = UIBezierPath(rect: draggingView!.bounds).cgPath
draggingView?.layer.shadowColor = UIColor.black.cgColor
draggingView?.layer.shadowOpacity = 0.8
draggingView?.layer.shadowRadius = 10
invalidateLayout()
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.55, initialSpringVelocity: 0, options: [], animations: {
self.draggingView?.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}, completion: nil)
}
func updateDragAtLocation(location: CGPoint) {
guard let view = draggingView else { return }
guard let cv = collectionView else { return }
view.center = CGPoint(x: location.x + dragOffset.x, y: location.y + dragOffset.y)
if let newIndexPath = cv.indexPathForItem(at: location) {
cv.moveItem(at: draggingIndexPath!, to: newIndexPath)
draggingIndexPath = newIndexPath
}
}
func endDragAtLocation(location: CGPoint) {
guard let dragView = draggingView else { return }
guard let indexPath = draggingIndexPath else { return }
guard let cv = collectionView else { return }
guard let datasource = cv.dataSource else { return }
let targetCenter = datasource.collectionView(cv, cellForItemAt: indexPath).center
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.55, initialSpringVelocity: 0, options: [], animations: {
dragView.center = targetCenter
dragView.transform = .identity
dragView.layer.shadowColor = UIColor.white.cgColor
dragView.layer.shadowOpacity = 0
dragView.layer.shadowRadius = 0
}) { (completed) in
if indexPath != self.originalIndexPath! {
datasource.collectionView?(cv, moveItemAt: self.originalIndexPath!, to: indexPath)
}
dragView.removeFromSuperview()
self.draggingIndexPath = nil
self.draggingView = nil
self.invalidateLayout()
}
}
}
import UIKit
private let reuseIdentifier = "NewCell"
class SeondViewController: UIViewController, UICollectionViewDelegateFlowLayout {
#IBOutlet var myNavBar: UINavigationBar!
var frutta: [String]!
override func viewDidLoad() {
super.viewDidLoad()
let margins = self.view.layoutMarginsGuide
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 30, left: 10, bottom: 10, right: 10)
layout.scrollDirection = .vertical
let newCollectionView: UICollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
newCollectionView.dataSource = self
newCollectionView.delegate = self
newCollectionView.register(SecondTabCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
newCollectionView.reloadData()
newCollectionView.backgroundColor = UIColor.clear
newCollectionView.isHidden = false
newCollectionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(newCollectionView)
frutta = ["Mele", "Olive", "Pere", "Noci", "Banane", "Kiwi", "Ananas"]
myNavBar.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
myNavBar.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
myNavBar.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
newCollectionView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
newCollectionView.topAnchor.constraint(equalTo: myNavBar.bottomAnchor).isActive = true
newCollectionView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
◦ newCollectionView.bottomAnchor.constraint(equalTo: margins.bottomAnchor).isActive = true
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.size.width / 2, height: collectionView.bounds.size.height / 3)
}
}
extension SecondViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return frutta.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SecondTabCollectionViewCell
myCell.backgroundColor = UIColor.clear
myCell.secondCellLabel = UILabel()
myCell.secondCellLabel.frame.size = CGSize(width: myCell.bounds.width / 2, height: myCell.secondCellLabel.bounds.height / 2)
myCell.secondCellLabel.adjustsFontSizeToFitWidth = true
myCell.secondCellLabel.backgroundColor = UIColor.clear
myCell.secondCellLabel.textColor = UIColor.black
myCell.secondCellLabel.text = frutta[indexPath.item]
myCell.translatesAutoresizingMaskIntoConstraints = false
myCell.contentView.addSubview(myCell.secondCellLabel)
return myCell
}
}
extension SecondViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Hai premuto \(indexPath.row).")
}
}
import UIKit
class SecondTabCollectionViewCell: UICollectionViewCell {
var secondCellLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) non e stato implementato.")
}
}
myCell.secondCellLabel.frame.size = CGSize(width: myCell.bounds.width / 2, height: myCell.secondCellLabel.bounds.height / 2)
The above line is wrong in cellForItemAt, you put the height of label is myCell.secondCellLabel.bounds.height instead of myCell.bounds.height.
correct one is
myCell.secondCellLabel.frame.size = CGSize(width: myCell.bounds.width / 2, height: myCell.bounds.height / 2)
It is a little strange to be laying out a cell's internal views in cellForRowAtIndexPath, this should be done in the cell class itself. The basic problem is that you your UILabel is appearing in the top left corner of the cell and has a height of zero, so you cannot see it. I moved the code you had in cellForRowAtIndexPath into the cell itself.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SecondTabCollectionViewCell
myCell.backgroundColor = UIColor.clear
myCell.secondCellLabel.text = frutta[indexPath.item]
return myCell
}
import UIKit
class SecondTabCollectionViewCell: UICollectionViewCell {
var secondCellLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.textColor = .black
label.adjustsFontSizeToFitWidth = true
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func configureViews() {
addSubview(secondCellLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
configureLayout()
}
private func configureLayout() {
let width = bounds.width / 2
let height = frame.height / 2
let size = CGSize(width: width, height: height)
let origin = CGPoint(x: width/2, y: height/2)
secondCellLabel.frame = CGRect(origin: origin, size: size)
}
}
I am trying to create a program which dynamically updates the collectoinView as the user enters text into the UITextField. Below is the extension I have created to achieve this
I have updated code based on your suggestions to include insert and delete statements,I am still in the process of refactoring and adding delegation but while the code doesn't result in an error it doesn't allow the user to keep typing and closes the keyboard have I implemented everything correctly
Update #2 (Thank you for all of your Help)
import UIKit
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate, UISearchBarDelegate {
lazy var collectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.delegate = self
cv.dataSource = self
return cv
}()
var genericTagsArray:[String] = ["tag1","tag2","tag3","tag4","tag5","tag6","tag7","tag8","tag9","tag10","tag11","tag12","A","B","C","D","E","F","G","Ab","Abc","za","tag1","tag2","tag3","tag4","tag5","tag6","tag7","tag8","tag9","tag10","tag11","tag12","A","B","C","D","E","F","G","Ab","Abc","za"]
var currentTagsArray:[String] = [String]() {
didSet {
collectionView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(collectionView)
collectionView.register(Cell.self, forCellWithReuseIdentifier: "cell")
collectionView.register(Header.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header")
collectionView.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: self.view.leadingAnchor, bottom: self.view.bottomAnchor, trailing: self.view.trailingAnchor, padding: .init(top: 30, left: 0, bottom: 30, right: 0))
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return currentTagsArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
cell.label.text = currentTagsArray[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header", for: indexPath) as! Header
header.searchBar.delegate = self
return header
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.contentSize.width, height: 75)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.view.frame.size.width, height: 50)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.currentTagsArray = self.genericTagsArray.filter { (text) -> Bool in
return text.contains(searchText.lowercased())
}
}
}
class Cell : UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.backgroundColor = .gray
self.addSubview(label)
label.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
}
}
class Header: UICollectionReusableView{
var searchBar = UISearchBar(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.backgroundColor = .gray
self.addSubview(searchBar)
searchBar.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: nil, trailing: self.trailingAnchor, padding: .init(top: 0, left: 5, bottom: 0, right: 5), size: .init(width: 0, height: 50))
}
}
extension UIView {
func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
}
if let leading = leading {
leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true
}
if let trailing = trailing {
trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true
}
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
}
Updated Function With Edits
#objc func textFieldDidChange(){
guard(!(feedSearchBar.text?.isEmpty)!) else{
VC.currentTagsArray = VC.genericTagsArray
VC.feedScreenCollectionView.reloadData()
return
}
VC.currentTagsArray = VC.genericTagsArray.filter({letter -> Bool in
if feedSearchBar.text!.count > letter.count{
return false
}
let stringRange = letter.index(letter.startIndex, offsetBy: feedSearchBar.text!.count)
let subword = letter[..<stringRange]
return subword.lowercased().contains(feedSearchBar.text!.lowercased())
})
if VC.currentTagsArray.isEmpty{
VC.feedScreenCollectionView.deleteItems()
VC.currentTagsArray.insert(feedSearchBar.text!, at: 0)
VC.feedScreenCollectionView.insertItems(at: [IndexPath(item: 0, section: 0)])
//VC.feedScreenCollectionView.reloadItems(inSection: 0)
// VC.feedScreenCollectionView.reloadItems(at: [IndexPath(item: 0, section: 0)])
}
//VC.feedScreenCollectionView.reloadData()
VC.feedScreenCollectionView.reloadItems(inSection: 0)
}
extension UICollectionView{
func reloadItems(inSection section:Int = 0) {
var indicies:[IndexPath] = [IndexPath]()
print("current number of items to reload \(self.numberOfItems(inSection: section))")
for i in 0..<self.numberOfItems(inSection: section){
indicies.append(IndexPath(item: i, section: section))
print("current item number: \(i)")
}
self.reloadItems(at: indicies)
// self.insertItems(at: indicies)
}
func deleteItems(inSection section:Int = 0){
var indicies:[IndexPath] = [IndexPath]()
for i in 0..<self.numberOfItems(inSection: section){
indicies.append(IndexPath(item: i, section: section))
}
self.deleteItems(at: indicies)
}
}
Original Function:
extension UICollectionView{
func reloadItems(inSection section:Int = 0) {
var indicies:[IndexPath] = [IndexPath]()
for i in 0..<self.numberOfItems(inSection: section){
indicies.append(IndexPath(item: i, section: section))
}
self.reloadItems(at: indicies)
}
}
However when ever I call this function after the textField has been edited the program crashes with the error
* Assertion failure in -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3698.54.4/UICollectionView.m:5867
2018-07-18 16:28:02.226606-0400 FeedScreenReuseableView[87752:9704142] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 6 into section 0, but there are only 6 items in section 0 after the update'
I am not sure how to fix this. Is there a better way to go about reloading cells that does not affect the header. Thank you for all of your help in advance. Below is the code from the project in its entirety.
import UIKit
class ViewController: UIViewController,UICollectionViewDelegateFlowLayout,UICollectionViewDelegate,UICollectionViewDataSource,printDelegateWorkedDelegate,updateCollectionView{
func updateCollectionView() {
self.feedScreenCollectionView.reloadData()
}
func printDelegateWorkedDelegate() {
print("The delegate worked")
}
var genericTagsArray:[String] = ["tag1","tag2","tag3","tag4","tag5","tag6","tag7","tag8","tag9","tag10","tag11","tag12","A","B","C","D","E","F","G","Ab","Abc","za","tag1","tag2","tag3","tag4","tag5","tag6","tag7","tag8","tag9","tag10","tag11","tag12","A","B","C","D","E","F","G","Ab","Abc","za"]
var currentTagsArray:[String] = [String]()
var tagsSelected:[String] = [String]()
let keyboardSlider = KeyboardSlider()
var header:feedViewHeader = feedViewHeader()
#IBOutlet weak var feedScreenCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
keyboardSlider.subscribeToKeyboardNotifications(view: view)
currentTagsArray = genericTagsArray
let viewTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(feedViewHeader.viewTapped(gestureRecognizer:)))
viewTapGestureRecognizer.cancelsTouchesInView = false
self.feedScreenCollectionView.addGestureRecognizer(viewTapGestureRecognizer)
feedScreenCollectionView.delegate = self
//
feedScreenCollectionView.dataSource = self
//
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 5, left: 1, bottom: 0, right: 1)
layout.minimumLineSpacing = 0
layout.headerReferenceSize = CGSize(width: 50, height: 75)
layout.sectionHeadersPinToVisibleBounds = true
feedScreenCollectionView.collectionViewLayout = layout
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: 50, height: 75)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "feedViewHeader", for: indexPath) as! feedViewHeader
header.VC = self
return header
}
//
//Data Source
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return currentTagsArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "feedViewCell", for: indexPath) as! feedViewCell
cell.feedImageView.backgroundColor = .blue
cell.feedImageView.clipsToBounds = true
cell.feedImageView.layer.cornerRadius = CGFloat((cell.feedImageView.frame.width)/5)
cell.feedLabel.text = currentTagsArray[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat{
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let collectionViewWidth = collectionView.bounds.width/4.0
let collectionViewHeight = collectionViewWidth
return CGSize(width: collectionViewWidth-4, height: collectionViewHeight+25)
}
var lastContentOffset:CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.lastContentOffset > self.feedScreenCollectionView.contentOffset.y && self.feedScreenCollectionView.contentOffset.y > 0 && self.feedScreenCollectionView.contentOffset.y < self.feedScreenCollectionView.frame.maxY {
self.lastContentOffset = scrollView.contentOffset.y
header.isHidden = false
}
else if (self.lastContentOffset < self.feedScreenCollectionView.contentOffset.y) && (self.feedScreenCollectionView.contentOffset.y < self.feedScreenCollectionView.frame.maxY) && (self.feedScreenCollectionView.contentOffset.y > 0) {
print("you scrolled down,content offSet: \(scrollView.contentOffset.y)->\(self.feedScreenCollectionView.contentOffset.y)")
header.isHidden = true
}
else{
self.lastContentOffset = scrollView.contentOffset.y
print("content offSet: \(scrollView.contentOffset.y)")
print("Nothing happened")
// self.headerDelegate?.hideHeaderView(hide: true)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardFrameChangeNotification(notification:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
var offsetY:CGFloat = 0
#objc func keyboardFrameChangeNotification(notification: Notification) {
}
}
class feedViewCell:UICollectionViewCell{
#IBOutlet weak var feedImageView: UIImageView!
#IBOutlet weak var feedLabel: UILabel!
let keyboardSlider = KeyboardSlider()
override func awakeFromNib() {
super.awakeFromNib()
feedLabel.translatesAutoresizingMaskIntoConstraints = false
feedImageView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
feedImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
feedImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
feedImageView.bottomAnchor.constraint(equalTo: self.feedLabel.topAnchor).isActive = true
feedImageView.translatesAutoresizingMaskIntoConstraints = false
feedLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
feedLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
feedLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
feedLabel.topAnchor.constraint(equalTo: self.feedImageView.bottomAnchor).isActive = true
feedLabel.textAlignment = .center
}
}
class feedViewHeader:UICollectionReusableView,UITextFieldDelegate,UICollectionViewDelegate{
#IBOutlet weak var feedSearchBar: UITextField!
var delegateWorked:printDelegateWorkedDelegate?
var updateCV:updateCollectionView?
var VC:ViewController!
var collectionView:UICollectionView?
var stringToBeSet = "String to be set"
override func awakeFromNib() {
super.awakeFromNib()
feedSearchBar.delegate = self
feedSearchBar.autocorrectionType = .no
feedSearchBar.keyboardType = .default
feedSearchBar.addTarget(self, action: #selector(feedViewHeader.textFieldDidChange), for: .editingChanged)
self.feedSearchBar.borderStyle = .roundedRect
self.feedSearchBar.layer.borderColor = UIColor.black.cgColor
self.feedSearchBar.layer.borderWidth = 4
var searchBarHeight = self.feedSearchBar.bounds.height
self.feedSearchBar.placeholder = "Tap To Search"
self.feedSearchBar.returnKeyType = .search
self.feedSearchBar.rightViewMode = .always
}
#objc func viewTapped(gestureRecognizer:UIGestureRecognizer){
if feedSearchBar.isFirstResponder{
feedSearchBar.resignFirstResponder()
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
VC.feedScreenCollectionView.reloadData()
//VC.feedScreenCollectionView.reloadSections([0])
return true
}
/// Helper to dismiss keyboard
#objc func didStopEditing() {
}
func textFieldDidEndEditing(_ textField: UITextField) {
UIView.setAnimationCurve(UIViewAnimationCurve.easeInOut)
UIView.animate(withDuration: 0.2) {
self.VC.view.frame.origin.y = 0
}
}
#objc func textFieldDidChange(){
guard(!(feedSearchBar.text?.isEmpty)!) else{
VC.currentTagsArray = VC.genericTagsArray
VC.feedScreenCollectionView.reloadData()
return
}
VC.currentTagsArray = VC.genericTagsArray.filter({letter -> Bool in
if feedSearchBar.text!.count > letter.count{
return false
}
let stringRange = letter.index(letter.startIndex, offsetBy: feedSearchBar.text!.count)
let subword = letter[..<stringRange]
return subword.lowercased().contains(feedSearchBar.text!.lowercased())
})
if VC.currentTagsArray.isEmpty{
VC.currentTagsArray.insert(feedSearchBar.text!, at: 0)
}
VC.feedScreenCollectionView.reloadItems(inSection: 0)
}
}
extension Notification.Name{
static let showKeyboard = Notification.Name("showKeyboard")
}
class KeyboardSlider: NSObject {
// variables to hold and process information from the view using this class
weak var view: UIView?
#objc func keyboardWillShow(notification: NSNotification) {
// method to move keyboard up
// view?.frame.origin.y = 0 - getKeyboardHeight(notification as Notification)
print("made it to keyboard will show")
}
func getKeyboardHeight(_ notification:Notification) -> CGFloat {
// get exact height of keyboard on all devices and convert to float value to return for use
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
return keyboardSize.cgRectValue.height
}
func subscribeToKeyboardNotifications(view: UIView) {
// assigning view to class' counterpart
self.view = view
// when UIKeyboardWillShow do keyboardWillShow function
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
}
func unsubscribeFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
}
}
class blankView:UICollectionReusableView{
}
extension UICollectionView{
func reloadItems(inSection section:Int = 0) {
print("made it to reload")
for i in 0..<self.numberOfItems(inSection: section){
self.reloadItems(at: [IndexPath(item: i, section: section)])
}
}
}
You are adding a new string to your VC.currentTagsArray but you aren't calling insertItems(at:) on your collection view, so when numberOfItemsInSection suddenly starts returning more items than it did previously you get the consistency exception.
A couple of other observations on style:
I would use delegation to pass events back to the view controller rather than having the header view and cells holding a reference to the VC and tightly coupling the objects.
the variable VC should be vc. Capital first letter is a class
Just call it currentTags not currentTagsArray
Your problem is because you are only calling insertItemsAt, which does exactly that, inserts an item. You are forgetting to deleteItemsAt, which should delete all the items that you don’t want to display any longer.
BTW you should really consider refactoring your code. It is not easy to read and things are not being done in the right place. For example, your header should never be the one in charge of updating your collection view. You should leave that to the controller itself and use delegation to get the text from the header up to the view controller.
Update
Here is an entire code that does the search for you. It is just an example so you can see how it can be done.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate, UISearchBarDelegate {
lazy var collectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.delegate = self
cv.dataSource = self
return cv
}()
var genericTagsArray:[String] = ["tag1","tag2","tag3","tag4","tag5","tag6","tag7","tag8","tag9","tag10","tag11","tag12","A","B","C","D","E","F","G","Ab","Abc","za","tag1","tag2","tag3","tag4","tag5","tag6","tag7","tag8","tag9","tag10","tag11","tag12","A","B","C","D","E","F","G","Ab","Abc","za"]
var currentTagsArray:[String] = [String]() {
didSet {
collectionView.reloadSections(IndexSet.init(integer: 1))
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(collectionView)
collectionView.register(Cell.self, forCellWithReuseIdentifier: "cell")
collectionView.register(Header.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header")
collectionView.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: self.view.leadingAnchor, bottom: self.view.bottomAnchor, trailing: self.view.trailingAnchor, padding: .init(top: 0, left: 0, bottom: 0, right: 0))
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 { return 0 }
return currentTagsArray.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header", for: indexPath) as! Header
header.searchBar.delegate = self
return header
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if section == 0 {
return CGSize(width: self.view.frame.width, height: 50)
}
return CGSize()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
cell.label.text = currentTagsArray[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.view.frame.size.width, height: 50)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.currentTagsArray = self.genericTagsArray.filter { (text) -> Bool in
return text.contains(searchText.lowercased())
}
}
}
class Cell : UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.backgroundColor = .gray
self.addSubview(label)
label.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
}
}
class Header : UICollectionViewCell {
let searchBar = UISearchBar()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.backgroundColor = .gray
self.addSubview(searchBar)
searchBar.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
}
}
extension UIView {
func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
}
if let leading = leading {
leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true
}
if let trailing = trailing {
trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true
}
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
}