For your approach...
You want to constrain your contentView
to the scroll view's Content Layout Guide. This will automatically determine the "scrollable" area.
Since you're not using auto-layout for the contentView's subviews, you'll need to update the .contentView
Width and Height constraints each time you add a new subview.
Here's an example. We'll create a MyScrollView
at 40-pts from Top / Leading / Trailing with a Height of 240-pts .
The subview will have a Green background, the contentView will have a Blue background, and the scroll view will have a Red background (so we can easily see the frames).
We'll start with just ONE subview to make it easy to see what happens. At first, with only one small subview, there will be no scrolling.
Each time we tap anywhere, we'll add a new Subview to the contentView, with a Yellow background, and update the contentView's Width and Height constraints as needed. You'll see that the Blue content view gets bigger to match the subviews. As soon as the subviews cause the content view to be larger than the width or height of the scroll view, scrolling will be automatic.
public class MyScrollView: UIScrollView {
private var contentView:UIView!
// contentView's Width and Height constraints
// we'll update the .constant values when we add subviews
private var cvWidthConstraint: NSLayoutConstraint!
private var cvHeightConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.translatesAutoresizingMaskIntoConstraints = false
self.isScrollEnabled = true
self.isDirectionalLockEnabled = true
self.showsHorizontalScrollIndicator = true
self.showsVerticalScrollIndicator = false
self.decelerationRate = .normal
self.delaysContentTouches = false
self.bouncesZoom = true
setupSubviews()
}
private func setupSubviews() {
contentView = UIView()
contentView.backgroundColor = UIColor.clear
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.isUserInteractionEnabled = true
self.addSubview(contentView)
// constrain contentView to scroll view's Content Layout Guide
// this determines the "scrollable" area
contentView.leadingAnchor.constraint(equalTo: self.contentLayoutGuide.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: self.contentLayoutGuide.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor).isActive = true
// create contentView's Width and Height constraints
cvWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0.0)
cvHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 0.0)
// activate them
cvWidthConstraint.isActive = true
cvHeightConstraint.isActive = true
//Add other subviews to contentView
// we'll start with ONE subview, so we can easily see what's happening
let subviewWidth = CGFloat(240)
let subviewHeight = CGFloat(20)
let subview = UILabel(frame: CGRect(x: 5, y: 5, width: subviewWidth, height: subviewHeight))
subview.textAlignment = .center
subview.text = "First"
subview.backgroundColor = .green
contentView.addSubview(subview)
// so we can see the frames
self.backgroundColor = .red
self.contentView.backgroundColor = .blue
// update the contentView constraints
updateContent()
}
private func updateContent() -> Void {
// array of subviews
let views = contentView.subviews
// get the
// max Y of the subview frames
// max X of the subview frames
guard let maxYValue = views.lazy.map({ $0.frame.maxY }).max(),
let maxXValue = views.lazy.map({ $0.frame.maxX }).max()
else { return }
// update contentView Width and Height constraints
cvWidthConstraint.constant = maxXValue + 5.0
cvHeightConstraint.constant = maxYValue + 5.0
}
func addLabel(frame _frame: CGRect, text: String) -> Void {
// add a new subview
let subview = UILabel(frame: _frame)
subview.textAlignment = .center
subview.text = text
subview.backgroundColor = .yellow
contentView.addSubview(subview)
// update the contentView constraints
updateContent()
}
}
class ExampleViewController: UIViewController {
let myScrollView = MyScrollView()
var count: Int = 1
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(myScrollView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain custom scroll view Top / Leading / Trailing
// 40-pts from the safe-area edges
myScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
myScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// scroll view Height: 240-pts
myScrollView.heightAnchor.constraint(equalToConstant: 240.0),
])
// add tap gesture recognizer so we can add a new subview
// every time we tap
let t = UITapGestureRecognizer(target: self, action: #selector(self.testAddSubview))
view.addGestureRecognizer(t)
}
@objc func testAddSubview() -> Void {
let s = "New Subview \(count)"
let x: CGFloat = CGFloat(count) * 60.0
let y: CGFloat = CGFloat(count) * 35.0
myScrollView.addLabel(frame: CGRect(x: x, y: y, width: 200, height: 30), text: s)
count += 1
}
}
On launch - one subview - no scrolling:

After adding one new subview - blue content view is larger, but not big enough for scrolling:

After adding 4 new subviews - now we have scrolling:

UIScrollView
works. If you're using auto-layout for the scroll view's content (which, you should be doing), you never want to set.contentSize
-- that's just wrong. - DonMag