0

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

chemFour
  • 140
  • 7
  • 1
    Does `NSScrollView` automatically create its `contentView`? Have you tried only managing the constraints inside the document view and outside the scroll view and let `NSScrollVIew` manage its own views and constraints? – Willeke Feb 11 '22 at 02:17
  • What are you trying to accomplish? – Willeke Feb 11 '22 at 09:25
  • Tip: use "Capture View Hierarchy" to inspect the constraints. – Willeke Feb 11 '22 at 09:33
  • `NSScrollView` manages its `contentView` and `documentView` and will add subviews and constraints as needed. Pin the scroll view to its superview and constrain the parent view around its child views. If you pin the content view and document view to the outside of the scroll view then they won't move. If you don't see the parent view then it's size might be zero. – Willeke Feb 11 '22 at 14:18
  • Maybe this helps: [Enabling NSScrollView to scroll its contents using Auto Layout](https://stackoverflow.com/questions/29241474/enabling-nsscrollview-to-scroll-its-contents-using-auto-layout-in-interface-buil). – Willeke Feb 11 '22 at 14:19

1 Answers1

1

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()
        
    }
}
Willeke
  • 14,578
  • 4
  • 19
  • 47