iosswiftconstraintssafearealayoutguide

My UIView doesn't show up as I try to set constraints relative to safe area programmatically


I have a StackContainerView inside my main view controller called TodayPicksViewController. I am trying to programmatically set the StackContainerView to fill up the whole view controller side to side, with around 50 from top and bottom (just like a Tinder card).

However, as I try to implement constraints relative to safe area as follows(as other answers on StackOverflow suggest), turned out the StackContainerView doesn't show up at all. I don't know where the problem is.

Please advice.

Code of my main view controller, TodayPicksViewController:

class TodayPicksViewController: UIViewController  {
        
    //MARK: - Properties
    var viewModelData = [CardsDataModel(bgColor: UIColor(red:0.96, green:0.81, blue:0.46, alpha:1.0), text: "Hamburger", image: "hamburger"),
                         CardsDataModel(bgColor: UIColor(red:0.29, green:0.64, blue:0.96, alpha:1.0), text: "Puppy", image: "puppy"),
                         CardsDataModel(bgColor: UIColor(red:0.29, green:0.63, blue:0.49, alpha:1.0), text: "Poop", image: "poop"),
                         CardsDataModel(bgColor: UIColor(red:0.69, green:0.52, blue:0.38, alpha:1.0), text: "Panda", image: "panda"),
                         CardsDataModel(bgColor: UIColor(red:0.90, green:0.99, blue:0.97, alpha:1.0), text: "Subway", image: "subway"),
                         CardsDataModel(bgColor: UIColor(red:0.83, green:0.82, blue:0.69, alpha:1.0), text: "Robot", image: "robot")]
    
    var stackContainer : StackContainerView!
    
    private let spinner = JGProgressHUD(style: .dark)

    private var users = [[String: String]]()

    private var results = [SearchResult]()
    
    private var hasFetched = false

    var divisor: CGFloat!
    
    private let noResultsLabel: UILabel = {
        let label = UILabel()
        label.isHidden = true
        label.text = "No Results"
        label.textAlignment = .center
        label.textColor = .green
        label.font = .systemFont(ofSize: 21, weight: .medium)
        return label
    }()

    override func loadView() {
        view = UIView()
        stackContainer = StackContainerView()
        view.addSubview(stackContainer)
        stackContainer.translatesAutoresizingMaskIntoConstraints = false
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(noResultsLabel)
        configureStackContainer()
        stackContainer.dataSource = self

    }
    
    @IBAction func panMatch(_ sender: UIPanGestureRecognizer) {
        let match = sender.view!
        let point = sender.translation(in: view)
        let xFromCenter = match.center.x - view.center.x
        print(xFromCenter)
        
        match.center = CGPoint(x: view.center.x + point.x, y: view.center.y + point.y)
        
        match.transform = CGAffineTransform(rotationAngle: xFromCenter/divisor)
       
        
        if sender.state == UIGestureRecognizer.State.ended {
            
            if match.center.x < 75 {
                // Move off to the left side
                UIView.animate(withDuration: 0.3, animations: {
                    match.center = CGPoint(x: match.center.x - 200, y: match.center.y + 75)
                    match.alpha = 0
                })
                return
            } else if match.center.x > (view.frame.width - 75) {
                // Move off to the right side
                UIView.animate(withDuration: 0.3, animations: {
                    match.center = CGPoint(x: match.center.x + 200, y: match.center.y + 75)
                    match.alpha = 0
                })
                return
            }
//            resetCard()
            
        }
    }
    
    private var loginObserver: NSObjectProtocol?
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        validateAuth()
    }
    
    private func validateAuth() {
        if FirebaseAuth.Auth.auth().currentUser == nil {
            let vc = SignInViewController()
            let nav = UINavigationController(rootViewController: vc)
            nav.modalPresentationStyle = .fullScreen
            present(nav, animated: false)
        }
    }
    
    @objc private func pageControlDidChange(_ sender: UIPageControl) {
        let current = sender.currentPage
        //        scrollView.setContentOffset(CGPoint(x: CGFloat(current) * view.frame.size.width,
        //                                            y: 70), animated: true)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    }
    
    //MARK: - Configurations
    func configureStackContainer() {
        stackContainer.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        stackContainer.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -60).isActive = true
        
        //        stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
        //        stackContainer.heightAnchor.constraint(equalToConstant: 400).isActive = true
        stackContainer.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
        stackContainer.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
        stackContainer.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
        stackContainer.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
    }


    func updateUI() {
        if results.isEmpty {
            noResultsLabel.isHidden = false
        }
        else {
            noResultsLabel.isHidden = true
        }
    }
    
    func calcAge(birthday: Date) -> Int {
        let dateFormater = DateFormatter()
        dateFormater.dateFormat = "MM/dd/yyyy"
//        let birthdayDate = dateFormater.date(from: birthday)
        let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
        let now = Date()
        let calcAge = calendar.components(.year, from: birthday, to: now, options: [])
        let age = calcAge.year
        return age!
    }

extension TodayPicksViewController : SwipeCardsDataSource {

    func numberOfCardsToShow() -> Int {
        return viewModelData.count
    }
    
    func card(at index: Int) -> SwipeCardView {
        let card = SwipeCardView()
        card.dataSource = viewModelData[index]
        return card
    }
    
    func emptyView() -> UIView? {
        return nil
    }
    

}

Probably doesn't matter, but here is my code for the StackContainerView:

class StackContainerView: UIView, SwipeCardsDelegate {

    //MARK: - Properties
    var numberOfCardsToShow: Int = 0
    var cardsToBeVisible: Int = 3
    var cardViews : [SwipeCardView] = []
    var remainingcards: Int = 0
    
    let horizontalInset: CGFloat = 10.0
    let verticalInset: CGFloat = 10.0
    
    var visibleCards: [SwipeCardView] {
        return subviews as? [SwipeCardView] ?? []
    }
    var dataSource: SwipeCardsDataSource? {
        didSet {
            reloadData()
        }
    }
    //MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: .zero)
        backgroundColor = .clear
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    
    func reloadData() {
        removeAllCardViews()
        guard let datasource = dataSource else { return }
        setNeedsLayout()
        layoutIfNeeded()
        numberOfCardsToShow = datasource.numberOfCardsToShow()
        remainingcards = numberOfCardsToShow
        
        for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) {
            addCardView(cardView: datasource.card(at: i), atIndex: i )
            
        }
    }

    //MARK: - Configurations

    private func addCardView(cardView: SwipeCardView, atIndex index: Int) {
        cardView.delegate = self
        addCardFrame(index: index, cardView: cardView)
        cardViews.append(cardView)
        insertSubview(cardView, at: 0)
        remainingcards -= 1
    }
    
    func addCardFrame(index: Int, cardView: SwipeCardView) {
        var cardViewFrame = bounds
        let horizontalInset = (CGFloat(index) * self.horizontalInset)
        let verticalInset = CGFloat(index) * self.verticalInset
        
        cardViewFrame.size.width -= 2 * horizontalInset
        cardViewFrame.origin.x += horizontalInset
        cardViewFrame.origin.y += verticalInset
        
        cardView.frame = cardViewFrame
    }
    
    private func removeAllCardViews() {
        for cardView in visibleCards {
            cardView.removeFromSuperview()
        }
        cardViews = []
    }
    
    func swipeDidEnd(on view: SwipeCardView) {
        guard let datasource = dataSource else { return }
        view.removeFromSuperview()

        if remainingcards > 0 {
            let newIndex = datasource.numberOfCardsToShow() - remainingcards
            addCardView(cardView: datasource.card(at: newIndex), atIndex: 2)
            for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
                UIView.animate(withDuration: 0.2, animations: {
                cardView.center = self.center
                  self.addCardFrame(index: cardIndex, cardView: cardView)
                    self.layoutIfNeeded()
                })
            }

        }else {
            for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
                UIView.animate(withDuration: 0.2, animations: {
                    cardView.center = self.center
                    self.addCardFrame(index: cardIndex, cardView: cardView)
                    self.layoutIfNeeded()
                })
            }
        }
    }
    

}


Solution

  • According to the apple developer doc for loadView(), they said "The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property." This might be the cause of the problem. I would recommend you to perform the view set up operations in viewDidLoad or other proper lifecycle methods. Based on my understanding, this line view = UIView() isn't necessary. In your configureStackContainer() func, you set the centerX and centerY anchor and then set the top, leading, trailing, bottom anchor again. This may also raise the constraint conflicts. I think you don't need to specify centerX and centerY anchor if you want to constraint with top, leading, trailing and bottom and vice versa. I hope this will be helpful.