Tap button to change UICollectionView Cell size? - swift

I have a collection view on my HomeController (UICollectionView) Class and my PostCell (UICollectionViewCell) class. HomeController has just 4 different cells. I also have a button inside my PostCell that when touched, handles a handleChangeSize function. When I touch that button, I want that specific cells size to change. Preferably change in height, similar to Instagrams "show more" feature on their cells. Any help will be highly, highly appreciated. Thank you... Here's my code:
class HomeController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor(white: 0.90, alpha: 1.0)
collectionView?.register(PostCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.showsVerticalScrollIndicator = false
collectionView?.alwaysBounceVertical = true
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PostCell
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = (view.frame.width) * 9 / 16
return CGSize(width: view.frame.width, height: height + 50 + 50)
}
}
class PostCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
lazy var myButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Change Size", for: .normal)
button.addTarget(self, action: #selector(handleChangeSize), for: .touchUpInside)
return button
}()
func handleChangeSize() {
}
func setupViews() {
addSubview(myButton)
myButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
myButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

I have done this in tableView. follow the same for collectionView.
var expandedCells = [Int]()
here is the cellForRowAtIndexPath method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RequestTableViewCell") as! RequestTableViewCell
cell.selectionStyle = .none
cell.optionButton.tag = indexPath.row
cell.optionButton?.addTarget(self, action: #selector(RequestsViewController.deleteAction), for: UIControlEvents.touchUpInside)
return cell
}
and the action for option button in tableViewCell is
func deleteAction(_ sender: UIButton){
if let button = sender as? UIButton {
// If the array contains the button that was pressed, then remove that button from the array
if expandedCells.contains(sender.tag) {
expandedCells = expandedCells.filter { $0 != sender.tag }
}
// Otherwise, add the button to the array
else {
expandedCells.removeAll()
expandedCells.append(sender.tag)
}
// Reload the tableView data anytime a button is pressed
self.requestTableView.beginUpdates()
self.requestTableView.endUpdates()
}
}
and the heightForRowAtIndexPath method is
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Set the row height based on whether or not the Int associated with that row is contained in the expandedCells array
if expandedCells.contains(indexPath.row) {
return 112
} else {
return 67
}
}

The solution to this problem is quite easy. First, you need to specify what height you want to set for the particular cell and perform that change in handleChangeSize() method (this could be done by simple frame.size.height = ...).
After that, you probably need to resize the whole collection view, otherwise, there are going to be some nasty surprises for you. Perform the calculation (to get the new height) after you have resized the cell (you might call a notification or something that triggers whenever a to resize is necessary).

Related

didSelectItemAt from collectionView not getting called when the collectionView is inside a UITableView row

I have a tableView to feature searches. The second row (indexPath.row = 1) has a UICollectionView inside to display elements horizontally.
As these UICollectionView cells will be users, I want to detect clicks on them, which I cannot achieve. I guess there might be something I don't cover since the collectionView is inside a row of a UITableView. I attach an image where the red squares are the collectionView cells inside a row of a table.
Inside my tableView I have:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! RecentUserCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: recentTextReuseIdentifier, for: indexPath) as! RecentTextCell
cell.viewModel = RecentTextCellViewModel(recentText: recentSearchedText[indexPath.row - 1])
cell.selectionStyle = .none
return cell
}
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! RecentUserCell
return cell
}
}
And the RecentUserCell, has a collectionView inside. The last method, didSelectItemAt, is not getting called, which means I cannot access to a particular user
private let identifier = "collectionCell"
class RecentUserCell: UITableViewCell {
//MARK: - Properties
private var users = [User]()
public let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: layout)
return collectionView
}()
//MARK: - Lifecycle
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
fetchUsers()
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isUserInteractionEnabled = true
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: identifier)
collectionView.showsHorizontalScrollIndicator = false
addSubview(collectionView)
collectionView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - API
func fetchUsers() {
UserService.fetchUsers { users in
self.users = users
self.collectionView.reloadData()
}
}
}
//MARK: - UICollectionViewDataSource
extension RecentUserCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return users.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) //as! UserCollectionViewCell
//cell.delegate = self
cell.backgroundColor = .red
//cell.viewModel = UserCellViewModel(user: users[indexPath.row])
return cell
}
}
//MARK: - UICollectionViewDelegateFlowLayout
extension RecentUserCell: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 70, height: 80)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item at \(indexPath.row)")
}
}
Anyone has an idea of how I can get this method called or what strategy I could use to detect clicks on this UICollectionViewCells?
Your issue is related to adding subviews directly to UITableViewCell and not using contentView. You can read more about UITableViewCell internals here

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]

Reliably using UICollectionView indexPath to access dictionary keys?

I'm trying to use the current CollectionViewcell's IndexPath to access data from a dictionary. The dictionary's keys are of type Int.
This CollectionView has "full page" cells, meaning that each cell takes up the full view area where I am using horizontal scrolling (paging enabled) to navigate between cells.
The dictionary is :
var dataFromServer: [Int: [VillageFestival]]?
Each CollectionViewCell has a TableView inside it, where I plan to have a variable number of rows, depending on how many items there are in [VillageFestival]
However, in CollectionView cellForItemAt indexPath the behaviour of the method is causing some trouble, as printing indexPath.item or setting it as my navigationController's title, returns odd but "understandable" results given how I think dequeueReusableCell works?...
Example: the current index is 0. When I scroll to the right, the current index is now 2. If I navigate to page 6 and then one page back, the current index indicates 3.
I have changed my dictionary keys from Date to String and now to Int, in an attempt to simplify logic.. But the problem persists.
I am using a global pageIndex: Int that is being updated inside CollectionView cellForItemAt
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
pageIndex = indexPath.item
self.navigationController?.navigationBar.topItem?.title = String(pageIndex)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionViewCell
// CollectionViewCell resize to CollectionView Bounds
cell.tableViewCellOffsets = (UIApplication.shared.statusBarFrame.size.height + (self.navigationController?.navigationBar.frame.height ?? 0.0) , self.tabBarController?.tabBar.frame.height ?? 0)
return cell
}
In Tableview numberOfRowsInSection, I'm using pageIndex to access the dictionary values.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let festivals = dataFromServer?[pageIndex] else {return 0}
return festivals.count
}
With my current code, the app displays 0 rows for some pages and 1 row for others. My guess is that the collectionView's cellForItemAt is called before (also possibly after?) the tableView's methods and this makes using a global pageIndex unreliable...
Thanks!
Try this playground, it might help you:
import UIKit
import PlaygroundSupport
class Cell: UICollectionViewCell, UITableViewDataSource {
var data: [Int]! = [] {
didSet {
tableView.reloadData()
}
}
private let tableView: UITableView
override init(frame: CGRect) {
tableView = UITableView(frame: .zero, style: .plain)
super.init(frame: frame)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
tableView.topAnchor.constraint(equalTo: contentView.topAnchor),
tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = String(data[indexPath.item])
return cell
}
}
class VC: UICollectionViewController {
let data = [
0: [1, 2, 3, 4, 5],
1: [6, 4],
2: [5, 5, 5, 5, 6],
3: [9, 9, 8, 4, 5, 5, 5]
]
override init(collectionViewLayout layout: UICollectionViewLayout) {
super.init(collectionViewLayout: layout)
collectionView.isPagingEnabled = true
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
override func viewDidLayoutSubviews() {
(collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize = collectionView.bounds.size
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
cell.data = data[indexPath.item]
return cell
}
}
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
PlaygroundPage.current.liveView = VC(collectionViewLayout: layout)

How do you add UILabel and UIImageView to a CollectionView programmatically?

I've created a collection view programmatically and can't figure out how to add labels and a background image to the cells. Here's the code I have so far for the collection view.
import UIKit
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cell = "cellId"
let text = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Vote"
collectionView?.backgroundColor = UIColor.white
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cell)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cell, for: indexPath)
cell.backgroundColor = UIColor.lightGray
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 200)
}
}
A simple custom UICollectionViewCell class would look like this:
class MyCell : UICollectionViewCell {
var label1: UILabel
var label2: UILabel
var bgImg: UIImageView
}
Edit the viewDidLoad function:
collectionView?.register(MyCell.self, forCellWithReuseIdentifier: cell)
Edit the cellForItemAt function:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cell,
for: indexPath) as! MyCell
cell.bgImg.image = UIImage(named: "img.png")
cell.label1.text = "..."
cell.label2.text = "..."
return cell
Use the init function in the MyCell class to layout the labels.
Create Subclass of collection view cell and Instead of UICollectionViewCell use CustomCollectionViewCell
class CustomCollectionViewCell: UICollectionViewCell {
var imageView: UIImageView?
var label: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
imageView = UIImageView(frame: self.bounds)
//customise imageview
imageView?.backgroundColor = UIColor.red
contentView.addSubview(imageView!)
label = UILabel(frame: CGRect(x: 20, y: 20, width: self.bounds.width - 20, height: 20))
//Customsize label
label?.text = "Hello"
label?.textColor = UIColor.white
contentView.addSubview(label!)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var bounds: CGRect {
didSet {
contentView.frame = bounds
}
}
}
In View Controller updated following code:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Vote"
collectionView?.backgroundColor = UIColor.white
collectionView?.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: cell)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cell, for: indexPath) as! CustomCollectionViewCell
cell.backgroundColor = UIColor.lightGray
return cell
}

UICollectionViewCell changes cell contents when scrolling

I have a custom UICollectionView. I am using a UIView instead of a UICollectionViewCell as I'm wanting to use the UIView elsewhere.
The UICollectionView scrolls only horizontally and has 14 cells; each with the UIView as its contents.
Inside this UIView is another UICollectionView which houses a UICollectionViewCell which is just an image of a dice.
So:
Outer UICollectionView with Cell
The cell has a UIView
The UIView has a UICollectionView and UICollectionViewCell
The UICollectionViewCell is just an imageView of a dice.
The problem I'm having is, when the UICollectionView scrolls horizontally, the contents of the cell are changing when they should remain static; sometimes it randomly moves the contents, or completely removes the contents.
I'm not sure what is causing this to happen.
How can I ensure that upon scrolling that the UICollectionView keeps its contents intact?
Outer UICollectionView:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allLocos?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:OuterCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "OuterCollectionViewCell", for: indexPath) as! OuterCollectionViewCell
if cell.cardView == nil {
let arr = UINib(nibName: "CardView", bundle: nil).instantiate(withOwner: nil, options: nil)
let view = arr[0] as! CardView
cell.contentView.addSubview(view)
cell.cardView = view
}
if let locos = self.allLocos {
let loco:EYLocomotive = locos[indexPath.row]
print(indexPath.row, loco.name, loco.orders)
cell.engineCardView?.setup(loco:loco)
}
cell.layoutIfNeeded()
return cell
}
I think the issue is being caused by this line:
cell.engineCardView?.setup(loco:loco)
My UIView (The cardView) has this code:
#IBOutlet weak var diceCollectionView: UICollectionView!
var dice:[Int] = [Int]()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setup(loco:EYLocomotive) {
self.diceCollectionView.delegate = self
self.diceCollectionView.dataSource = self
self.diceCollectionView.allowsSelection = false
self.diceCollectionView.allowsMultipleSelection = false
self.diceCollectionView.isUserInteractionEnabled = false
self.diceCollectionView.showsVerticalScrollIndicator = false
self.diceCollectionView.showsHorizontalScrollIndicator = false
self.diceCollectionView.register(UINib(nibName: "EYDiceCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "EYDiceCollectionViewCell")
// add dummy dice for testing purposes only
for var _ in 1...3 {
let die = Die().roll
dice.append(die)
}
}
The diceCollectionView is the holder of the Dice Imagery.
When we get to to the UICollectionViewDelegate; in the same file;
// MARK: Collection View Delegate
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dice.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: EYDiceCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "EYDiceCollectionViewCell", for: indexPath) as! EYDiceCollectionViewCell
let dieValue = self.dice[indexPath.row] as Int
let imageFile:String = "die-face-\(dieValue)"
cell.imageView.image = UIImage(named: imageFile)
cell.layoutIfNeeded()
return cell
}
The UICollectionView works fine, the problem comes when I start scrolling horizontally out of the viewport and then scroll back.
Contents of the cell change.
Contents of the cell sometimes change to a different one to the one I'm expecting
Contents of the cell sometimes disappear upon scrolling
What I'd like to know is how can I ensure the contents do not change?
With thanks