Search code examples
iosswiftcellscollectionview

didSet property value only appears in print()


INITIAL GOAL:

Have a view with a list of cells positioned vertically displaying some information. As soon as the user clicks on a cell to show a new view with more information.

THE ROAD SO FAR (curry on my wayward son!):

  • I created 2 view controllers: ViewController (subclassing UICollectionViewController, UICollectionViewDelegateFlowLayout) and DetailViewController (subclassing UIViewController).
  • I created a Cell that the ViewController uses to generate the collection view and a DetailView that the DetailViewController uses
  • I created a struct named Detail as a custom data type which provides storage of data using properties (ex. name, surname, address, etc.)

The struct:

struct Detail: Decodable {
    let name: String?
    let surname: String?
    let address: String?
    let description: String?
}

I use the following data for testing (after the testing is done I will get this data from an API call). I placed it inside ViewController:

let details: [Detail] = [Detail(name: "Chris", surname: "Doe", address: "Neverland 31", description: "This is a description about Chris Doe"), Detail(name: "Tony", surname: "Cross", address: "Galaxy Road 1", description: "This is a description about Tony Cross")]

To create the cells using the information above and the method:

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell

And also:

let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! Cell

As the method requires us to return a UICollectionViewCell, I first send the associated information to Cell by doing the following:

cell.details = details[indexPath.item]
return cell

Inside the Cell I created the following property using didSet to help me retrieve the information:

var details: Detail? {
    didSet {
        guard let details = details else { return }
        guard let name = details.name else { return }
        ....
        ....
     }    

As you can understand using the information coming from ViewController I dynamically constructed each cell.

All were good at this point.

Then I tried to show a detailed view when clicking on a cell. To do this I followed the same practice inside the method:

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

    let detailView = DetailView()
    detailView.details = details[indexPath.item]

    let detailViewController = DetailViewController()
    detailViewController.modalTransitionStyle = .coverVertical
    self.present(detailViewController, animated: true, completion: nil)
}

Again, in the DetailView I use the same approach to get the data associated with the selected item. This way I can have access to the data of the cell the user selects, as shown below:

 import UIKit

 class DetailView: UIView {

     var dismissDetailViewAction: (() -> Void)?

     var details: Detail? {
        didSet {

            // get details
            guard let details = details else { return }
            guard let name = details.name else { return }
            guard let surname = details.surname else { return }
            guard let address = details.address else { return }
            guard let description = details.description else { return }

            // print description and it shows in the console but not in the view
            print(description)

            let attributedTextDescription = NSMutableAttributedString(string: description, attributes: [NSAttributedStringKey.font: UIFont.FontBook.AvertaRegular.of(size: 20), NSAttributedStringKey.foregroundColor: UIColor.white])
            briefDescription.attributedText = attributedTextDescription
            briefDescription.textAlignment = .center
            briefDescription.textColor = .white
            briefDescription.numberOfLines = 0
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupView()
    }

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

    fileprivate func setupView() {
        setupDescriptionText()
        setupCloseButton()
    }

    let briefDescription: UITextView = {
        let text = UITextView()          
        text.textColor = .red
        return text
    }()

    let closeButton: UIButton = {
        let button = UIButton(title: "Close", font: UIFont.FontBook.AvertaRegular.of(size: 18), textColor: .white, cornerRadius: 5)
        button.backgroundColor = .black
        button.addTarget(self, action: #selector(closeDetailView), for: .touchUpInside)
        return button
    }()


    fileprivate func setupDescriptionText() {
        self.addSubview(briefDescription)
        briefDescription.translatesAutoresizingMaskIntoConstraints = false
        briefDescription.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5).isActive = true
        briefDescription.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5).isActive = true
        briefDescription.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
        briefDescription.heightAnchor.constraint(equalToConstant: 300).isActive = true
    }

    fileprivate func setupCloseButton() {
        self.addSubview(closeButton)
        closeButton.translatesAutoresizingMaskIntoConstraints = false

        closeButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
        closeButton.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
        closeButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 40).isActive = true
        closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -40).isActive = true
        closeButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
    }

    @objc func closeDetailView() {
        dismissDetailViewAction?()
    }

}

So, what I actually do is to design the static part of the view outside didSet, and what is dynamic part inside didSet. This works with the cells of collectionView.

I use the DetailViewController to display the DetailView and dismiss itself when the user clicks on the "Close" button.

import UIKit

class DetailViewController: UIViewController {

    // reference DetailView view
    var detailView: DetailView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // setup view elements
        setupView()
    }

    fileprivate func setupView() {
        let mainView = DetailView(frame: self.view.frame)
        self.detailView = mainView
        self.view.addSubview(detailView)

        self.homeDetailView.dismissDetailViewAction = dismissDetailView

        // pin view
        self.detailView.translatesAutoresizingMaskIntoConstraints = false
        self.detailView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        self.detailView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        self.detailView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        self.detailView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
    }

    fileprivate func dismissDetailView() {
        // dismiss current (DetailViewController) controller
        self.dismiss(animated: true, completion: nil)
    }
}

The reason I did this is that I like to keep my ViewControllers as clean as possible (Massive View Controller, not my thing).

THE PROBLEM

The whole thing is built without any problem, but when I click on a cell to go to the DetailView no information is displayed.

THE WEIRD PART

Inside the DetailView --> didSet, when I use print(name), it works just fine (you see the correct name inside console). But when I try to use that value inside the view it will not be displayed.

And I know that my DetailView is just fine since if I use hardcoded values in it, it works (you see the correct result).

Any advise why this is not working properly?

PS: I am building the whole thing programmatically. No storyboards involved.

Thanks in advance and sorry for the lost post.


Solution

  • As was mentioned, your detailView is not referenced inside detailViewController. Instead, you create another instance of DetailView inside DetailViewController but this one has no Detail in it.

    The console message was called from inside your detailView, but inside detailViewController is another instance that did not call this message, because its Detail is set to nil by default.

    To be short, to fix that you should simply do the following changes:

    import UIKit
    
    class DetailViewController: UIViewController {
    
    var detail: Detail!
    
    private lazy var detailView: DetailView = {
        let mainView = DetailView(frame: self.view.frame)
        mainView.details = detail
        return mainView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        setupView()
    }
    
    fileprivate func setupView() {
        self.view.addSubview(detailView)
    
        self.homeDetailView.dismissDetailViewAction = dismissDetailView
    
        // pin view
        self.detailView.translatesAutoresizingMaskIntoConstraints = false
        self.detailView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        self.detailView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        self.detailView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        self.detailView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
    }
    
    fileprivate func dismissDetailView() {
        // dismiss current (DetailViewController) controller
        self.dismiss(animated: true, completion: nil)
    }
    
    }
    

    And inside your collectionView(...) func:

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let detailViewController = DetailViewController()
        detailViewController.detail = details[indexPath.item]
        detailViewController.modalTransitionStyle = .coverVertical
        self.present(detailViewController, animated: true, completion: nil)
    }