Search code examples
iosswiftuiscrollviewconstraints

I can't fix scroll view constraints in this complex view


I am having issues in my code with constraints. I'm seeing the following types of errors:

Will attempt to recover by breaking constraint <NSLayoutConstraint:0x600000ea6440 UIView:0x7fc684509760.height == UIScrollView:0x7fc685818000.height (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

I think the constraints are the problem.

Another problem that I have is with the buttons in buttonview. They are offset a little bit and sometimes when the numN is changing it doesn't fit well on the screen.

import UIKit
import Foundation

class PianoRollView: UIView {
    var numN = 127
    var numT = 4
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        // Draw the  grid
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(UIColor.black.cgColor)
        context?.setLineWidth(1.0)
        
        let noteHeight = rect.height / CGFloat(numN)
      
        for i in 1..<numN {
            let y = CGFloat(i) * noteHeight
            context?.move(to: CGPoint(x: 0, y: y))
            context?.addLine(to: CGPoint(x: rect.width, y: y))
        }
        
        let timeSlotWidth = rect.width / CGFloat(numT)
        
        for i in 1..<numT {
            let x = CGFloat(i) * timeSlotWidth
            context?.move(to: CGPoint(x: x, y: 0))
            context?.addLine(to: CGPoint(x: x, y: rect.height))
        }
        
        // Add line to right of grid
       context?.move(to: CGPoint(x: rect.width, y: 0))
      context?.addLine(to: CGPoint(x: rect.width, y: rect.height))
        // Add line to left of grid
        context?.move(to: CGPoint(x: 0, y: 0))
       context?.addLine(to: CGPoint(x: 0, y: rect.height))
        
        
        // Add line to top of grid
        context?.move(to: CGPoint(x: 0, y: 0))
       context?.addLine(to: CGPoint(x: rect.width, y: 0))
        
        // Draw bottom line
        context?.move(to: CGPoint(x: 0, y: rect.height))
       context?.addLine(to: CGPoint(x: rect.width, y: rect.height))
        
        context?.strokePath()
    }
}

class ViewController: UIViewController, UIScrollViewDelegate {
    let scrollView = UIScrollView()
    let pianoRollView = PianoRollView()
    
    // Create a container view to hold both the PianoRollView and the ButtonView
    let containerView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Add the UIScrollView to the view controller's view
        view.addSubview(scrollView)
        // Set the container view as the content view of the scroll view
        scrollView.addSubview(containerView)
        
        // Disable the autoresizing mask for both the UIScrollView and the PianoRollView
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        pianoRollView.translatesAutoresizingMaskIntoConstraints = false
        
        // Set the constraints for the UIScrollView to fill the view controller's view
        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        
        // Initialize the piano roll view
        pianoRollView.frame = CGRect(x: 0, y: 0, width: 2000, height: 2000)
        pianoRollView.layer.zPosition = 1
        
       containerView.frame = pianoRollView.frame
       
        containerView.addSubview(pianoRollView)
       
        
        // Set the constraints for the PianoRollView to fill the container view
        pianoRollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
        pianoRollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
        pianoRollView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
        pianoRollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
        pianoRollView.widthAnchor.constraint(equalTo: containerView.widthAnchor).isActive = true
        
        // Set the constraints for the container view to fill the scroll view
        containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
        containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
        containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
        containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
        containerView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
        
        // Add a UIView under the PianoRollView
        let buttonView = UIView()
              buttonView.translatesAutoresizingMaskIntoConstraints = false
        buttonView.frame =  pianoRollView.frame
        containerView.addSubview(buttonView)
        
        // Set the constraints for the ButtonView to align with the PianoRollView
        buttonView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
        buttonView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
        buttonView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
        buttonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
        buttonView.heightAnchor.constraint(equalTo: containerView.heightAnchor).isActive = true
        
// calculate size of notes
        let noteHeight = Int(round(pianoRollView.frame.height / CGFloat(pianoRollView.numN)) )
        
        // Add buttons to the buttonView with the same height as the space between the grids
        for i in 0..<pianoRollView.numN {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            buttonView.addSubview(button)
            
            button.leadingAnchor.constraint(equalTo: buttonView.leadingAnchor).isActive = true
            button.trailingAnchor.constraint(equalTo: buttonView.trailingAnchor).isActive = true
            button.topAnchor.constraint(equalTo: buttonView.topAnchor, constant: CGFloat(i) * CGFloat(noteHeight)).isActive = true
            button.bottomAnchor.constraint(equalTo: buttonView.topAnchor, constant: CGFloat(i+1) * CGFloat(noteHeight)).isActive = true

            button.setTitle("Button \(i)", for: .normal)
            button.setTitleColor(.black, for: .normal)
            button.backgroundColor = .red
        }

        // Set the content size of the scroll view to match the size of the piano roll view
        scrollView.contentSize = containerView.bounds.size
        
        // Enable scrolling in both directions
        scrollView.isScrollEnabled = true
        scrollView.showsVerticalScrollIndicator = true
        scrollView.showsHorizontalScrollIndicator = true
        
        // Set the background color of the piano roll view to white
        pianoRollView.backgroundColor = UIColor.clear

     
        // Set the delegate of the scroll view to self
        scrollView.delegate = self
        
        // Set the minimum zoom scale
       let minZoomScale = max(view.bounds.width / pianoRollView.bounds.width, view.bounds.height / pianoRollView.bounds.height)
       scrollView.minimumZoomScale = minZoomScale
       // scrollView.minimumZoomScale = 1
        scrollView.maximumZoomScale = 5
    }
    
    // Return the container view in the viewForZooming method
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return containerView
    }
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        // Adjust the content offset so that the content stays centered when zooming
        let horizontalInset = max(0, (scrollView.bounds.width - scrollView.contentSize.width) / 2)
        let verticalInset = max(0, (scrollView.bounds.height - scrollView.contentSize.height) / 2)
        scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
    }
}

Solution

  • Part of the problem is that you're mixing explicit frame settings with constraints.

    As a rule when working with UIScrollView, we either set frames and .contentSize OR we use constraints and let auto-layout do all the work for us.

    It can be very helpful to organize your code into "related" tasks. For example:

    • create views and set properties
    • add all views to the hierarchy
    • set constraints
    • do any additional tasks

    With your project, scrollView / pianoRollView / containerView are created as class properties, so the first thing in viewDidLoad() will be:

        // buttons will go under the PianoRollView
        let buttonView = UIView()
    

    next, add them to the view hierarchy:

        // Add the UIScrollView to the view controller's view
        view.addSubview(scrollView)
    
        // Set the container view as the content view of the scroll view
        scrollView.addSubview(containerView)
    
        // add buttonView to containerView
        containerView.addSubview(buttonView)
    
        // add pianoRollView to containerView
        containerView.addSubview(pianoRollView)
    

    we want to use auto-layout, so follow that with:

        // we will use auto-layout on all views
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        buttonView.translatesAutoresizingMaskIntoConstraints = false
        pianoRollView.translatesAutoresizingMaskIntoConstraints = false
    

    Now we can set constraints on all those views, relative to each other...

        // we (almost) always want to respect the safe area
        let safeG = view.safeAreaLayoutGuide
    
        // we want to constrain scrollView subviews to the scrollView's Content Layout Guide
        let contentG = scrollView.contentLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // Set the constraints for the UIScrollView to fill the view controller's view (safe area)
            scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
    
            // constrain containerView to Content Layout Guide
            //  this will define the "scrollable area"
            //  so we won't be setting .contentSize anywhere
            containerView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
            containerView.topAnchor.constraint(equalTo: contentG.topAnchor),
            containerView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
            
            // constrain all 4 sides of buttonView to containerView
            buttonView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            buttonView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            buttonView.topAnchor.constraint(equalTo: containerView.topAnchor),
            buttonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            
            // constrain all 4 sides of pianoRollView to containerView
            pianoRollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            pianoRollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            pianoRollView.topAnchor.constraint(equalTo: containerView.topAnchor),
            pianoRollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            
            // pianoRollView width
            pianoRollView.widthAnchor.constraint(equalToConstant: 2000.0),
            
        ])
    

    Note that we do NOT set a heightAnchor on pianoRollView. You'll see why...

    You're also adding numN buttons to buttonView ... that makes a little more sense to do after setting constraints on the above views.

    Your code indicates you want pianoView (and thus, buttonView) to have a Height of 2000. And then you want to evenly fill that height with numN buttons.

    In the code you posted, you're calculating the buttons' height like this:

    let noteHeight = Int(round(pianoRollView.frame.height / CGFloat(pianoRollView.numN)) )
    

    So, Height is 2000 and numN is 127:

    2000.0 / 127.0 == 15.748031496062993
    

    You're using round(), which is fine because it's a good idea to NOT use partial-points.

    If we use round(), we get:

    round(2000.0 / 127.0) == 16.0
    

    but, if we have 127 buttons each with a Height of 16...

    16 * 127 == 2032
    

    So we won't see the bottom two buttons!

    We could try using floor() ... and we get:

    floor(2000.0 / 127.0) == 15
    

    but, if we have 127 buttons each with a Height of 15...

    15 * 127 == 1905
    

    So we have a 95-point "gap" at the bottom!

    What you probably want to do is use floor() and ceil(), and then compare those values to see which one is closest to 2000.

    This could be written in fewer lines, but for clarification:

    // in this example, pianoRollView.numN is 127
    let targetHeight: CGFloat = 2000.0
    let floatNumN: CGFloat = CGFloat(pianoRollView.numN)
    
    let noteH1: CGFloat = floor(targetHeight / floatNumN)   // == 15.0
    let noteH2: CGFloat = ceil(targetHeight / floatNumN)    // == 16.0
    
    let totalHeight1: CGFloat = noteH1 * floatNumN          // == 1905
    let totalHeight2: CGFloat = noteH2 * floatNumN          // == 2032
        
    let diff1: CGFloat = abs(targetHeight - totalHeight1)   // == 95
    let diff2: CGFloat = abs(targetHeight - totalHeight2)   // == 32
    
    // if diff1 is less than diff2, use noteH1 else noteH2
    let noteHeight: CGFloat = diff1 < diff2 ? noteH1 : noteH2
    

    noteHeight now equals 16 and the totalHeight equals 2032 ... which is about 2000.

    Instead of setting a height on buttonView and then looping through and setting frames of the buttons, let's continue to take advantage of auto-layout.

    We'll create a "vertical chain" of constraints:

    • constrain the Top of the first button to the Top of buttonView
    • constrain the Top of each following button to the Bottom of the button above it
    • constrain the Bottom of the last button to the Bottom of buttonView

    that constraint chain now:

    • determines the height of buttonView
    • which also determines the height of pianoRollView
    • which also determines the height of containerView
    • which defines the .contentSize -- the "scrollable area"

    Another issue you have is that you are using different calculations for noteHeight for your buttons:

    let noteHeight = Int(round(pianoRollView.frame.height / CGFloat(pianoRollView.numN)) )
        
    

    and for the horizontal lines in pianoRollView:

    let noteHeight = rect.height / CGFloat(numN)
    

    So your buttons each had a height of 16-points, but your lines were spaced at 15.748031496062993.

    A better option would be to make noteHeight a property of pianoRollView ... and set it when you've calculated noteHeight (the button height) in viewDidLoad(). No need for another calculation.

    Here's the full thing, with some additional modifications. Read through the comments so you understand what's going on:

    class PianoRollView: UIView {
        
        var noteHeight: CGFloat = 0
        
        var numN = 127
        var numT = 4
        
        override func draw(_ rect: CGRect) {
            super.draw(rect)
            
            // Draw the  grid
            let context = UIGraphicsGetCurrentContext()
            context?.setStrokeColor(UIColor.black.cgColor)
            context?.setLineWidth(1.0)
            
            // UIKit *may* tell us to draw only PART of the view
            //  (the rect), so use bounds instead of rect
            
            // noteHeight will be set by the controller
            //  when it calculates the button heights
            // let noteHeight = rect.height / CGFloat(numN)
    
            for i in 1..<numN {
                let y = CGFloat(i) * noteHeight
                context?.move(to: CGPoint(x: 0, y: y))
                context?.addLine(to: CGPoint(x: bounds.width, y: y))
            }
            
            let timeSlotWidth = bounds.width / CGFloat(numT)
            
            for i in 1..<numT {
                let x = CGFloat(i) * timeSlotWidth
                context?.move(to: CGPoint(x: x, y: 0))
                context?.addLine(to: CGPoint(x: x, y: bounds.height))
            }
            
            // Add line to right of grid
            context?.move(to: CGPoint(x: bounds.width, y: 0))
            context?.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
            // Add line to left of grid
            context?.move(to: CGPoint(x: 0, y: 0))
            context?.addLine(to: CGPoint(x: 0, y: bounds.height))
            
            
            // Add line to top of grid
            context?.move(to: CGPoint(x: 0, y: 0))
            context?.addLine(to: CGPoint(x: bounds.width, y: 0))
            
            // Draw bottom line
            context?.move(to: CGPoint(x: 0, y: bounds.height))
            context?.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
            
            context?.strokePath()
            
        }
        
    }
    
    class PianoViewController: UIViewController, UIScrollViewDelegate {
        
        let scrollView = UIScrollView()
        let pianoRollView = PianoRollView()
        
        // Create a container view to hold both the PianoRollView and the ButtonView
        let containerView = UIView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
    
            // buttons will go under the PianoRollView
            let buttonView = UIView()
    
            // Add the UIScrollView to the view controller's view
            view.addSubview(scrollView)
    
            // Set the container view as the content view of the scroll view
            scrollView.addSubview(containerView)
    
            // add buttonView to containerView
            containerView.addSubview(buttonView)
    
            // add pianoRollView to containerView
            containerView.addSubview(pianoRollView)
    
            // we will use auto-layout on all views
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            containerView.translatesAutoresizingMaskIntoConstraints = false
            buttonView.translatesAutoresizingMaskIntoConstraints = false
            pianoRollView.translatesAutoresizingMaskIntoConstraints = false
    
            // we (almost) always want to respect the safe area
            let safeG = view.safeAreaLayoutGuide
            
            // we want to constrain scrollView subviews to the scrollView's Content Layout Guide
            let contentG = scrollView.contentLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // Set the constraints for the UIScrollView to fill the view controller's view (safe area)
                scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
                scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
                scrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
                scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
    
                // constrain containerView to Content Layout Guide
                //  this will define the "scrollable area"
                //  so we won't be setting .contentSize anywhere
                containerView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
                containerView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
                containerView.topAnchor.constraint(equalTo: contentG.topAnchor),
                containerView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
                
                // constrain all 4 sides of buttonView to containerView
                buttonView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                buttonView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
                buttonView.topAnchor.constraint(equalTo: containerView.topAnchor),
                buttonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
                
                // constrain all 4 sides of pianoRollView to containerView
                pianoRollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                pianoRollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
                pianoRollView.topAnchor.constraint(equalTo: containerView.topAnchor),
                pianoRollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
                
                // pianoRollView width
                pianoRollView.widthAnchor.constraint(equalToConstant: 2000.0),
                // we don't set height, because that will be controlled by the
                //  number of buttons (pianoRollView.numN) sized to get
                //  as close to 2000 as possible
                //pianoRollView.heightAnchor.constraint(equalToConstant: 2000.0),
                
            ])
    
            // calculate size of notes
            
            // let's get close to 2000
    
            // in this example, pianoRollView.numN is 127
            let targetHeight: CGFloat = 2000.0
            let floatNumN: CGFloat = CGFloat(pianoRollView.numN)
    
            let noteH1: CGFloat = floor(targetHeight / floatNumN)   // == 15.0
            let noteH2: CGFloat = ceil(targetHeight / floatNumN)    // == 16.0
    
            let totalHeight1: CGFloat = noteH1 * floatNumN          // == 1905
            let totalHeight2: CGFloat = noteH2 * floatNumN          // == 2032
            
            let diff1: CGFloat = abs(targetHeight - totalHeight1)   // == 95
            let diff2: CGFloat = abs(targetHeight - totalHeight2)   // == 32
    
            // if diff1 is less than diff2, use noteH1 else noteH2
            let noteHeight: CGFloat = diff1 < diff2 ? noteH1 : noteH2
    
            // noteHeight now equals 16
            
            // Add buttons to the buttonView
            //  we can constrain them vertically to each other
            var previousButton: UIButton!
            for i in 0..<pianoRollView.numN {
                let button = UIButton()
                button.translatesAutoresizingMaskIntoConstraints = false
                buttonView.addSubview(button)
                
                button.leadingAnchor.constraint(equalTo: buttonView.leadingAnchor).isActive = true
                button.trailingAnchor.constraint(equalTo: buttonView.trailingAnchor).isActive = true
                button.heightAnchor.constraint(equalToConstant: noteHeight).isActive = true
                
                if previousButton == nil {
                    // constrain FIRST button to Top of buttonView
                    button.topAnchor.constraint(equalTo: buttonView.topAnchor).isActive = true
                } else {
                    // constrain other buttons to Bottom of Previous Button
                    button.topAnchor.constraint(equalTo: previousButton.bottomAnchor).isActive = true
                }
                
                // update previousButton to the current button
                previousButton = button
                
                button.setTitle("Button \(i)", for: .normal)
                button.setTitleColor(.black, for: .normal)
                button.backgroundColor = .red
            }
            
            // constrain bottom of LAST button to bottom of buttonsView
            previousButton.bottomAnchor.constraint(equalTo: buttonView.bottomAnchor).isActive = true
            
            // so, our buttons are constrained with a "vertical chain" inside buttonView
            //  which determines the height of buttonView
            //  which also determines the height of pianoRollView
            //  which also determines the height of containerView
            //  which defines the .contentSize -- the "scrollable area"
            
            // set the noteHeight in pianoRollView so it will match
            //  pianoRollView no longer needs to re-caluclate the "line spacing"
            pianoRollView.noteHeight = noteHeight
            
            // these all default to True, so not needed
            // Enable scrolling in both directions
            //scrollView.isScrollEnabled = true
            //scrollView.showsVerticalScrollIndicator = true
            //scrollView.showsHorizontalScrollIndicator = true
            
            // Set the background color of the piano roll view to clear
            pianoRollView.backgroundColor = UIColor.clear
            
            // Set the delegate of the scroll view to self
            scrollView.delegate = self
            
            // Set the minimum zoom scale
            // can't do this here... views have not been laid-out yet
            //  so do it in viewDidAppear
            //let minZoomScale = max(view.bounds.width / pianoRollView.bounds.width, view.bounds.height / pianoRollView.bounds.height)
            //scrollView.minimumZoomScale = minZoomScale
            
            scrollView.maximumZoomScale = 5
            
            // during development, so we can see the scrollView framing
            scrollView.backgroundColor = .systemBlue
            
        }
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            // views have been laid-out, so now we can use their frames/bounds to calculate minZoomScale
            let minZoomScale = min(scrollView.frame.width / pianoRollView.bounds.width, scrollView.frame.height / pianoRollView.bounds.height)
            scrollView.minimumZoomScale = minZoomScale
        }
        
        // Return the container view in the viewForZooming method
        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return containerView
        }
        func scrollViewDidZoom(_ scrollView: UIScrollView) {
            // Adjust the content offset so that the content stays centered when zooming
            let horizontalInset = max(0, (scrollView.frame.width - scrollView.contentSize.width) / 2)
            let verticalInset = max(0, (scrollView.frame.height - scrollView.contentSize.height) / 2)
            scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
        }
    }
    

    If any of that is not clear - or even if you now understand it all - you would really benefit yourself by working through a bunch of Auto-Layout / UIScrollView tutorials...


    Edit

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    
        // views have been laid-out, so now we can use their frames/bounds to calculate minZoomScale
    
        // if we want min zoom to fit
        //  FULL pianoRollView
        let minZoomScale = min(scrollView.frame.width / pianoRollView.bounds.width, scrollView.frame.height / pianoRollView.bounds.height)
        
        // if we want min zoom to fit
        //  pianoRollView HEIGHT
        //let minZoomScale = scrollView.frame.height / pianoRollView.bounds.height
        
        // if we want min zoom to fit
        //  one "column" of pianoRollView
        //let minZoomScale = scrollView.frame.width / (pianoRollView.bounds.width / CGFloat(pianoRollView.numT))
        
        scrollView.minimumZoomScale = minZoomScale
    }