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)
}
}
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()
}