Don_Mag icon

Piano Roll

Don_Mag | PRO | 03/14/23 01:37:37 PM UTC | 0 ⭐ | 8027 👁️ | Never ⏰ | []
Swift |

15.1 KB

|

None

|

0 👍

/

0 👎

 
// the main change to fix your issue...
//
// on zoom, we change the Width constraint of the Left Container View - based on the zoomScale
//   so it always matches the Width of the Left Scroll View frame
//
// made some other changes to try and help...
//   - changed object / var names for consistency (makes it easier to keep track of things)
//   - modified a few constraints - some were not needed
//   - re-organized some of the code for readability
//   - disabled .bouncesZoom for a better visual
 
import UIKit
 
class PianoViewController: UIViewController, UIScrollViewDelegate {
    
    let rightScrollView = UIScrollView()
    let rightPianoRollView = PianoRollView()
 
    let leftScrollView = UIScrollView()
    let leftPianoRollView = PianoRollViewl()
 
    let rightContainerView = UIView()
    let leftContainerView = UIView()
    
    // we will modify the left container width constraint when zooming
    //  so it always fills the left scroll view width
    // let's define the left container / subviews / scroll view width here
    let leftContainerWidth: CGFloat = 50
    var leftContainerWidthConstraint: NSLayoutConstraint!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor.black
        
        // buttons will go under the PianoRollView
        let rightButtonsView = UIView()
        
        let leftButtonsView = UIView()
        
        // Add the UIScrollView to the view controller's view
        view.addSubview(rightScrollView)
        view.addSubview(leftScrollView)
        
        // Set the container view as the content view of the scroll view
        rightScrollView.addSubview(rightContainerView)
        leftScrollView.addSubview(leftContainerView)
        
        // add buttons views amd pianoRoll views to container views
        rightContainerView.addSubview(rightButtonsView)
        rightContainerView.addSubview(rightPianoRollView)
        
        leftContainerView.addSubview(leftButtonsView)
        leftContainerView.addSubview(leftPianoRollView)
        
        // we will use auto-layout on all views
        
        rightScrollView.translatesAutoresizingMaskIntoConstraints = false
        leftScrollView.translatesAutoresizingMaskIntoConstraints = false
        leftContainerView.translatesAutoresizingMaskIntoConstraints = false
        leftButtonsView.translatesAutoresizingMaskIntoConstraints = false
        leftPianoRollView.translatesAutoresizingMaskIntoConstraints = false
        rightContainerView.translatesAutoresizingMaskIntoConstraints = false
        rightButtonsView.translatesAutoresizingMaskIntoConstraints = false
        rightPianoRollView.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 = rightScrollView.contentLayoutGuide
        let contentgleft = leftScrollView.contentLayoutGuide
        
        // left pianoRollView width
        // auto-layout often complains (generates "breaking constraints" messages to the debug console)
        //  when views get sized dynamically - this will avoid those warning messages
        // we will activate it below
        let leftPianoRollWidthConstraint = leftPianoRollView.widthAnchor.constraint(equalToConstant: leftContainerWidth)
        leftPianoRollWidthConstraint.priority = .required - 1
 
        // we will be modifying this constraint when zooming
        leftContainerWidthConstraint = leftContainerView.widthAnchor.constraint(equalToConstant: leftContainerWidth)
 
        NSLayoutConstraint.activate([
            
            leftScrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
            leftScrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
            leftScrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
            leftScrollView.widthAnchor.constraint(equalToConstant: leftContainerWidth),
 
            rightScrollView.leadingAnchor.constraint(equalTo: leftScrollView.trailingAnchor),
            rightScrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
            rightScrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
            rightScrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
            
            leftContainerView.leadingAnchor.constraint(equalTo: contentgleft.leadingAnchor),
            leftContainerView.topAnchor.constraint(equalTo: contentgleft.topAnchor),
            leftContainerView.bottomAnchor.constraint(equalTo: contentgleft.bottomAnchor),
            // leftContainerView does NOT need a trailing anchor
            
            // activate leftContainerWidthConstraint
            leftContainerWidthConstraint,
            
            leftPianoRollView.leadingAnchor.constraint(equalTo: leftContainerView.leadingAnchor),
            leftPianoRollView.trailingAnchor.constraint(equalTo: leftContainerView.trailingAnchor),
            leftPianoRollView.topAnchor.constraint(equalTo: leftContainerView.topAnchor),
            leftPianoRollView.bottomAnchor.constraint(equalTo: leftContainerView.bottomAnchor),
 
            // activate the leftPianoRollView width constraint we created above
            leftPianoRollWidthConstraint,
 
            leftButtonsView.leadingAnchor.constraint(equalTo: leftContainerView.leadingAnchor),
            leftButtonsView.trailingAnchor.constraint(equalTo: leftContainerView.trailingAnchor),
            leftButtonsView.topAnchor.constraint(equalTo: leftContainerView.topAnchor),
            leftButtonsView.bottomAnchor.constraint(equalTo: leftContainerView.bottomAnchor),
            
            // constrain containerView to Content Layout Guide
            //  this will define the "scrollable area"
            //  so we won't be setting .contentSize anywhere
            rightContainerView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
            rightContainerView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
            rightContainerView.topAnchor.constraint(equalTo: contentG.topAnchor),
            rightContainerView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
            
            // constrain all 4 sides of buttonView to containerView
            rightButtonsView.leadingAnchor.constraint(equalTo: rightContainerView.leadingAnchor),
            rightButtonsView.trailingAnchor.constraint(equalTo: rightContainerView.trailingAnchor),
            rightButtonsView.topAnchor.constraint(equalTo: rightContainerView.topAnchor),
            rightButtonsView.bottomAnchor.constraint(equalTo: rightContainerView.bottomAnchor),
            
            // constrain all 4 sides of pianoRollView to containerView
            rightPianoRollView.leadingAnchor.constraint(equalTo: rightContainerView.leadingAnchor),
            rightPianoRollView.trailingAnchor.constraint(equalTo: rightContainerView.trailingAnchor),
            rightPianoRollView.topAnchor.constraint(equalTo: rightContainerView.topAnchor),
            rightPianoRollView.bottomAnchor.constraint(equalTo: rightContainerView.bottomAnchor),
            
            // pianoRollView width
            rightPianoRollView.widthAnchor.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(rightPianoRollView.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 leftButtonsView
        //  we can constrain them vertically to each other
        var leftpreviousButton: UIButton!
        for i in 0..<rightPianoRollView.numN {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            leftButtonsView.addSubview(button)
            
            button.leadingAnchor.constraint(equalTo: leftButtonsView.leadingAnchor).isActive = true
            button.trailingAnchor.constraint(equalTo: leftButtonsView.trailingAnchor).isActive = true
            button.heightAnchor.constraint(equalToConstant: noteHeight).isActive = true
            
            if leftpreviousButton == nil {
                // constrain FIRST button to Top of buttonView
                button.topAnchor.constraint(equalTo: leftButtonsView.topAnchor).isActive = true
            } else {
                // constrain other buttons to Bottom of Previous Button
                button.topAnchor.constraint(equalTo: leftpreviousButton.bottomAnchor).isActive = true
            }
            
            // update previousButton to the current button
            leftpreviousButton = button
            
            button.setTitle("B\(i)", for: .normal)
            button.setTitleColor(.black, for: .normal)
            button.backgroundColor = .green
        }
        
        // constrain bottom of LAST button to bottom of buttonsView
        leftpreviousButton.bottomAnchor.constraint(equalTo: leftButtonsView.bottomAnchor).isActive = true
        
        // Add buttons to the buttonView
        //  we can constrain them vertically to each other
        var previousButton: UIButton!
        for i in 0..<rightPianoRollView.numN {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            rightButtonsView.addSubview(button)
            
            button.leadingAnchor.constraint(equalTo: rightButtonsView.leadingAnchor).isActive = true
            button.trailingAnchor.constraint(equalTo: rightButtonsView.trailingAnchor).isActive = true
            button.heightAnchor.constraint(equalToConstant: noteHeight).isActive = true
            
            if previousButton == nil {
                // constrain FIRST button to Top of buttonView
                button.topAnchor.constraint(equalTo: rightButtonsView.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: rightButtonsView.bottomAnchor).isActive = true
        
        
        rightPianoRollView.noteHeight = noteHeight
        leftPianoRollView.noteHeight = noteHeight
        
        // make sure left PianoRoll has the same .numN
        leftPianoRollView.numN = rightPianoRollView.numN
        
        // Set the background color of the piano roll view to clear
        leftPianoRollView.backgroundColor = UIColor.clear
        rightPianoRollView.backgroundColor = UIColor.clear
        leftButtonsView.backgroundColor = UIColor.purple
        
        // Set the delegate of the scroll view to self
        rightScrollView.delegate = self
        leftScrollView.delegate = self
        
        rightScrollView.maximumZoomScale = 5
        leftScrollView.maximumZoomScale = 5
 
        leftScrollView.showsHorizontalScrollIndicator = false
        // probably also want to not-show the vertical indicator
        //  because it just looks odd
        leftScrollView.showsVerticalScrollIndicator = false
 
        // for better visual result, disable zoom bouncing
        rightScrollView.bouncesZoom = false
        leftScrollView.bouncesZoom = false
        
        // during development, so we can see the scrollView framing
        rightScrollView.backgroundColor = UIColor.blue
        leftScrollView.backgroundColor = UIColor.orange
        
        leftContainerView.backgroundColor = UIColor.yellow
 
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let minZoomScale = rightScrollView.frame.height / rightPianoRollView.bounds.height
        rightScrollView.minimumZoomScale = minZoomScale
        leftScrollView.minimumZoomScale = minZoomScale
    }
    
    // Return the container view in the viewForZooming method
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return scrollView.subviews.first
    }
    
    // implement the UIScrollViewDelegate method for tracking the zoom scale
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        
        if scrollView == rightScrollView {
 
            // Adjust the content offset so that the content stays centered when zooming
            let horizontalInset = max(0, (scrollView.bounds.width - scrollView.contentSize.width) )
            let verticalInset = max(0, (scrollView.bounds.height - scrollView.contentSize.height) )
            scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
            
            leftScrollView.zoomScale = rightScrollView.zoomScale
            
        } else if scrollView == leftScrollView {
 
            rightScrollView.zoomScale = leftScrollView.zoomScale
 
        }
 
        // set leftContainer Width based on zoomScale so it always
        //  fits the leftScrollView Width
        leftContainerWidthConstraint.constant = leftContainerWidth / leftScrollView.zoomScale
        
    }
    
    // implement UIScrollViewDelegate method
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == self.rightScrollView {
            self.syncScrollView(self.leftScrollView, toScrollView: self.rightScrollView)
        }
        else if scrollView == self.leftScrollView {
            self.syncScrollView(self.rightScrollView, toScrollView: leftScrollView)
        }
    }
    
    func syncScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
        scrollViewToScroll.contentOffset.y = scrolledView.contentOffset.y
    }
    
}
 
class PianoRollView: UIView {
    
    var noteHeight: CGFloat = 0
    var notes: [(note: Int, startTime: Double, duration: Double)] = []
    var numN = 22
    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)
        
        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 PianoRollViewl: UIView {
    
    var noteHeight: CGFloat = 0
    
    var numN = 22
    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))
        }
        
        context?.strokePath()
    }
    
}
 
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎