iosswiftviewcontrollerchildviewcontrollerparentviewcontroller

View not displaying for childViewController of another ChildViewController


I'm having an issue with a multi-level hierarchy of childViewControllers in iOS/Swift. There are three layers to the current setup from lowest-to-highest:

  1. InfoViewController
  2. SelectionViewController
  3. MainViewController

The InfoViewController has a view that is loaded from XIB.

SelectionViewController contains a UIStackView with an InfoViewController and a UIButton.

MainViewController is the top-level VC which is usually embedded in a UINavigationController.

The Problem

When I add the InfoViewController and it's view directly to the MainViewController everything works great.

func setupInfoViewControllerDirectlyOnMainVC () {
    addChildViewController(infoViewController)
    infoViewController.view.embedInside(otherView: infoContainerView)
    infoViewController.didMove(toParentViewController: self)
}

However, if I add the SelectionViewController to the MainViewController using the same method, the embedded InfoViewController doesn't update it's UI - it always looks like the untouched XIB file that it is controlling. When it is NOT embedded in this SelectionViewController is behaves as expected.

As shown below, the UI is visible, but any changes made to it via asking it's ViewController do not show up - it looks exactly like the XIB file it was created from.

example of empty UI

Below is the class setup starting with the BasicInfoView followed by the three viewControllers listed above.

BasicInfoView

class PLBasicInfoView: PLDesignableViewFromXib {

    //
    // MARK: Outlets
    //
    //

    @IBOutlet weak var photoView: PFRoundImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!


    var imageFile:PFFile? {
        didSet {
            photoView.file = imageFile
            photoView.loadInBackground()
        }
    }

    //
    // MARK: Initialization
    //
    //

    override var nameOfXib: String {
        return "PLBasicInfoView"
    }

    override var intrinsicContentSize: CGSize {
        return CGSize(width: super.intrinsicContentSize.width, height: 56)
    }

}

BasicInfoViewController

class PLBasicInfoViewController: UIViewController {

    /**
     The BasicInfoView which will be managed by this controller.
     */
    var basicInfoView = PLBasicInfoView()

    /**
     This is the master stack view which contains all subviews.
     */
    var stackView = UIStackView()


    /**
     PFFile representing the image to be displayed in the imageView. Setting a valid imageFile object automatically laods the image from the server. If set to nil, the defaultImage is displayed instead.
     */
    var imageFile: PFFile? {
        didSet {
            if imageFile != nil {
                basicInfoView.imageFile = imageFile
            } else {
                basicInfoView.photoView.image = defaultImage
            }
        }
    }

    /**
     Default UIImage to be displayed in the imageView if there is no imageFile assigned.
     */
    var defaultImage: UIImage! {
        return #imageLiteral(resourceName: "ios7-camera-outline")
    }


    /**
     Main text of the infoView
     */
    var titleText:String? {
        didSet {
            basicInfoView.titleLabel.isHidden = (titleText == nil)
            basicInfoView.titleLabel.text = titleText
        }
    }

    /**
     Secondary text of the infoView. Displays under titleText.
     */
    var subtitleText:String? {
        didSet {
            basicInfoView.subtitleLabel.isHidden = (subtitleText == nil)
            basicInfoView.subtitleLabel.text = subtitleText
        }
    }

    /**
     Embed our stackView into main view. The custom embedInsider(otherView:UIView) method (UIView extension) will take care of the subview additional as well as all layout constraints.
     */
    func setupStackView () {
        stackView.embedInside(otherView: view)
        stackView.axis = .vertical
        stackView.addArrangedSubview(basicInfoView)
    }

    override
    func viewDidLoad() {
        super.viewDidLoad()
        setupStackView()
    }


}

SelectionViewController

class PLSelectableInfoViewController: UIViewController {


    /**
     If true, the info view will be shown and the selection button will be hidden.
     */
    var isAssigned = false {
        didSet {
            selectionButton.isHidden = isAssigned
            infoView.isHidden = !isAssigned
        }
    }


    /**
     The View controller dispaying the object in question.
     */
    var infoViewController: PLBasicInfoViewController! {
        return PLBasicInfoViewController()
    }

    private
    var infoView: UIView!

    /**
     Button on bottom of stack. Intended to allow user to assign a new value to the contact property.
     */
    var selectionButton = PLButton()

    /**
     Stack view containing all subviews.
     */
    var stackView = UIStackView()


    //
    // MARK: UIViewController Overrides
    //
    //

    override
    func viewDidLoad() {
        super.viewDidLoad()
        setupStackView()
        addInfoView()
    }


    private
    func setupStackView () {
        stackView.embedInside(otherView: view)
        stackView.axis = .vertical
    }

    private
    func addInfoView () {
        addChildViewController(infoViewController)
        infoView = infoViewController.view
        stackView.addArrangedSubview(infoView)
        infoViewController.didMove(toParentViewController: self)
    }

}

Some non-relevant code has been removed

Notes

Please note that in practice, both BasicInfoViewController and SelectionViewController are subclassed. For example, I have a ContactInfoViewController which can be passed a Contact object and display full name, company name and photo (as explained above, this works fine). There is also a subclass of SelectionViewController to complement this: ContactSelectionViewController. ContactSelectionViewController also has a Contact object property which can be assigned and is then passed to the embedded ContactInfoViewController - this is the point at which the data is not displayed. I have included these subclasses below for additional reference.

ContactInfoViewController

Again, this works perfectly when placed directly into the MainViewController.

class PLContactInfoViewController: PLBasicInfoViewController {

    /**
     Contact object managed by this controller.
     */
    var contact: PLContact? {
        didSet {
            if contact == nil {
                titleText = "Not Set"
                subtitleText = nil
                return
            }
            contact?.fetchIfNeededInBackground(block: { (object, error) in
                if let _ = object as? PLContact {
                    self.updateWithContact()
                }
            })
        }
    }

    override
    var defaultImage: UIImage! {
        return #imageLiteral(resourceName: "ios7-contact-outline")
    }


    private
    func updateWithContact () {
        if let c = contact {
            titleText = c.fullName
            imageFile = c.photo
            c.company?.fetchIfNeededInBackground(block: { (object, error) in
                if let comp = object as? PLCompany {
                    self.subtitleText = comp.name
                } else {
                    self.subtitleText = nil
                }
            })
        }
    }

}

ContactSelectionViewController

This VC functions properly, but the embedded ContactInfoViewController does not display data. For some reason, the view from the ContactInfoViewController is not being updated with data when it is embedded inside this controller.

class PLContactAssignmentViewController: PLSelectableInfoViewController {


    /**
     The contact currently selected by this contorller
     */
    var selectedContact: PLContact? {
        didSet {
            isAssigned = !(selectedContact == nil)
            contactInfoViewController.contact = selectedContact
        }
    }


    override
    var infoViewController: PLBasicInfoViewController! {
        return PLContactInfoViewController()
    }


    private
    var contactInfoViewController: PLContactInfoViewController {
        return infoViewController as! PLContactInfoViewController
    }

}

Solution

  • Try

        var _infoViewController: PLBasicInfoViewController?
        var infoViewController: PLBasicInfoViewController! {
            if let vc = _infoViewController {
                return vc
            }
            _infoViewController = PLBasicInfoViewController()
            return _infoViewController!
        }
    

    or

    lazy var infoViewController: PLBasicInfoViewController = {
            return PLBasicInfoViewController()
        }()
    

    It might be because you are initiating PLBasicInfoViewController every time when you try to access.