1

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

Shivam Tripathi
  • 1,405
  • 3
  • 19
  • 37
Ron Baker
  • 381
  • 6
  • 16
  • wait why did i get a downvote – Ron Baker Feb 12 '18 at 05:07
  • Can you put up a GitHub repo with a minimal reproducible example of your problem? – Vivek Molkar Feb 12 '18 at 05:39
  • that may be hard my code base is kinda big @VivekMolkar – Ron Baker Feb 12 '18 at 05:41
  • i found a video where it seems this may be easier if I switch to a table view are there any git repo that does this expanding and collapsing for you – Ron Baker Feb 12 '18 at 06:00
  • I really feel like I would find the issue with this if I'm running the code in xcode right now... but this is lot of code to analyse. Here's what I would do: Create a Exception breakpoint to see the last call stack and make sure the instance of the UIButton that is crashing the code is still alive. – Aju Antony Feb 12 '18 at 06:49
  • Could you track the exact line causing the crash? It seems that at some point, your code think the parameter is a `NSString` (for a `someUILabel.text? For `NSAttributedString`?)` object, but I think that it is in fact a `UIButton`. I didn't see a `UIButton` creation/call in your code though. – Larme Feb 12 '18 at 08:00

1 Answers1

0

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.

vadian
  • 274,689
  • 30
  • 353
  • 361