Quite a few SO posts on implementing a scroll view programmatically but I haven't found a solution for this case... here I am adding subviews to the document view (with everything laid out using layout anchors), which all works apart from the scrolling.
I think the issue here is that AppKit interprets the constraints in the example to mean there isn't anything to scroll, but I am not sure why...
import Cocoa
class ViewController: NSViewController {
var scrollView = NSScrollView()
var contentView = NSClipView()
var documentView = ParentView()
var generateButton = NSButton()
@objc func generate(_ sender: NSObject) {
let child = ChildView()
child.translatesAutoresizingMaskIntoConstraints = false
documentView.addSubview(child)
documentView.setupChildViewLayout(sv: child)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(generateButton)
view.addSubview(scrollView)
setupLayout()
}
func setupLayout() {
scrollView.contentView = contentView
scrollView.documentView = documentView
scrollView.borderType = .lineBorder
scrollView.hasHorizontalScroller = true
scrollView.hasVerticalScroller = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
documentView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
scrollView.trailingAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -150),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100)
])
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
scrollView.trailingAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -150),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100)
])
scrollView.addConstraints([
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(greaterThanOrEqualTo: scrollView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.bottomAnchor)
])
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(greaterThanOrEqualTo: scrollView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.bottomAnchor)
])
scrollView.addConstraints([
documentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
documentView.trailingAnchor.constraint(greaterThanOrEqualTo: scrollView.trailingAnchor),
documentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
documentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.bottomAnchor)
])
NSLayoutConstraint.activate([
documentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
documentView.trailingAnchor.constraint(greaterThanOrEqualTo: scrollView.trailingAnchor),
documentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
documentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.bottomAnchor)
])
// Setup anchor constraints for the button
setupGenerateButton()
generateButton.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
generateButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
generateButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
generateButton.heightAnchor.constraint(equalToConstant: 30),
generateButton.widthAnchor.constraint(equalToConstant: 200)
])
NSLayoutConstraint.activate([
generateButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
generateButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
generateButton.heightAnchor.constraint(equalToConstant: 30),
generateButton.widthAnchor.constraint(equalToConstant: 200)
])
}
func setupGenerateButton() {
generateButton.attributedTitle = NSMutableAttributedString(string: "GenerateChildView", attributes: [NSAttributedString.Key.strokeColor: (NSColor.white), NSAttributedString.Key.font: NSFont.systemFont(ofSize: (NSFont.systemFontSize))])
generateButton.wantsLayer = true
generateButton.bezelColor = NSColor(red: 0.2, green: 0.2, blue: 0.6, alpha: 1.0)
generateButton.action = #selector(self.generate(_:))
}
}
class ParentView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
func setupChildViewLayout(sv: ChildView) {
if (self.subviews.count < 2) {
self.addConstraints([
sv.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
sv.topAnchor.constraint(equalTo: self.topAnchor)
])
NSLayoutConstraint.activate([
sv.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
sv.topAnchor.constraint(equalTo: self.topAnchor)
])
}
else {
let c = self.subviews.count - 2
let lastView = self.subviews[c]
self.addConstraints([
sv.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
sv.topAnchor.constraint(equalTo: lastView.bottomAnchor)
])
NSLayoutConstraint.activate([
sv.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
sv.topAnchor.constraint(equalTo: lastView.bottomAnchor)
])
}
}
}
class ChildView: NSView {
override var intrinsicContentSize: NSSize {
return CGSize(width: 650, height: 200)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.gray.set()
self.bounds.frame()
}
}
CodePudding user response:
You're still pinning the content view and the document view to the scroll view. Only add constraints to the outside of the scroll view and the inside of the document view. Don't add constraints between the content view, the document view and the scroll view. Add constraints between the parent view and its child views so the childs views will fit inside the parent view.
class ViewController: NSViewController {
var scrollView = NSScrollView()
var documentView = ParentView()
var generateButton = NSButton()
@objc func generate(_ sender: NSObject) {
let child = ChildView()
child.translatesAutoresizingMaskIntoConstraints = false
documentView.addSubview(child)
documentView.setupChildViewLayout(sv: child)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(generateButton)
view.addSubview(scrollView)
setupLayout()
}
func setupLayout() {
scrollView.documentView = documentView
scrollView.borderType = .lineBorder
scrollView.hasHorizontalScroller = true
scrollView.hasVerticalScroller = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
documentView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -150),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100)
])
// Setup anchor constraints for the button
setupGenerateButton()
generateButton.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
generateButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
generateButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
generateButton.heightAnchor.constraint(equalToConstant: 30),
generateButton.widthAnchor.constraint(equalToConstant: 200)
])
}
func setupGenerateButton() {
generateButton.attributedTitle = NSMutableAttributedString(string: "GenerateChildView", attributes: [NSAttributedString.Key.strokeColor: (NSColor.white), NSAttributedString.Key.font: NSFont.systemFont(ofSize: (NSFont.systemFontSize))])
generateButton.wantsLayer = true
generateButton.bezelColor = NSColor(red: 0.2, green: 0.2, blue: 0.6, alpha: 1.0)
generateButton.action = #selector(self.generate(_:))
}
}
class ParentView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
func setupChildViewLayout(sv: ChildView) {
if (self.subviews.count == 1) {
self.addConstraints([
sv.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
sv.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor),
sv.topAnchor.constraint(equalTo: self.topAnchor),
sv.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
else {
let c = self.subviews.count - 2
let lastView = self.subviews[c]
self.addConstraints([
sv.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),
sv.topAnchor.constraint(equalTo: lastView.bottomAnchor)
])
let bottomConstraints = self.constraints.filter {
$0.firstAttribute == NSLayoutConstraint.Attribute.bottom
}
self.removeConstraints(bottomConstraints)
self.addConstraints([
sv.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
}
}
class ChildView: NSView {
override var intrinsicContentSize: NSSize {
return CGSize(width: 650, height: 200)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.gray.set()
self.bounds.frame()
}
}