Issues with CollectionViewControllerCell not Showing up CollectionViewController - ios

I'm making an iPhone app using Firebase and I'm having a small issue with the following code.
import UIKit
import Firebase
class ChatController: UICollectionViewController, UITextFieldDelegate, UICollectionViewDelegateFlowLayout {
var game: Game? {
didSet {
navigationItem.title = game?.name
}
}
lazy var inputTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter message..."
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.alwaysBounceVertical = true
collectionView?.backgroundColor = UIColor.white
collectionView?.register(ChatMessageCell.self, forCellWithReuseIdentifier: cellId)
setupInputComponents()
}
var messages = [Message]()
func observeMessages() {
let ref = Database.database().reference().child("games").child((game?.id)!).child("messages")
ref.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let message = Message()
message.setValuesForKeys(dictionary)
self.messages.append(message)
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
}
}, withCancel: nil)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath as IndexPath) as! ChatMessageCell
// let message = messages[indexPath.item]
// cell.textView.text = message.text
cell.backgroundColor = UIColor.blue
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: NSIndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 80)
}
// Sets up the entire message sending
// container for the send button and input
// text field
func setupInputComponents() {
// Creates the container for send button
// and input text field
let containerView = UIView()
containerView.backgroundColor = UIColor.white
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
// anchor constraints and x,y,w,h
// for positioning the container
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
containerView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 50).isActive = true
// Creates the send button
let sendButton = UIButton(type: .system)
sendButton.setTitle("Send", for: .normal)
sendButton.translatesAutoresizingMaskIntoConstraints = false
sendButton.addTarget(self, action: #selector(handleSend), for: .touchUpInside)
containerView.addSubview(sendButton)
// Positions the send button within the container
sendButton.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
sendButton.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
sendButton.widthAnchor.constraint(equalToConstant: 80).isActive = true
sendButton.heightAnchor.constraint(equalTo: containerView.heightAnchor).isActive = true
// Creates the input text field
containerView.addSubview(inputTextField)
// Positions the send button within the container
inputTextField.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 8).isActive = true
inputTextField.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
inputTextField.rightAnchor.constraint(equalTo: sendButton.leftAnchor).isActive = true
inputTextField.heightAnchor.constraint(equalTo: containerView.heightAnchor).isActive = true
// Create a seperator for this component
let seperatorLineView = UIView()
seperatorLineView.backgroundColor = UIColor(r: 220, g: 220, b: 220)
seperatorLineView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(seperatorLineView)
// Positions the seperator
seperatorLineView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 8).isActive = true
seperatorLineView.widthAnchor.constraint(equalTo: containerView.widthAnchor).isActive = true
seperatorLineView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
seperatorLineView.heightAnchor.constraint(equalToConstant: 1).isActive = true
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
#objc func handleSend() {
let ref = Database.database().reference().child("games").child("-L6lIJt_cw6saNNWMgu1").child("messages")
ref.keepSynced(true)
let childRef = ref.childByAutoId()
let values = ["text": inputTextField.text!]
childRef.updateChildValues(values)
}
}
I have some commented out pieces which are actually what I'm wanting to display as text in my cells but I can't even get the colored boxes to show up for me inside the CollectionViewController. The DB retrieval code works perfectly fine and I can display what it gives me in a TableViewController. It's just not in the CollectionViewController that I've made here.
What is happening exactly is that the application builds and loads successfully, but when I navigate to this screen, the cells that I am creating aren't showing up. Those lines specifically are the following:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
// return 5
}
override func collectionView(_ collectionView: UICollectionView,, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath as IndexPath) as! ChatMessageCell
// let message = messages[indexPath.item]
// cell.textView.text = message.text
cell.backgroundColor = UIColor.blue
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: NSIndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 80)
}

You need to add the following delegates and data source.
To your class add UICollectionViewDataSource and UICollectionViewDelegate.
Then in your viewDidLoad add self.collectionView?.delegate = selfand self.collectionView?.dataSource = self.
This tells the view controller that it’s delegate and data source methods are in its own class.
This should solve your issue. Sorry if the formatting is off a bit in this answer. On my phone so can’t see the question as I type the answer!

Related

UICollectionView cell loads one image on top of another image

I had everything working perfectly in test.
In production, a user saved a few images, two are ok but for some reason, two are doubling up on top of another image.
When tapping on the image (didSelectItemAt)
collectionView.reloadData()
Gets called and each tap, changes the image to clear it up into just one image.
I've worked back from this point but I'm stuck.
Images loaded in viewDidLoad
db.collection("SAVED IMAGE IDS").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)")
}
else
{
for document in querySnapshot!.documents
{
let id = document.documentID
let Ref = Storage.storage().reference(forURL: "SavedUserImages/\(id)")
Ref.getData(maxSize: 1 * 1024 * 1024)
{
data, error in
if error != nil
{
print("Error: Image could not download!")
}
else
{
let image = UIImage(data: data!)
self.picArray.append(image!)
self.imageID.append(id)
self.collectionView.reloadData()
}
}
}
}
}
Image loads in cell for row
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath as IndexPath)
let data = picArray[indexPath.row]
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
let iv = UIImageView()
cell.contentView.addSubview(iv)
iv.frame = cell.contentView.frame
iv.image = data
collectionView.selectItem(at: IndexPath(item: 0, section: 0) as IndexPath, animated: false, scrollPosition: .init())
return cell
}
Thanks in advance for any help
With this code:
you are adding another image view every time you reload the cell.
Instead, you need to design your cell to already have the image view and change your code to:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath as IndexPath)
let data = picArray[indexPath.row]
cell.iv.image = data
return cell
}
Edit - further explanation and examples...
Based on the code you've shown, you are using a default UICollectionViewCell instead of a custom subclassed cell.
So, if we do a complete example, using SF Symbol images from 0 to 14 for the picArray, using your approach:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
let cellID: String = "cell"
var collectionView: UICollectionView!
var cvWidth: CGFloat = 0
// let's use an array of images for this example
var picArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
// create images 0 through 14
for i in 0..<15 {
if let img = UIImage(systemName: "\(i).circle") {
picArray.append(img)
}
}
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
collectionView.dataSource = self
collectionView.delegate = self
// it appears you're using a default collection view cell class
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellID)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only do this is the collection view frame has changed
if cvWidth != collectionView.frame.width {
cvWidth = collectionView.frame.width
if let fl = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: cvWidth, height: 200.0)
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath as IndexPath)
let data = picArray[indexPath.row]
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
// this is wrong... we're Adding ANOTHER image view every time
let iv = UIImageView()
cell.contentView.addSubview(iv)
iv.frame = cell.contentView.frame
iv.image = data
// this makes no sense, but I'll leave it here
collectionView.selectItem(at: IndexPath(item: 0, section: 0) as IndexPath, animated: false, scrollPosition: .init())
return cell
}
}
It looks like this at the start:
if we scroll all the way down - to where the "14" image should be the bottom cell, it looks like this:
If we scroll back to the top:
and after scrolling up and down several times:
As we can see, as the cells are reused we keep adding more and more image views on top of each other.
So, instead, let's create a simple custom cell subclass that creates and adds an image view when it is created:
class SimpleImageCell: UICollectionViewCell {
let imgView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
imgView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imgView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
and we'll use an almost identical view controller - the only differences are registering our SimpleImageCell class, and using a correct cellForItemAt func:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
let cellID: String = "cell"
var collectionView: UICollectionView!
var cvWidth: CGFloat = 0
// let's use an array of images for this example
var picArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
// create images 0 through 14
for i in 0..<15 {
if let img = UIImage(systemName: "\(i).circle") {
picArray.append(img)
}
}
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
collectionView.dataSource = self
collectionView.delegate = self
// register cell class that already has an image view
collectionView.register(SimpleImageCell.self, forCellWithReuseIdentifier: cellID)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only do this is the collection view frame has changed
if cvWidth != collectionView.frame.width {
cvWidth = collectionView.frame.width
if let fl = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: cvWidth, height: 200.0)
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! SimpleImageCell
let data = picArray[indexPath.item]
cell.imgView.image = data
return cell
}
}
The results:
We can scroll up and down all we want, and we will never have images "on top of" each other.

Invalid Selector Using Delegate Pattern

I am attempting to use the delegate pattern to animate a change in height for a collectionView. The button that triggers this change is in the header. However when I press the button not only does the height not change but it also crashes with the error
'NSInvalidArgumentException', reason: '-[UIButton length]: unrecognized selector sent to instance 0x12f345b50'
I feel like I have done everything right but it always crashes when I click the button. Does anyone see anything wrong and is there anyway that I can animate the change in height for the cell the way I want it to. This is the cell class along with the protocol and the delegate.
import Foundation
import UIKit
protocol ExpandedCellDelegate:NSObjectProtocol{
func viewEventsButtonTapped(indexPath:IndexPath)
}
class EventCollectionCell:UICollectionViewCell {
var headerID = "headerID"
weak var delegateExpand:ExpandedCellDelegate?
public var indexPath:IndexPath!
var eventArray = [EventDetails](){
didSet{
self.eventCollectionView.reloadData()
}
}
var enentDetails:Friend?{
didSet{
var name = "N/A"
var total = 0
seperator.isHidden = true
if let value = enentDetails?.friendName{
name = value
}
if let value = enentDetails?.events{
total = value.count
self.eventArray = value
seperator.isHidden = false
}
if let value = enentDetails?.imageUrl{
profileImageView.loadImage(urlString: value)
}else{
profileImageView.image = imageLiteral(resourceName: "Tokyo")
}
self.eventCollectionView.reloadData()
setLabel(name: name, totalEvents: total)
}
}
let container:UIView={
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 16
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.3
return view
}()
//profile image view for the user
var profileImageView:CustomImageView={
let iv = CustomImageView()
iv.layer.masksToBounds = true
iv.layer.borderColor = UIColor.lightGray.cgColor
iv.layer.borderWidth = 0.3
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
//will show the name of the user as well as the total number of events he is attending
let labelNameAndTotalEvents:UILabel={
let label = UILabel()
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
let seperator:UIView={
let view = UIView()
view.backgroundColor = .lightGray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//collectionview that contains all of the events a specific user will be attensing
lazy var eventCollectionView:UICollectionView={
let flow = UICollectionViewFlowLayout()
flow.scrollDirection = .vertical
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = 0
flow.minimumInteritemSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//will register the eventdetailcell
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .white
cv.register(EventDetailsCell.self, forCellWithReuseIdentifier: "eventDetails")
cv.register(FriendsEventsViewHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: headerID)
cv.delegate = self
cv.dataSource = self
cv.backgroundColor = .blue
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
cv.showsVerticalScrollIndicator = false
cv.bounces = false
return cv
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpCell()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setLabel(name:String,totalEvents:Int){
let mainString = NSMutableAttributedString()
let attString = NSAttributedString(string:name+"\n" , attributes: [NSAttributedStringKey.foregroundColor:UIColor.black,NSAttributedStringKey.font:UIFont.systemFont(ofSize: 14)])
mainString.append(attString)
let attString2 = NSAttributedString(string:totalEvents == 0 ? "No events" : "\(totalEvents) \(totalEvents == 1 ? "Event" : "Events")" , attributes: [NSAttributedStringKey.foregroundColor:UIColor.darkGray,NSAttributedStringKey.font:UIFont.italicSystemFont(ofSize: 12)])
mainString.append(attString2)
labelNameAndTotalEvents.attributedText = mainString
}
}
//extension that handles creation of the events detail cells as well as the eventcollectionview
//notice the delegate methods
//- Mark EventCollectionView DataSource
extension EventCollectionCell:UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return eventArray.count
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerID, for: indexPath) as! FriendsEventsViewHeader
header.viewEventsButton.addTarget(self, action: #selector(viewEventsButtonTapped), for: .touchUpInside)
return header
}
#objc func viewEventsButtonTapped(indexPath:IndexPath){
print("View events button touched")
if let delegate = self.delegateExpand{
delegate.viewEventsButtonTapped(indexPath: indexPath)
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:"eventDetails" , for: indexPath) as! EventDetailsCell
cell.details = eventArray[indexPath.item]
cell.backgroundColor = .yellow
cell.seperator1.isHidden = indexPath.item == eventArray.count-1
return cell
}
}
//- Mark EventCollectionView Delegate
extension EventCollectionCell:UICollectionViewDelegateFlowLayout{
//size for each indvidual cell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 50)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 40)
}
}
This is the view that ultimately is supposed to be handling the expansion via the delegate function.
import UIKit
import Firebase
class FriendsEventsView: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
var friends = [Friend]()
var followingUsers = [String]()
var notExpandedHeight : CGFloat = 100
var expandedHeight : CGFloat?
var isExpanded = [Bool]()
//so this is the main collectonview that encompasses the entire view
//this entire view has eventcollectionCell's in it which in itself contain a collectionview which also contains cells
//so I ultimately want to shrink the eventCollectionView
lazy var mainCollectionView:UICollectionView={
// the flow layout which is needed when you create any collection view
let flow = UICollectionViewFlowLayout()
//setting the scroll direction
flow.scrollDirection = .vertical
//setting space between elements
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = spacingbw
flow.minimumInteritemSpacing = 0
//actually creating collectionview
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//register a cell for that collectionview
cv.register(EventCollectionCell.self, forCellWithReuseIdentifier: "events")
cv.translatesAutoresizingMaskIntoConstraints = false
//changing background color
cv.backgroundColor = .red
//sets the delegate of the collectionView to self. By doing this all messages in regards to the collectionView will be sent to the collectionView or you.
//"Delegates send messages"
cv.delegate = self
//sets the datsource of the collectionView to you so you can control where the data gets pulled from
cv.dataSource = self
//sets positon of collectionview in regards to the regular view
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
return cv
}()
//label that will be displayed if there are no events
let labelNotEvents:UILabel={
let label = UILabel()
label.textColor = .lightGray
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.italicSystemFont(ofSize: 14)
label.text = "No events found"
label.isHidden = true
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
//will set up all the views in the screen
self.setUpViews()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
}
func setUpViews(){
//well set the navbar title to Friends Events
self.title = "Friends Events"
view.backgroundColor = .white
//adds the main collection view to the view and adds proper constraints for positioning
view.addSubview(mainCollectionView)
mainCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
mainCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
mainCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
mainCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
//adds the label to alert someone that there are no events to the collectionview and adds proper constrains for positioning
mainCollectionView.addSubview(labelNotEvents)
labelNotEvents.centerYAnchor.constraint(equalTo: mainCollectionView.centerYAnchor, constant: 0).isActive = true
labelNotEvents.centerXAnchor.constraint(equalTo: mainCollectionView.centerXAnchor, constant: 0).isActive = true
//will fetch events from server
self.fetchEventsFromServer()
}
// MARK: CollectionView Datasource for maincollection view
//will let us know how many eventCollectionCells tht contain collectionViews will be displayed
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print(friends.count)
isExpanded = Array(repeating: false, count: friends.count)
return friends.count
}
//will control the size of the eventCollectionCells that contain collectionViews
height is decided for the collectionVIew here
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let event = friends[indexPath.item]
if let count = event.events?.count,count != 0{
notExpandedHeight += (CGFloat(count*40)+10)
}
self.expandedHeight = notExpandedHeight
if isExpanded[indexPath.row] == true{
return CGSize(width: collectionView.frame.width, height: expandedHeight!)
}else{
return CGSize(width: collectionView.frame.width, height: 100)
}
}
//will do the job of effieicently creating cells for the eventcollectioncell that contain eventCollectionViews using the dequeReusableCells function
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "events", for: indexPath) as! EventCollectionCell
cell.backgroundColor = UIColor.orange
cell.indexPath = indexPath
cell.delegateExpand = self
cell.enentDetails = friends[indexPath.item]
return cell
}
}
extension FriendsEventsView:ExpandedCellDelegate{
func viewEventsButtonTapped(indexPath:IndexPath) {
isExpanded[indexPath.row] = !isExpanded[indexPath.row]
print(indexPath)
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.mainCollectionView.reloadItems(at: [indexPath])
}, completion: { success in
print("success")
})
}
}
I used this post for reference to implement
Expandable UICollectionViewCell
This is a very common mistake.
The passed parameter in a target / action selector is always the affected UI element which triggers the action in your case the button.
You cannot pass an arbitrary object for example an indexPath because there is no parameter in the addTarget method to specify that arbitrary object.
You have to declare the selector
#objc func viewEventsButtonTapped(_ sender: UIButton) {
or without a parameter
#objc func viewEventsButtonTapped() {
UIControl provides a third syntax
#objc func viewEventsButtonTapped(_ sender: UIButton, withEvent event: UIEvent?) {
Any other syntax is not supported.

UICollectionViewCell's UiTextField, overlapping in UICollectionViewController

I am trying to design a multistep sign-up menu. For this purpose I am using UICollectionViewController with screen size cells. In these cells, I have a UITextView to ask questions and a UITextField to collect the answers. I also have a Page object for passing in information from uicollectionviewcontroller upon setting.
The problem I'm having now is that after every 3rd page my textField input from 3 pages ago repeats, instead of showing the placeholder. I have noticed yet another problem, the cells seem to be instantiating just 3 times, and not 6 times for how many pages I have. The instantiation order is very odd too. At first it does it once, then upon button click, twice more, then never again.
How can fix this, I am really struggling with this and I have no idea what's going wrong.
This is my code:
import UIKit
class OnboardingPageViewCell: UICollectionViewCell{
override init(frame: CGRect) {
super.init(frame: frame)
print("made a page")
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var oboardingPage = NewOnboardingPage() {
didSet{
reload()
}
}
private var questionTextField: UITextView = {
var q = UITextView()
q.textColor = UIColor.white
q.textAlignment = .left
q.font = UIFont(name: "Avenir-Black", size: 25)
q.isEditable = true
q.isScrollEnabled = false
q.backgroundColor = UIColor.black
q.translatesAutoresizingMaskIntoConstraints = false
print("made aquestion field")
return q
}()
private var answerField : CustomTextField = {
let tf = CustomTextField.nameField
print("made an answer field")
return tf
}()
private func setupView(){
backgroundColor = UIColor.white
addSubview(questionTextField)
questionTextField.topAnchor.constraint(equalTo: topAnchor, constant: 120).isActive = true
questionTextField.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
questionTextField.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.90).isActive = true
questionTextField.heightAnchor.constraint(equalToConstant: 90).isActive = true
addSubview(answerField)
answerField.topAnchor.constraint(equalTo: questionTextField.bottomAnchor, constant: 20).isActive = true
answerField.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
answerField.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.90).isActive = true
answerField.heightAnchor.constraint(equalToConstant: 90).isActive = true
}
private func reload(){
questionTextField.text = oboardingPage.question
answerField.placeholder = oboardingPage.answerField
}
}
class NewOnboardingPage {
var question : String?
var answerField : String?
}
import UIKit
class SignUpController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
private let cellId = "cellId"
private var pages = [NewOnboardingPage]()
override func viewDidLoad() {
super .viewDidLoad()
setupSignUpControllerView()
addPages()
}
private func addPages(){
let namePage = NewOnboardingPage()
namePage.question = "What's your name?"
namePage.answerField = "What's your name?"
pages.append(namePage)
let birthDayPage = NewOnboardingPage()
birthDayPage.question = "When's your birthdate?"
birthDayPage.answerField = "When's your birthdate?"
pages.append(birthDayPage)
let userNamePage = NewOnboardingPage()
userNamePage.question = "Choose a user name."
userNamePage.answerField = "Choose a user name."
pages.append(userNamePage)
let passWordPage = NewOnboardingPage()
passWordPage.question = "Set a password"
passWordPage.answerField = "Set a password"
pages.append(passWordPage)
let emailAuthPage = NewOnboardingPage()
emailAuthPage.question = "What's your email?"
emailAuthPage.answerField = "What's your email?"
pages.append(emailAuthPage)
let phoneNumberPage = NewOnboardingPage()
phoneNumberPage.question = "What's your phone number?"
phoneNumberPage.answerField = "What's your phone number?"
pages.append(phoneNumberPage)
}
private func setupSignUpControllerView(){
collectionView?.backgroundColor = .white
collectionView?.register(OnboardingPageViewCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.isPagingEnabled = true
collectionView?.isScrollEnabled = true
if let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
}
view.addSubview(nextButton)
nextButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 400).isActive = true
nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
nextButton.widthAnchor.constraint(equalToConstant: 250).isActive = true
nextButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
}
private let nextButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = UIColor.RED
button.setTitle("next", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir-Black", size: 25)
button.layer.cornerRadius = 30
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(turnNextPage), for: .touchUpInside)
return button
}()
#objc private func turnNextPage() {
let visibleItems: NSArray = collectionView?.indexPathsForVisibleItems as! NSArray
let currentItem: IndexPath = visibleItems.object(at: 0) as! IndexPath
let nextItem: IndexPath = IndexPath(item: currentItem.item + 1, section: 0)
if nextItem.row < pages.count {
collectionView?.scrollToItem(at: nextItem, at: .left, animated: true)
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! OnboardingPageViewCell
cell.oboardingPage = pages[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
import UIKit
class CustomTextField: UITextField, UITextFieldDelegate {
convenience init() {
self.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
self.allowsEditingTextAttributes = false
self.autocorrectionType = .no
self.tintColor = UIColor.RED
self.translatesAutoresizingMaskIntoConstraints = false
}
override func willMove(toSuperview newSuperview: UIView?) {
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
editingChanged(self)
}
#objc func editingChanged(_ textField: UITextField) {
guard let text = textField.text else { return }
textField.text = String(text.prefix(30))
}
override func selectionRects(for range: UITextRange) -> [Any] {
return []
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(paste(_:)) ||
action == #selector(cut(_:)) ||
action == #selector(copy(_:)) ||
action == #selector(select(_:)) ||
action == #selector(selectAll(_:)){
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! OnboardingPageViewCell
cell.answerField.text = nil
cell.oboardingPage = pages[indexPath.item]
return cell
}
1- textFeild showing same data instead of placeholder bacause of cell dequeuing so you must hook these properties and clear their content in cellForRowAt
2- Instantiation is 3 not 6 aslo cell dequeuing
Solve:
Add two properties to your model NewOnboardingPage name them currentQuestion and currentAnswer and as the user inputs and scroll to next page save them in the modelarray that you should make global to be accessed indside cell and outside set these values to the textfeild and textView as you scroll in cellForRowAt

How to Track UICollectionView index

I want a variable in my code that keeps track of the index of my UICollectionView, but I can't get it to work. After some troubleshooting, I've boiled down the code to the following, which if pasted into an empty viewController should work since no storyboard is involved. The animated gif illustrates the problem. Initially my variable "selectedItem" is equal to the UICollectionView Cell text which reflects the data = [0,1,2,3], but then when I swipe right, it immediately becomes off by 1. Then it stays off by 1 until at the last cell where it matches again. The pattern repeats when going in reverse. Thanks for any help --
import UIKit
class CodeCollView2: UIViewController, UICollectionViewDataSource,UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var data = [0,1,2,3] //["0", "1", "2", "3" ]
let cellId = "cellId2"
var selectedItem = 0
lazy var cView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.isPagingEnabled = true
cv.dataSource = self
cv.delegate = self
return cv
}()
var indexLabel: UILabel = {
let label = UILabel()
label.text = ""
label.font = UIFont.systemFont(ofSize: 30)
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
cView.register(CCell2.self, forCellWithReuseIdentifier: cellId)
view.addSubview(cView)
cView.translatesAutoresizingMaskIntoConstraints = false
cView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
cView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
cView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
cView.heightAnchor.constraint(equalToConstant: 200).isActive = true
view.addSubview(indexLabel)
indexLabel.translatesAutoresizingMaskIntoConstraints = false
indexLabel.bottomAnchor.constraint(equalTo: cView.topAnchor).isActive = true
indexLabel.centerXAnchor.constraint(equalTo: cView.centerXAnchor).isActive = true
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CCell2
selectedItem = indexPath.item
indexLabel.text = "seletedItem = \(selectedItem)"
cell.itemValue = data[selectedItem]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.frame.size
}
}
//============== CVCell ==================
class CCell2: UICollectionViewCell {
var itemValue: Int? {
didSet {
if let val = itemValue {
itemLabel.text = "\(val)"
}
}
}
var itemLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 100)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .lightGray
addSubview(itemLabel)
itemLabel.translatesAutoresizingMaskIntoConstraints = false
itemLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
itemLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As Nikita's answer mentions, cellForItemAt is called when a cell is going to be shown, even if you only see a bit of it and go back to the previous one, so you shouldn't use to decided what cell is at the centre.
scrollViewDidScroll is the right way of tracking which cell you have at the centre, and you can print what index you are on with something like this:
func scrollViewDidScroll(_ scrollView:UIScrollView)
{
let midX:CGFloat = scrollView.bounds.midX
let midY:CGFloat = scrollView.bounds.midY
let point:CGPoint = CGPoint(x:midX, y:midY)
guard
let indexPath:IndexPath = collectionView.indexPathForItem(at:point)
else
{
return
}
let currentPage:Int = indexPath.item
indexLabel.text = "seletedItem = \(currentPage)"
}
Tracking the selected item in the 'cellForItemAt' is not a good idea. I would suggest you to track it in the scrollViewDidScroll delegate method of the UIScrollViewDelegate.
Something like this should work:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentPage = cView.contentOffset.x / self.view.bounds.width
}

ImageView of a custom CollectionViewCell is nil when it should be configured

I have a tableViewCell with a collectionView, collectionView's cells are custom ones, they contains just a imageView.
Here is my test project
Here are DataSource required methods from my CollectionView class:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
let image = UIImage(named: listItems[indexPath.row])
cell.testImageView.image = image
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return listItems.count
}
When I try to set image for cell's imageView I get this error:
fatal error: unexpectedly found nil while unwrapping an Optional value
I have checked image, it isn't nil, but testImageView is, I get this error when I try to set image to collectionViewCell's testImageView.
How can I fix it?
EDIT1
Here is method called from tableViewController to fill collectionView's listItem
func load(listItem: [String]) {
self.listItems = listItem
reloadData()
}
Also if I remove code from collectionView cellForItemAt indexPath with this one all is working fine
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath)
let imageView = UIImageView(image:UIImage(named: listItems[indexPath.row]))
cell.backgroundView = imageView
You have mistaken your two view controllers. Your IB outlet is connected to a cell in a different view controller. I mean you can have multiple views in different controllers connected to a same IBOutlet, but in your case the one that loads first is not connected, so that is why it crashes.
This is the cell your outlet was connected to.
This is that you are trying to load (but did not connect IBOutlet to image view):
Just in case you want to use code instead..
import UIKit
class ImageCell : UICollectionViewCell {
private var imageView: UIImageView!
private var descLabel: UILabel!
public var image: UIImage? {
get {
return self.imageView.image
}
set {
self.imageView.image = newValue
}
}
public var imageDesc: String? {
get {
return self.descLabel.text
}
set {
self.descLabel.text = newValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.initControls()
self.setTheme()
self.doLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initControls()
self.setTheme()
self.doLayout()
}
override func awakeFromNib() {
super.awakeFromNib()
}
func initControls() {
self.imageView = UIImageView()
self.descLabel = UILabel()
}
func setTheme() {
self.imageView.contentMode = .scaleAspectFit
self.descLabel.numberOfLines = 1
self.descLabel.lineBreakMode = .byWordWrapping
self.descLabel.textAlignment = .center
self.descLabel.textColor = UIColor.black
self.contentView.backgroundColor = UIColor.white
}
func doLayout() {
self.contentView.addSubview(self.imageView)
self.contentView.addSubview(self.descLabel)
self.imageView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 5).isActive = true
self.imageView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -5).isActive = true
self.imageView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 0).isActive = true
self.descLabel.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 5).isActive = true
self.descLabel.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -5).isActive = true
self.descLabel.topAnchor.constraint(equalTo: self.imageView.bottomAnchor, constant: 5).isActive = true
self.descLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -5).isActive = true
for view in self.contentView.subviews {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView!
private var dataSource: Array<String>!
override func viewDidLoad() {
super.viewDidLoad()
self.initDataSource()
self.initControls()
self.setTheme()
self.registerClasses()
self.doLayout()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func initDataSource() {
self.dataSource = ["Image1", "Image2", "Image3", "Image4", "Image5", "Image6"]
}
func initControls() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 117, height: 125)
layout.invalidateLayout()
self.collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
self.collectionView.delegate = self
self.collectionView.dataSource = self
}
func setTheme() {
self.collectionView.backgroundColor = UIColor.clear
self.edgesForExtendedLayout = UIRectEdge(rawValue: 0)
self.view.backgroundColor = UIColor.blue
}
func registerClasses() {
self.collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCellIdentifier")
}
func doLayout() {
self.view.addSubview(self.collectionView)
self.collectionView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.collectionView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
self.collectionView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
for view in self.view.subviews {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCellIdentifier", for: indexPath) as! ImageCell
let imageName = self.dataSource[indexPath.row]
cell.image = UIImage(named: imageName)
cell.imageDesc = imageName
return cell
}
}
http://imgur.com/o7O7Plw
maybe the "testImageView" outlet variable is not connected from the interface builder or there is no CollectionViewCell with reuseIdentifier "ImageCell". Check whether cell is nil or not using LLDB po command.

Resources