Search code examples
iosswiftuiscrollviewautolayout

Cant get dynamic scroll view that sizes to its content working working


I'm having an issue getting my scroll view to to dynamically size and scroll to fit all of my content. I'm doing it all programmatically as I find it the easiest to use when dealing with auto layout. Anyways every solution I've come across online doesn't seem to work and the closest I got was when I tried this. contentView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).isActive = true Setting the height anchor of the context view to the frame layout guide of the scroll view allowed me to scroll but it still didn't reach the bottom. I feel like I must be overlooking something but I can't figure it out for the life of me. Here is my code below.Thanks Also sorry about how it's displayed below I can't get it to all fit in the box.

Edit: Prior to posting this I was experimenting with embedding my content view in a stack view which is why it's there but I have tried it without the stack and it still didn't work.

Edit 2: Found a solution and fixed how the code was displayed.

import UIKit
import Charts

class ViewController: UIViewController, UIScrollViewDelegate, ChartViewDelegate {

    let scrollView = UIScrollView()
    let contentStackView = UIStackView()
    let contentView = UIView()
    var dealStack, customScoreStack, basicInfoStack: UIStackView!
    var thisListing = Listing()
    var priceDollarSign, dealIndicatorArrow, vehicleImage, customScoreLogo: UIImageView!
    var vehiclePrice, dealType, dealValueLbl, milesLbl, customScoreValue, customScoreTitle, basicCarInfo, distanceAndDealer, graphTitle, specTitle, insuranceCalcTitle, commentsTitle, similarListingsTitle: UILabel!
    var graphSwitchingSegment, yearSwitchingSegment1, yearSwitchingSegment2: UISegmentedControl!
    var carPrice = "", dealValue = "1", miles = "", year = "2021", make = "GMC", model = "YUKON", trim = "DENALI", distance = "20", dealerName = "Dealer"
    var graphState = true
    var graphData1, graphData2: String!
    
    lazy var graph1: LineChartView = {
        let chartView = LineChartView()
        return chartView
    }()
    
    lazy var graph2: LineChartView = {
        let chartView = LineChartView()
        return chartView
    }()
    
    var months: [String]!
    var yValues: [ChartDataEntry]!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
        yValues = [
            ChartDataEntry(x: 1, y: 20.0),
            ChartDataEntry(x: 2, y: 4.0),
            ChartDataEntry(x: 3, y: 6.0),
            ChartDataEntry(x: 4, y: 3.0),
            ChartDataEntry(x: 5, y: 12.0),
            ChartDataEntry(x: 6, y: 16.0),
            ChartDataEntry(x: 7, y: 4.0),
            ChartDataEntry(x: 8, y: 18.0),
            ChartDataEntry(x: 9, y: 2.0),
            ChartDataEntry(x: 10, y: 4.0),
            ChartDataEntry(x: 11, y: 5.0),
            ChartDataEntry(x: 12, y: 4.0)
        ]
        
        self.scrollView.backgroundColor = .lightGray
        self.view.addSubview(scrollView)
        self.view.sendSubviewToBack(scrollView)
        self.scrollView.translatesAutoresizingMaskIntoConstraints = false
        self.scrollView.isScrollEnabled = true
        
        self.scrollView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
        self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        self.scrollView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
        self.scrollView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
        
        self.scrollView.addSubview(contentStackView)
        self.contentStackView.translatesAutoresizingMaskIntoConstraints = false
        self.contentStackView.axis = .vertical
        
        self.contentStackView.topAnchor.constraint(equalTo: self.scrollView.topAnchor).isActive = true
        self.contentStackView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor).isActive = true
        self.contentStackView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor).isActive = true
        self.contentStackView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor).isActive = true
        self.contentStackView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
        
        contentStackView.addArrangedSubview(contentView)
        contentView.translatesAutoresizingMaskIntoConstraints = false
        contentView.topAnchor.constraint(equalTo: self.contentStackView.topAnchor).isActive = true
        contentView.leadingAnchor.constraint(equalTo: self.contentStackView.leadingAnchor).isActive = true
        contentView.trailingAnchor.constraint(equalTo: self.contentStackView.trailingAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: self.contentStackView.bottomAnchor).isActive = true
        contentView.widthAnchor.constraint(equalTo: self.contentStackView.widthAnchor).isActive = true
        //contentView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).isActive = true
        contentView.sizeToFit()
        
        setUpView()

        graph1Setup()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.delegate = self
        print("View Size: \(self.view.frame.debugDescription) \nScroll View Size: \(self.scrollView.frame.debugDescription) \nScroll Content View Size: \(self.contentView.frame.debugDescription)")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        // Test subview locations here: print(_SUBVIEW.frame.debugDescription)
    }
    
    @objc func graphSwitcher(_ segmentedControl: UISegmentedControl) {
        switch graphSwitchingSegment.selectedSegmentIndex {
            case 0:
                graphState = true
            print("VALUE CHANGED")
                print(self.contentView.frame.debugDescription)
            case 1:
                graphState = false
                print("VALUE CHANGED")
            default:
                graphState = true
                break
        }
    }
    
    func setUpView() {
        //Object Instantiation
        dealStack = UIStackView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        dealStack.translatesAutoresizingMaskIntoConstraints = false
        dealStack.axis = .vertical
        dealStack.spacing = 1
        dealStack.distribution = .fillProportionally
        
        customScoreStack = UIStackView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        customScoreStack.translatesAutoresizingMaskIntoConstraints = false
        customScoreStack.axis = .vertical
        customScoreStack.spacing = 1
        customScoreStack.distribution = .fillProportionally
        
        basicInfoStack = UIStackView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        basicInfoStack.translatesAutoresizingMaskIntoConstraints = false
        basicInfoStack.axis = .vertical
        basicInfoStack.spacing = 2
        basicInfoStack.distribution = .fillProportionally
        
        priceDollarSign = UIImageView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        priceDollarSign.image = UIImage(named: "greenDollarsign")
        priceDollarSign.translatesAutoresizingMaskIntoConstraints = false
        
        dealIndicatorArrow = UIImageView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        dealIndicatorArrow.image = UIImage(named: "greenUpArrow")
        dealIndicatorArrow.translatesAutoresizingMaskIntoConstraints = false
        
        vehicleImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        vehicleImage.image = UIImage(named: "audiA7")
        vehicleImage.translatesAutoresizingMaskIntoConstraints = false

        customScoreLogo = UIImageView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        customScoreLogo.image = UIImage(named: "aladdinGenie")
        customScoreLogo.translatesAutoresizingMaskIntoConstraints = false
        
        customScoreTitle = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        customScoreTitle.text = "Genie Score:"
        customScoreTitle.textAlignment = .right
        customScoreTitle.translatesAutoresizingMaskIntoConstraints = false
        
        customScoreValue = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        customScoreValue.text = "VALUE_PLACE_HOLDER"
        customScoreValue.translatesAutoresizingMaskIntoConstraints = false
        
        vehiclePrice = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        vehiclePrice.text = numberFormatter.string(from: NSNumber(value: thisListing.getDetails().returnCarPrice()))
        vehiclePrice.font = UIFont.systemFont(ofSize: 30, weight: .semibold)
        vehiclePrice.textAlignment = .left
        vehiclePrice.translatesAutoresizingMaskIntoConstraints = false
        
        dealType = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        dealType.text = thisListing.getDetails().returnDealRatingType().0
        dealType.font = UIFont(name: dealType.font.fontName, size: 28)
        dealType.textColor = self.thisListing.getDetails().returnDealRatingType().1
        dealType.adjustsFontSizeToFitWidth = true
        dealType.minimumScaleFactor = 0.8
        dealType.numberOfLines = 0
        dealType.textAlignment = .right
        dealType.translatesAutoresizingMaskIntoConstraints = false
        
        dealValueLbl = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        dealValueLbl.text = "$" + (numberFormatter.string(from: NSNumber(value: self.thisListing.getDetails().returnDealDiff()))!) + (self.thisListing.getDetails().returnDealDiff() > 0 ? " ABOVE":" BELOW")
        dealValueLbl.font = UIFont(name: dealValueLbl.font.fontName, size: 19)
        dealValueLbl.adjustsFontSizeToFitWidth = true
        dealValueLbl.minimumScaleFactor = 0.8
        dealValueLbl.numberOfLines = 0
        dealValueLbl.textAlignment = .right
        dealValueLbl.translatesAutoresizingMaskIntoConstraints = false
        
        milesLbl = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        milesLbl.font = UIFont.systemFont(ofSize: 34, weight: .regular)
        milesLbl.textAlignment = .left
        milesLbl.adjustsFontSizeToFitWidth = true
        milesLbl.minimumScaleFactor = 0.8
        milesLbl.text = "\(numberFormatter.string(from: NSNumber(value: (self.miles as NSString).integerValue)) ?? "") Miles"
        milesLbl.translatesAutoresizingMaskIntoConstraints = false
        
        basicCarInfo = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        basicCarInfo.text = "\(year) \(make) \(model) \(trim)"
        basicCarInfo.font = UIFont.systemFont(ofSize: 20, weight: .semibold)
        basicCarInfo.adjustsFontSizeToFitWidth = true
        basicCarInfo.numberOfLines = 0
        basicCarInfo.minimumScaleFactor = 0.8
        basicCarInfo.textAlignment = .center
        basicCarInfo.translatesAutoresizingMaskIntoConstraints = false
        
        
        distanceAndDealer = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        distanceAndDealer.text = "\(distance) miles away from you at \(dealerName)"
        distanceAndDealer.font = UIFont.systemFont(ofSize: 20, weight: .regular)
        distanceAndDealer.adjustsFontSizeToFitWidth = true
        distanceAndDealer.numberOfLines = 0
        distanceAndDealer.minimumScaleFactor = 0.8
        distanceAndDealer.textAlignment = .center
        distanceAndDealer.translatesAutoresizingMaskIntoConstraints = false
        
        graphTitle = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        graphTitle.text = "Graphs"
        graphTitle.font = UIFont.systemFont(ofSize: 22, weight: .semibold)
        graphTitle.numberOfLines = 0
        graphTitle.adjustsFontSizeToFitWidth = true
        graphTitle.minimumScaleFactor = 0.8
        graphTitle.textAlignment = .center
        graphTitle.translatesAutoresizingMaskIntoConstraints = false
        
        graphSwitchingSegment = UISegmentedControl(items: ["Graph 1","Graph 2"])
        graphSwitchingSegment.addTarget(self, action: #selector(graphSwitcher(_:)), for: .valueChanged)
        graphSwitchingSegment.selectedSegmentIndex = 0
        graphSwitchingSegment.translatesAutoresizingMaskIntoConstraints = false
        
        graph1.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
        graph1.noDataText = "OOPS NO DATA HERE!"
        graph1.noDataTextColor = .green
        graph1.backgroundColor = .white
        graph1.translatesAutoresizingMaskIntoConstraints = false
        
        yearSwitchingSegment1 = UISegmentedControl(items: ["Year 1","Year 2"])
        yearSwitchingSegment1.addTarget(self, action: #selector(graphSwitcher(_:)), for: .valueChanged)
        yearSwitchingSegment1.selectedSegmentIndex = 0
        yearSwitchingSegment1.translatesAutoresizingMaskIntoConstraints = false
        
        //Adds objects to the view
        self.contentView.addSubview(priceDollarSign)
        self.contentView.addSubview(dealStack)
        self.contentView.addSubview(customScoreStack)
        self.contentView.addSubview(basicInfoStack)
        self.contentView.addSubview(dealIndicatorArrow)
        self.contentView.addSubview(vehicleImage)
        self.contentView.addSubview(vehiclePrice)
        self.contentView.addSubview(milesLbl)
        self.contentView.addSubview(customScoreLogo)
        self.contentView.addSubview(graphTitle)
        self.contentView.addSubview(graphSwitchingSegment)
        self.contentView.addSubview(graph1)
        self.contentView.addSubview(yearSwitchingSegment1)
        
        dealStack.addArrangedSubview(dealType)
        dealStack.addArrangedSubview(dealValueLbl)
        customScoreStack.addArrangedSubview(customScoreTitle)
        customScoreStack.addArrangedSubview(customScoreValue)
        basicInfoStack.addArrangedSubview(basicCarInfo)
        basicInfoStack.addArrangedSubview(distanceAndDealer)
        
        //Object Constraints
        priceDollarSign.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 7).isActive = true
        priceDollarSign.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 7).isActive = true
        priceDollarSign.widthAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.1).isActive = true
        priceDollarSign.widthAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
        priceDollarSign.widthAnchor.constraint(lessThanOrEqualToConstant: 100).isActive = true
        priceDollarSign.heightAnchor.constraint(equalTo: priceDollarSign.widthAnchor).isActive = true
        priceDollarSign.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
        priceDollarSign.heightAnchor.constraint(lessThanOrEqualToConstant: 100).isActive = true
        
        vehiclePrice.topAnchor.constraint(equalTo: priceDollarSign.topAnchor).isActive = true
        vehiclePrice.bottomAnchor.constraint(equalTo: priceDollarSign.bottomAnchor).isActive = true
        vehiclePrice.leadingAnchor.constraint(equalTo: priceDollarSign.trailingAnchor, constant: 8).isActive = true
        vehiclePrice.sizeToFit()
        vehiclePrice.layoutIfNeeded()
        
        dealStack.topAnchor.constraint(equalTo: priceDollarSign.topAnchor).isActive = true
        dealStack.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -7).isActive = true
        dealStack.heightAnchor.constraint(equalTo: priceDollarSign.heightAnchor).isActive = true
        
        dealType.heightAnchor.constraint(equalTo: dealValueLbl.heightAnchor).isActive = true
        dealType.widthAnchor.constraint(equalTo: dealValueLbl.widthAnchor).isActive = true
        dealType.sizeToFit()
        dealType.layoutIfNeeded()
        
        dealValueLbl.sizeToFit()
        dealValueLbl.layoutIfNeeded()

        dealIndicatorArrow.topAnchor.constraint(equalTo: priceDollarSign.topAnchor).isActive = true
        dealIndicatorArrow.bottomAnchor.constraint(equalTo: priceDollarSign.bottomAnchor).isActive = true
        dealIndicatorArrow.trailingAnchor.constraint(equalTo: dealType.leadingAnchor, constant: -1).isActive = true
        dealIndicatorArrow.widthAnchor.constraint(equalTo: priceDollarSign.widthAnchor).isActive = true
        
        vehicleImage.topAnchor.constraint(equalTo: priceDollarSign.bottomAnchor, constant: 8).isActive = true
        vehicleImage.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
        vehicleImage.widthAnchor.constraint(equalTo: self.contentView.widthAnchor).isActive = true
        vehicleImage.heightAnchor.constraint(equalTo: self.vehicleImage.widthAnchor, multiplier: 9/16).isActive = true
        vehicleImage.heightAnchor.constraint(lessThanOrEqualToConstant: self.view.frame.height * 0.3).isActive = true
        vehicleImage.contentMode = .scaleAspectFit
        
        milesLbl.topAnchor.constraint(equalTo: vehicleImage.bottomAnchor, constant: 8).isActive = true
        milesLbl.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8).isActive = true
        milesLbl.sizeToFit()
        milesLbl.layoutIfNeeded()
        
        customScoreStack.topAnchor.constraint(equalTo: milesLbl.topAnchor).isActive = true
        customScoreStack.heightAnchor.constraint(equalTo: milesLbl.heightAnchor).isActive = true
        customScoreStack.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -8).isActive = true
        
        customScoreLogo.topAnchor.constraint(equalTo: milesLbl.topAnchor).isActive = true
        customScoreLogo.trailingAnchor.constraint(equalTo: customScoreStack.leadingAnchor, constant: 1).isActive = true
        customScoreLogo.widthAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.1).isActive = true
        customScoreLogo.widthAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
        customScoreLogo.widthAnchor.constraint(lessThanOrEqualToConstant: 100).isActive = true
        customScoreLogo.heightAnchor.constraint(equalTo: customScoreLogo.widthAnchor).isActive = true
        customScoreLogo.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
        customScoreLogo.heightAnchor.constraint(lessThanOrEqualToConstant: 100).isActive = true
        
        basicInfoStack.topAnchor.constraint(equalTo: milesLbl.bottomAnchor, constant: 32).isActive = true
        basicInfoStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        basicInfoStack.widthAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.widthAnchor, constant: -10).isActive = true
        
        basicCarInfo.heightAnchor.constraint(equalTo: distanceAndDealer.heightAnchor).isActive = true
        
        graphTitle.topAnchor.constraint(equalTo: basicInfoStack.bottomAnchor, constant: 24).isActive = true
        graphTitle.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        graphTitle.widthAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.widthAnchor, constant: -16).isActive = true
        
        graphSwitchingSegment.topAnchor.constraint(equalTo: graphTitle.bottomAnchor, constant: 20).isActive = true
        graphSwitchingSegment.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        graphSwitchingSegment.widthAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
        
        graph1.topAnchor.constraint(equalTo: graphSwitchingSegment.bottomAnchor, constant: 2).isActive = true
        graph1.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        graph1.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        graph1.heightAnchor.constraint(equalTo: self.graph1.widthAnchor, multiplier: 12/16).isActive = true
        
        yearSwitchingSegment1.topAnchor.constraint(equalTo: graph1.bottomAnchor, constant: 100).isActive = true
        yearSwitchingSegment1.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        yearSwitchingSegment1.widthAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
    }
    
    func graph1Setup() {
        let dataSet = LineChartDataSet(entries: yValues, label: "Months")
        
        let data = LineChartData(dataSet: dataSet)
        graph1.data = data
    }
    
    func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
        print(entry)
    }
}

Solution

  • I found a solution that worked and turns out it was something small that I missed which blocked me for hours. I created a UIScrollView and UIView extension with a function that sorts the subviews by maxY of each and resizes the content view and height to be equal to that. The reason it wasn't working earlier is because the scroll view only had one subview which was a container view that housed all of the UI components. I'm not sure exactly why the container view still wasn't growing with it's subviews because whenever I would get rid of any height constraints it would end up being zero and the sub views would just be out of the bounds but with this fix it seems to work fine now. Heres what I did and the link to where I found help.

    Extensions

    extension UIScrollView {
        func updateContentView() {
            contentSize.height = subviews.sorted(by: { $0.frame.maxY < $1.frame.maxY }).last?.frame.maxY ?? contentSize.height
        }
    }
    extension UIView {
        func updateHeightToFitContent() {
            frame.size.height = subviews.sorted(by: { $0.frame.maxY < $1.frame.maxY }).last?.frame.maxY ?? frame.size.height
        }
    }
    

    viewDidLayoutSubviews()

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.delegate = self
    
        self.view.layoutIfNeeded()           //MAKE SURE YOU ADD THIS IT'S NEEDED
        contentView.updateHeightToFitContent()
        scrollView.updateContentView()
    }