skip to Main Content

I have a uiCollectionViewCell which loads image from an api. I want to display another image/icon on the cell when a user clicks on it. In my custom cell I have two images one which display the image from the URL and the second one is the one I would like to show if the user has clicked on it. I’m doing this to alert the user that they have selected that cell. Below is my sample code

protocol ModalDelegate {
func changeValue(userChoice: String, rateMovieID: String, rateImageUrl: String, title: String)
}

class GuestRateMovieView: UIViewController, ModalDelegate {
func changeValue(userChoice: String, rateMovieID: String, rateImageUrl: String, title: String) {
    self.userChoice = userChoice
    totalRated = totalRated + 1
    lblRated.text = "(totalRated) rated"
    if totalRated > 0 {
        ratedView.backgroundColor = .ratedGoldColour
    }else{
        ratedView.backgroundColor = .white
    }
    if totalRated >= 5 {
        btnFloatNext.alpha = 1
    }
    if totalRated > 5 {
        userChoiceMovieImage.sd_setImage(with: URL(string: rateImageUrl), placeholderImage: UIImage(named: "ImagePlaceholder"))
        lblUserChoice.text = "Great taste. We love the (title) too."
    }
    var rating = 1
    if userChoice == "Hate it"{
        rating = 1
    }else if userChoice == "Good" {
        rating = 3
    }else{
        rating = 5
    }
    
    let guestRatingValues = GuestUserRate(id: rateMovieID, imageUrl: rateImageUrl, userRate: rating)
    GuestRateMovieView.createUserRating(guestRatingValues) =>", rateImageUrl)
    print("Received on movie", totalRated)
}
func getMovieDetails(){
    activityLoader.displayActivityLoader(image: activityLoader, view: activityLoaderView)
    let urlString = "https://t2fmmm2hfg.execute-api.eu-west-2.amazonaws.com/mobile/media/onboarding-items"
    let url = URL(string: urlString)!
    var request = URLRequest(url: url)
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpMethod = "POST"
    let postParameters: Dictionary<String, Any> = [
        "category": "tv"
    ]
    if let postData = (try? JSONSerialization.data(withJSONObject: postParameters, options: JSONSerialization.WritingOptions.prettyPrinted)){
        request.httpBody = postData
        
        let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard let data = data, error == nil else {
                return
            }
            
            do {
                print("got data")
                let jsonResult = try JSONDecoder().decode([Responder].self, from: data)
                DispatchQueue.main.async {
                    self?.movieObj = jsonResult
                    self?.moviesCollectionView.reloadData()
                    self?.activityLoader.removeActivityLoader(image: self!.activityLoader, view: self!.activityLoaderView)
                }

// jsonResult.forEach { course in print(course.type) }
}catch {
print(error)
}
}

        task.resume()
    }
    
    
}
var movieObj: [Responder] = []

override func viewDidLoad() {
    super.viewDidLoad()
    getMovieDetails()
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return movieObj.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseMoviesCellID, for: indexPath) as! MoviesCollectionCell
    
    cell.movieImage.image = nil
    cell.configure(with: movieObj[indexPath.row].packShot?.thumbnail ?? ""
    
    return cell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let modalVC = RateSingleMovieView()
    modalVC.movieID = movieObj[indexPath.row].id
    modalVC.movieTitle = movieObj[indexPath.row].title
    modalVC.movieImageURL = movieObj[indexPath.row].packShot?.thumbnail ?? ""
    modalVC.delegate = self
    modalVC.modalPresentationStyle = .overCurrentContext
    modalVC.modalTransitionStyle = .crossDissolve
    present(modalVC, animated: true, completion: nil)
}
class MoviesCollectionCell: UICollectionViewCell {

private var movieImages = NSCache<NSString, NSData>()
weak var textLabel: UILabel!
let movieImage: UIImageView = {
    let image = UIImageView()
    image.translatesAutoresizingMaskIntoConstraints = false
    image.clipsToBounds = true
    image.contentMode = .scaleAspectFill
    image.layer.cornerRadius = 10
    return image
}()

let btnRate: UIImageView = {
    let image = UIImageView()
    image.translatesAutoresizingMaskIntoConstraints = false
    image.clipsToBounds = true
    image.contentMode = .scaleAspectFit
    image.alpha = 0
    return image
}()

override init(frame: CGRect) {
    super.init(frame: frame)
    
    contentView.addSubview(movieImage)
    movieImage.addSubview(btnRate)
    
    NSLayoutConstraint.activate([
        movieImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
        movieImage.topAnchor.constraint(equalTo: contentView.topAnchor),
        movieImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
        movieImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        
        btnRate.centerXAnchor.constraint(equalTo: movieImage.centerXAnchor),
        btnRate.centerYAnchor.constraint(equalTo: movieImage.centerYAnchor),
        btnRate.widthAnchor.constraint(equalToConstant: 30),
        btnRate.heightAnchor.constraint(equalToConstant: 30)
    ])
    
    btnRate.tintColor = .white
    btnRate.layer.shadowColor = UIColor.black.cgColor
    btnRate.layer.shadowOffset = CGSize(width: 1.0, height: 2.0)
    btnRate.layer.shadowRadius = 2
    btnRate.layer.shadowOpacity = 0.8
    btnRate.layer.masksToBounds = false
}
override func prepareForReuse() {
    super.prepareForReuse()
    movieImage.image = nil
    btnRate.image = nil
}

func configure(with urlString: String, ratingObj: MovieRating){

    movieImage.sd_setImage(with: URL(string: urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

}

Now in my modal where the user will rate. In my case I’m using swipe gesture for the rating

class RateSingleMovieView: UIViewController, ModalDelegate, ModalSearchDelegate {
func changeValue(userChoice: String, rateMovieID: String, rateImageUrl: String, title: String) {
        self.userChoice = userChoice
        totalRated = totalRated + 1
        
    }
var delegate: ModalDelegate?
override func viewDidLoad() {
    super.viewDidLoad()
    
    redBottomView.addGestureRecognizer(createSwipeGestureRecognizer(for: .up))redBottomView.addGestureRecognizer(createSwipeGestureRecognizer(for: .left))redBottomView.addGestureRecognizer(createSwipeGestureRecognizer(for: .right))
}
@objc private func didSwipe(_ sender: UISwipeGestureRecognizer) {
    switch sender.direction {
        case .up:
        showUserRatingSelection(userChoice: "Good")
        case .left:
        showUserRatingSelection(userChoice: "Hate it")
        case .right:
        showUserRatingSelection(userChoice: "Love it")
        default:
            break
        }
}
@objc private func removeModal(){
    dismiss(animated: true, completion: nil)
}

private func showUserRatingSelection(userChoice: String){
    self.hateItView.alpha = 1
    if userChoice == "Hate it"{
        userChoiceEmoji.image = UIImage(named: "HateIt")
        lblRate.text = "Hate it"
    }else if userChoice == "Good" {
        userChoiceEmoji.image = UIImage(named: "goodRate")
        lblRate.text = "Good"
    }else{
        userChoiceEmoji.image = UIImage(named: "LoveIt")
        lblRate.text = "Love it"
    }
    userChoiceEmoji.alpha = 1
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        print("hello ", userChoice)
        self.delegate?.changeValue(userChoice: userChoice, rateMovieID: self.movieID!, rateImageUrl: self.movieImageURL!, title: self.movieTitle!)
        
        self.removeModal()
    }
    
}
}

I am able to use the delegate here to send info back to GuestRateMovieView Controller and update a label there. Now my only problem is displaying the icon on the selected cell with the user choice.

2

Answers


  1. in your MoviesCollectionCell file put function like this

    func loadImageAfterClickingCell() { 
        // TODO: check this cell is already clicked and already download the image like if yourImageView == nil or not nil so you can add guard like guard yourImageView.image == nil else { return } similar to this
        guard let preparedUrl = URL(string: urlString) else { return } 
        yourImageView.alpha = 1
        yourImageView.sd_setImage(with: preparedUrl, placeholderImage: UIImage(named: "ImagePlaceholder"))
    }
    

    And after that in didSelectItemAt

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseMoviesCellID, for: indexPath) as! MoviesCollectionCell
        cell.loadImageAfterClickingCell()
    }
    

    A simple method injection like this must save you as you want.

    Login or Signup to reply.
  2. First note… setup your constraints in initAbsolutely NOT in layoutSubviews().

    Edit — forget everything else previously here, because it had nothing to do with what you’re actually trying to accomplish.


    New Answer

    To clarify your goal:

    • display a collection view of objects – in this case, movies
    • when the user selects a cell, show a "Rate This Movie" view
    • when the user selects a Rating (hate, good, love), save that rating and update the cell with a "Rating Image"

    So, the first thing you need is a data structure that includes a "rating" value. Let’s use an enum for the rating itself:

    enum MovieRating: Int {
        case none, hate, good, love
    }
    

    Then we might have a "Movie Object" like this:

    struct MovieObject {
        var title: String = ""
        var urlString: String = ""
        var rating: MovieRating = .none
        // maybe some other properties
    }
    

    For our data, we’ll have an Array of MovieObject. When we configure each cell (in cellForItemAt), we need to set the Movie Image and the Rating Image.

    So, your cell class may have this:

    func configure(with movieObj: MovieObject) {
    
        movieImage.sd_setImage(with: URL(string: movieObj.urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
    
        switch movieObj.rating {
        case .hate:
            if let img = UIImage(systemName: "hand.thumbsdown") {
                btnRate.image = img
            }
        case .good:
            if let img = UIImage(systemName: "face.smiling") {
                btnRate.image = img
            }
        case .love:
            if let img = UIImage(systemName: "hand.thumbsup") {
                btnRate.image = img
            }
        default:
            btnRate.image = nil
        }
    }
    

    and your cellForItemAt would look like this:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath) as! MoviesCollectionCell
    
        c.configure(with: moviesArray[indexPath.row])
        
        return c
    }
    

    When the user selects a cell, we can present a "Rate This Movie" view controller – which will have buttons for Hate / Good / Love.

    If the user taps one of those buttons, we can use a closure to update the data and reload that cell:

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let vc = RateTheMovieVC()
        vc.movieObj = moviesArray[indexPath.item]
        
        vc.callback = { [weak self] rating in
            guard let self = self else { return }
    
            // update the data
            self.moviesArray[indexPath.item].rating = rating
    
            // reload the cell
            self.collectionView.reloadItems(at: [indexPath])
            
            // dismiss the RateTheMovie view controller
            self.dismiss(animated: true)
        }
        
        // present the RateTheMovie view controller
        present(vc, animated: true)
    }
    

    Here’s a complete example… I don’t have your data (movie names, images, etc), so we’ll use an array of "Movie Titles" from A to Z, and the cells will look like this:

    enter image description here

    and so on.

    enum and struct

    enum MovieRating: Int {
        case none, hate, good, love
    }
    
    struct MovieObject {
        var title: String = ""
        var urlString: String = ""
        var rating: MovieRating = .none
    }
    

    collection view cell

    class MoviesCollectionCell: UICollectionViewCell {
        let movieImage: UIImageView = {
            let image = UIImageView()
            image.translatesAutoresizingMaskIntoConstraints = false
            image.clipsToBounds = true
            image.contentMode = .scaleAspectFill
            image.layer.cornerRadius = 10
            image.backgroundColor = .blue
            return image
        }()
        
        let btnRate: UIImageView = {
            let image = UIImageView()
            image.translatesAutoresizingMaskIntoConstraints = false
            image.clipsToBounds = true
            image.contentMode = .scaleAspectFit
            return image
        }()
        
        // we don't have Movie Images for this example, so
        //  we'll use some labels for the Movie Title
        var labels: [UILabel] = []
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            
            contentView.addSubview(movieImage)
            
            for _ in 0..<4 {
                let v = UILabel()
                v.translatesAutoresizingMaskIntoConstraints = false
                v.textAlignment = .center
                v.textColor = .cyan
                if let f = UIFont(name: "TimesNewRomanPS-BoldMT", size: 60) {
                    v.font = f
                }
                contentView.addSubview(v)
                labels.append(v)
            }
            
            // stack views for the labels
            let stTop = UIStackView()
            stTop.axis = .horizontal
            stTop.distribution = .fillEqually
            stTop.addArrangedSubview(labels[0])
            stTop.addArrangedSubview(labels[1])
    
            let stBot = UIStackView()
            stBot.axis = .horizontal
            stBot.distribution = .fillEqually
            stBot.addArrangedSubview(labels[2])
            stBot.addArrangedSubview(labels[3])
    
            let st = UIStackView()
            st.axis = .vertical
            st.distribution = .fillEqually
            st.addArrangedSubview(stTop)
            st.addArrangedSubview(stBot)
            st.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(st)
            
            contentView.addSubview(btnRate)
            
            // setup constriaints here
            NSLayoutConstraint.activate([
                movieImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
                movieImage.topAnchor.constraint(equalTo: contentView.topAnchor),
                movieImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
                movieImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
    
                st.topAnchor.constraint(equalTo: movieImage.topAnchor),
                st.leadingAnchor.constraint(equalTo: movieImage.leadingAnchor),
                st.trailingAnchor.constraint(equalTo: movieImage.trailingAnchor),
                st.bottomAnchor.constraint(equalTo: movieImage.bottomAnchor),
    
                btnRate.centerXAnchor.constraint(equalTo: movieImage.centerXAnchor),
                btnRate.centerYAnchor.constraint(equalTo: movieImage.centerYAnchor),
                btnRate.widthAnchor.constraint(equalToConstant: 40),
                btnRate.heightAnchor.constraint(equalToConstant: 40)
            ])
            
            btnRate.tintColor = .white
            btnRate.layer.shadowColor = UIColor.black.cgColor
            btnRate.layer.shadowOffset = CGSize(width: 1.0, height: 2.0)
            btnRate.layer.shadowRadius = 2
            btnRate.layer.shadowOpacity = 0.8
            btnRate.layer.masksToBounds = false
        }
        
        override func prepareForReuse() {
            super.prepareForReuse()
            movieImage.image = nil
        }
        
        func configure(with movieObj: MovieObject) {
    
            // I don't have your cell images, or the "sd_setImage" function
            //  un-comment the next line to set your images
            // movieImage.sd_setImage(with: URL(string: movieObj.urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
    
            labels.forEach { v in
                v.text = movieObj.title
            }
    
            switch movieObj.rating {
            case .hate:
                if let img = UIImage(systemName: "hand.thumbsdown") {
                    btnRate.image = img
                }
            case .good:
                if let img = UIImage(systemName: "face.smiling") {
                    btnRate.image = img
                }
            case .love:
                if let img = UIImage(systemName: "hand.thumbsup") {
                    btnRate.image = img
                }
            default:
                btnRate.image = nil
            }
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
    }
    

    example view controller

    class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
        
        var collectionView: UICollectionView!
        
        var moviesArray: [MovieObject] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let fl = UICollectionViewFlowLayout()
            fl.itemSize = CGSize(width: 100.0, height: 200.0)
            fl.scrollDirection = .vertical
            fl.minimumLineSpacing = 8
            fl.minimumInteritemSpacing = 8
            
            collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
            
            collectionView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(collectionView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            ])
            
            collectionView.register(MoviesCollectionCell.self, forCellWithReuseIdentifier: "c")
            collectionView.dataSource = self
            collectionView.delegate = self
            
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            // let's change the collection view cell size to fit
            //  two "columns"
            if let fl = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
                fl.itemSize = CGSize(width: (collectionView.frame.width - fl.minimumInteritemSpacing) * 0.5, height: 200.0)
            }
            
            simulateGettingData()
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return moviesArray.count
        }
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath) as! MoviesCollectionCell
    
            c.configure(with: moviesArray[indexPath.row])
            
            return c
        }
        
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            let vc = RateTheMovieVC()
            vc.movieObj = moviesArray[indexPath.item]
            
            vc.callback = { [weak self] rating in
                guard let self = self else { return }
    
                // update the data
                self.moviesArray[indexPath.item].rating = rating
    
                // reload the cell
                self.collectionView.reloadItems(at: [indexPath])
                
                // dismiss the RateTheMovie view controller
                self.dismiss(animated: true)
            }
            
            // present the RateTheMovie view controller
            present(vc, animated: true)
        }
        
        func simulateGettingData() {
            
            // let's just create an array of MovieObject
            //  where each Title will be a letter from A to Z
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ".forEach { c in
                let m = MovieObject(title: String(c), urlString: "", rating: .none)
                moviesArray.append(m)
            }
            
            collectionView.reloadData()
        }
    }
    

    example "Rate The Movie" view controller

    class RateTheMovieVC: UIViewController {
        
        // this will be used to tell the presenting controller
        //  that a rating button was selected
        var callback: ((MovieRating) -> ())?
        
        var movieObj: MovieObject!
        
        let movieImage: UIImageView = {
            let image = UIImageView()
            image.translatesAutoresizingMaskIntoConstraints = false
            image.clipsToBounds = true
            image.contentMode = .scaleAspectFill
            image.layer.cornerRadius = 10
            image.backgroundColor = .systemBlue
            return image
        }()
        
        // we don't have Movie Images for this example, so
        //  we'll use a label for the "Movie Title"
        let titleLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.numberOfLines = 0
            v.textAlignment = .center
            v.textColor = .yellow
            v.font = .systemFont(ofSize: 240, weight: .bold)
            return v
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .lightGray
            
            // let's add 3 "rate" buttons near the bottom
            let btnHate = UIButton()
            let btnGood = UIButton()
            let btnLove = UIButton()
            
            let btns: [UIButton] = [btnHate, btnGood, btnLove]
            let names: [String] = ["hand.thumbsdown", "face.smiling", "hand.thumbsup"]
            for (b, s) in zip(btns, names) {
                b.backgroundColor = .systemRed
                b.layer.cornerRadius = 8
                b.layer.masksToBounds = true
                if let img = UIImage(systemName: s, withConfiguration: UIImage.SymbolConfiguration(pointSize: 32)) {
                    b.setImage(img, for: [])
                }
                b.tintColor = .white
                b.heightAnchor.constraint(equalToConstant: 60.0).isActive = true
            }
            
            let btnStack = UIStackView()
            btnStack.spacing = 20
            btnStack.distribution = .fillEqually
            btns.forEach { b in
                btnStack.addArrangedSubview(b)
            }
            
            view.addSubview(movieImage)
            view.addSubview(titleLabel)
            
            btnStack.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(btnStack)
            
            // setup constriaints here
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                btnStack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
                
                titleLabel.topAnchor.constraint(equalTo: movieImage.topAnchor, constant: 8.0),
                titleLabel.leadingAnchor.constraint(equalTo: movieImage.leadingAnchor, constant: 12.0),
                titleLabel.trailingAnchor.constraint(equalTo: movieImage.trailingAnchor, constant: -12.0),
                titleLabel.bottomAnchor.constraint(equalTo: movieImage.bottomAnchor, constant: -8.0),
                
                movieImage.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                movieImage.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                movieImage.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
                movieImage.bottomAnchor.constraint(equalTo: btnStack.topAnchor, constant: -20.0),
                
            ])
            
            // here we would set the movie image
            
            // set the title label text, since we don't have images right now
            titleLabel.text = movieObj.title
            
            btnHate.addTarget(self, action: #selector(hateTap(_:)), for: .touchUpInside)
            btnGood.addTarget(self, action: #selector(goodTap(_:)), for: .touchUpInside)
            btnLove.addTarget(self, action: #selector(loveTap(_:)), for: .touchUpInside)
    
        }
        
        @objc func hateTap(_ sender: UIButton) {
            callback?(.hate)
        }
        @objc func goodTap(_ sender: UIButton) {
            callback?(.good)
        }
        @objc func loveTap(_ sender: UIButton) {
            callback?(.love)
        }
        
    }
    

    It will look like this on launch:

    enter image description here

    Then we select the first cell and we see this:

    enter image description here

    We select the "Thumbs Up" button, and we see this:

    enter image description here

    Then scroll down and select-and-rate a few other cells:

    enter image description here

    You mention in a comment a "cache DB" … assuming that will be persistent data, it’s up to you to store the user-selected Rating.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search