3

I am trying to create a statistics page for my app that will have various charts that are created dynamically depending on the type of data the user has. To do this, I am stacking multiple ViewControllers according to this tutorial: https://swiftwithmajid.com/2019/02/27/building-complex-screens-with-child-viewcontrollers/

I am running into an issue where the ViewController's View is added to the main StackView as an arrangedSubView, but instead of it stacking the views vertically and allowing me to scroll through them all, it just stacks them on top of each other in the z-direction.

enter image description here

Here is the StackViewController Code:

class StackViewController: UIViewController {
    private let scrollView = UIScrollView()
    private let stackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(scrollView)
        scrollView.addSubview(stackView)
        setupConstraints()
        stackView.axis = .vertical
    }

    private func setupConstraints() {
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        stackView.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
                    scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
                    scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
                    scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                    scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
                    stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
                    stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
                    stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
                    ])
    }
}

extension StackViewController {
    func add(_ child: UIViewController) {
        addChild(child)
        stackView.addArrangedSubview(child.view)
        print(child.view!)
        child.didMove(toParent: self)
    }

    func remove(_ child: UIViewController) {
        guard child.parent != nil else {
            return
        }

        child.willMove(toParent: nil)
        stackView.removeArrangedSubview(child.view)
        child.view.removeFromSuperview()
        child.removeFromParent()
    }
}

Here is where I create each View Controller and add it to the StackViewController. For now, I run a loop and add copies of a single view controller over and over:

class PrototypeViewController: StackViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        for _ in 0...10 {
            setupUI()
        }
    }

    private func setupUI() {
        let storyboard = UIStoryboard(name: "ConsistencyGraph", bundle: .main)
        let consistencyGraphVC = storyboard.instantiateViewController(identifier: "ConsistencyGraphVC") as! ConsistencyGraphVC
        add(consistencyGraphVC)
        consistencyGraphVC.setupUI(name: "sessionName", consistencyPercentage: 30, ballsHit: 10)
    }
}

Here is the View Controller Code:

class ConsistencyGraphVC: UIViewController {

    @IBOutlet weak var mainView: UIView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var ballsHitLabel: UILabel!
    @IBOutlet weak var pieChartView: PieChartView!
        
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    open func setupUI(name: String, consistencyPercentage: Double, ballsHit: Int) {
        displayName(name:name)
        drawPieChart(consistencyPercentage: consistencyPercentage)
        displayBallsHit(ballsHit: ballsHit)
    }
    
    private func displayName(name: String) {
        let prefix = "Consistency: "
        let title = prefix + name
        titleLabel.text = title
    }
    
    private func displayBallsHit(ballsHit: Int) {
        ballsHitLabel.text = String(ballsHit)
    }
    
    private func drawPieChart(consistencyPercentage: Double) {
        let maxPercent:Double = 100
        let remainingPercent = maxPercent - consistencyPercentage
        let dataEntry = [PieChartDataEntry(value: consistencyPercentage, data: String(consistencyPercentage)), PieChartDataEntry(value: remainingPercent, data: nil)]
        let dataSet = PieChartDataSet(entries: dataEntry)
        let chartData = PieChartData(dataSet: dataSet)
        let color1 = randomColor()
        let color2 = randomColor()
        dataSet.colors = [color1, color2]
        pieChartView.data = chartData
    }
    
    private func randomColor() -> UIColor {
        let red = Double(arc4random_uniform(256))
        let green = Double(arc4random_uniform(256))
        let blue = Double(arc4random_uniform(256))
        let color = UIColor(red: CGFloat(red/255), green: CGFloat(green/255), blue: CGFloat(blue/255), alpha: 1)
        return color
    }
}

At first, I thought it was because the View Controller sizes might be ambiguous. I then hardcoded the width and height of each View Controller, but still no luck.

Any and all help is much appreciated! I'm at a loss as to how this is even possible.

Thank you in advance!

Ian
  • 113
  • 1
  • 6

2 Answers2

2

I have finally found the solution!

I needed to add:

child.view.heightAnchor.constraint(equalToConstant: child.view.frame.size.height).isActive = true

to the StackViewController here:

func add(_ child: UIViewController) {
        addChild(child)
        child.view.heightAnchor.constraint(equalToConstant: child.view.frame.size.height).isActive = true
        stackView.addArrangedSubview(child.view)
        child.didMove(toParent: self)
    }

I am not sure why this is the case. I had already hard coded the height of the view, but the stackView also wanted me to constrain it before adding the view.

I hope this helps someone in the future! I was beating my head against a wall for ages...

Ian
  • 113
  • 1
  • 6
  • 2
    To explain why... When you load a VC with `.instantiateViewController()`, the "root" view of that controller has `.translatesAutoresizingMaskIntoConstraints = true`. However, when you do this: `stackView.addArrangedSubview(child.view)`, the stack view automatically sets `child.view.translatesAutoresizingMaskIntoConstraints = false`. If your child views are square (1:1 ratio), you can change that line to: `child.view.heightAnchor.constraint(equalTo: child.view.widthAnchor).isActive = true` – DonMag Oct 12 '20 at 20:51
  • Thank you very much for the explanation @DonMag! Always better to know why something works so I can use it in the future. – Ian Oct 13 '20 at 12:47
  • @DonMag good explanation. I always though that stackviews took care of all of the sizing on its own. I just encountered this same exact issue and Ian's answer helped. But I didn't know that stack views set their subviews to 'translatesAutoresizingMaskIntoConstraints = false'. good to know for the future. Thanks – Lance Samaria Mar 26 '22 at 07:53
0

You forgot some required constraints for your StackView inside ScrollView. You only have trailing, top, and bottom which are not enough.

  • The correct one:
stackView.leadingAnchor
stackView.trailingAnchor
stackView.topAnchor
stackView.bottomAnchor
stackView.widthAnchor
Canh Tran
  • 298
  • 1
  • 14
  • Are you talking about the StackViewController? If so I have "stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)" – Ian Oct 12 '20 at 03:12
  • Is there another place I should be changing the constraints? – Ian Oct 12 '20 at 03:15
  • But the code you showed does not exist those constraints? You only add 3 constraints for your stackView – Canh Tran Oct 12 '20 at 06:14
  • All of the constraints including the ones you mentioned are within the setupConstraints() function in the StackViewController class above. Is there somewhere else I should be setting up the StackView constraints? – Ian Oct 12 '20 at 13:19