Advertisement
Don_Mag

Piano Roll

Mar 14th, 2023
1,191
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Swift 15.10 KB | None | 0 0
  1.  
  2. // the main change to fix your issue...
  3. //
  4. // on zoom, we change the Width constraint of the Left Container View - based on the zoomScale
  5. //   so it always matches the Width of the Left Scroll View frame
  6. //
  7. // made some other changes to try and help...
  8. //   - changed object / var names for consistency (makes it easier to keep track of things)
  9. //   - modified a few constraints - some were not needed
  10. //   - re-organized some of the code for readability
  11. //   - disabled .bouncesZoom for a better visual
  12.  
  13. import UIKit
  14.  
  15. class PianoViewController: UIViewController, UIScrollViewDelegate {
  16.    
  17.     let rightScrollView = UIScrollView()
  18.     let rightPianoRollView = PianoRollView()
  19.  
  20.     let leftScrollView = UIScrollView()
  21.     let leftPianoRollView = PianoRollViewl()
  22.  
  23.     let rightContainerView = UIView()
  24.     let leftContainerView = UIView()
  25.    
  26.     // we will modify the left container width constraint when zooming
  27.     //  so it always fills the left scroll view width
  28.     // let's define the left container / subviews / scroll view width here
  29.     let leftContainerWidth: CGFloat = 50
  30.     var leftContainerWidthConstraint: NSLayoutConstraint!
  31.  
  32.     override func viewDidLoad() {
  33.         super.viewDidLoad()
  34.        
  35.         view.backgroundColor = UIColor.black
  36.        
  37.         // buttons will go under the PianoRollView
  38.         let rightButtonsView = UIView()
  39.        
  40.         let leftButtonsView = UIView()
  41.        
  42.         // Add the UIScrollView to the view controller's view
  43.         view.addSubview(rightScrollView)
  44.         view.addSubview(leftScrollView)
  45.        
  46.         // Set the container view as the content view of the scroll view
  47.         rightScrollView.addSubview(rightContainerView)
  48.         leftScrollView.addSubview(leftContainerView)
  49.        
  50.         // add buttons views amd pianoRoll views to container views
  51.         rightContainerView.addSubview(rightButtonsView)
  52.         rightContainerView.addSubview(rightPianoRollView)
  53.        
  54.         leftContainerView.addSubview(leftButtonsView)
  55.         leftContainerView.addSubview(leftPianoRollView)
  56.        
  57.         // we will use auto-layout on all views
  58.        
  59.         rightScrollView.translatesAutoresizingMaskIntoConstraints = false
  60.         leftScrollView.translatesAutoresizingMaskIntoConstraints = false
  61.         leftContainerView.translatesAutoresizingMaskIntoConstraints = false
  62.         leftButtonsView.translatesAutoresizingMaskIntoConstraints = false
  63.         leftPianoRollView.translatesAutoresizingMaskIntoConstraints = false
  64.         rightContainerView.translatesAutoresizingMaskIntoConstraints = false
  65.         rightButtonsView.translatesAutoresizingMaskIntoConstraints = false
  66.         rightPianoRollView.translatesAutoresizingMaskIntoConstraints = false
  67.        
  68.         // we (almost) always want to respect the safe area
  69.         let safeG = view.safeAreaLayoutGuide
  70.        
  71.         // we want to constrain scrollView subviews to the scrollView's Content Layout Guide
  72.         let contentG = rightScrollView.contentLayoutGuide
  73.         let contentgleft = leftScrollView.contentLayoutGuide
  74.        
  75.         // left pianoRollView width
  76.         // auto-layout often complains (generates "breaking constraints" messages to the debug console)
  77.         //  when views get sized dynamically - this will avoid those warning messages
  78.         // we will activate it below
  79.         let leftPianoRollWidthConstraint = leftPianoRollView.widthAnchor.constraint(equalToConstant: leftContainerWidth)
  80.         leftPianoRollWidthConstraint.priority = .required - 1
  81.  
  82.         // we will be modifying this constraint when zooming
  83.         leftContainerWidthConstraint = leftContainerView.widthAnchor.constraint(equalToConstant: leftContainerWidth)
  84.  
  85.         NSLayoutConstraint.activate([
  86.            
  87.             leftScrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
  88.             leftScrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
  89.             leftScrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
  90.             leftScrollView.widthAnchor.constraint(equalToConstant: leftContainerWidth),
  91.  
  92.             rightScrollView.leadingAnchor.constraint(equalTo: leftScrollView.trailingAnchor),
  93.             rightScrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
  94.             rightScrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
  95.             rightScrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
  96.            
  97.             leftContainerView.leadingAnchor.constraint(equalTo: contentgleft.leadingAnchor),
  98.             leftContainerView.topAnchor.constraint(equalTo: contentgleft.topAnchor),
  99.             leftContainerView.bottomAnchor.constraint(equalTo: contentgleft.bottomAnchor),
  100.             // leftContainerView does NOT need a trailing anchor
  101.            
  102.             // activate leftContainerWidthConstraint
  103.             leftContainerWidthConstraint,
  104.            
  105.             leftPianoRollView.leadingAnchor.constraint(equalTo: leftContainerView.leadingAnchor),
  106.             leftPianoRollView.trailingAnchor.constraint(equalTo: leftContainerView.trailingAnchor),
  107.             leftPianoRollView.topAnchor.constraint(equalTo: leftContainerView.topAnchor),
  108.             leftPianoRollView.bottomAnchor.constraint(equalTo: leftContainerView.bottomAnchor),
  109.  
  110.             // activate the leftPianoRollView width constraint we created above
  111.             leftPianoRollWidthConstraint,
  112.  
  113.             leftButtonsView.leadingAnchor.constraint(equalTo: leftContainerView.leadingAnchor),
  114.             leftButtonsView.trailingAnchor.constraint(equalTo: leftContainerView.trailingAnchor),
  115.             leftButtonsView.topAnchor.constraint(equalTo: leftContainerView.topAnchor),
  116.             leftButtonsView.bottomAnchor.constraint(equalTo: leftContainerView.bottomAnchor),
  117.            
  118.             // constrain containerView to Content Layout Guide
  119.             //  this will define the "scrollable area"
  120.             //  so we won't be setting .contentSize anywhere
  121.             rightContainerView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
  122.             rightContainerView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
  123.             rightContainerView.topAnchor.constraint(equalTo: contentG.topAnchor),
  124.             rightContainerView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
  125.            
  126.             // constrain all 4 sides of buttonView to containerView
  127.             rightButtonsView.leadingAnchor.constraint(equalTo: rightContainerView.leadingAnchor),
  128.             rightButtonsView.trailingAnchor.constraint(equalTo: rightContainerView.trailingAnchor),
  129.             rightButtonsView.topAnchor.constraint(equalTo: rightContainerView.topAnchor),
  130.             rightButtonsView.bottomAnchor.constraint(equalTo: rightContainerView.bottomAnchor),
  131.            
  132.             // constrain all 4 sides of pianoRollView to containerView
  133.             rightPianoRollView.leadingAnchor.constraint(equalTo: rightContainerView.leadingAnchor),
  134.             rightPianoRollView.trailingAnchor.constraint(equalTo: rightContainerView.trailingAnchor),
  135.             rightPianoRollView.topAnchor.constraint(equalTo: rightContainerView.topAnchor),
  136.             rightPianoRollView.bottomAnchor.constraint(equalTo: rightContainerView.bottomAnchor),
  137.            
  138.             // pianoRollView width
  139.             rightPianoRollView.widthAnchor.constraint(equalToConstant: 2000.0),
  140.            
  141.         ])
  142.        
  143.         // calculate size of notes
  144.        
  145.         // let's get close to 2000
  146.        
  147.         // in this example, pianoRollView.numN is 127
  148.         let targetHeight: CGFloat = 2000.0
  149.         let floatNumN: CGFloat = CGFloat(rightPianoRollView.numN)
  150.        
  151.         let noteH1: CGFloat = floor(targetHeight / floatNumN)   // == 15.0
  152.         let noteH2: CGFloat = ceil(targetHeight / floatNumN)    // == 16.0
  153.        
  154.         let totalHeight1: CGFloat = noteH1 * floatNumN          // == 1905
  155.         let totalHeight2: CGFloat = noteH2 * floatNumN          // == 2032
  156.        
  157.         let diff1: CGFloat = abs(targetHeight - totalHeight1)   // == 95
  158.         let diff2: CGFloat = abs(targetHeight - totalHeight2)   // == 32
  159.        
  160.         // if diff1 is less than diff2, use noteH1 else noteH2
  161.         let noteHeight: CGFloat = diff1 < diff2 ? noteH1 : noteH2
  162.        
  163.         // noteHeight now equals 16
  164.        
  165.         // Add buttons to the leftButtonsView
  166.         //  we can constrain them vertically to each other
  167.         var leftpreviousButton: UIButton!
  168.         for i in 0..<rightPianoRollView.numN {
  169.             let button = UIButton()
  170.             button.translatesAutoresizingMaskIntoConstraints = false
  171.             leftButtonsView.addSubview(button)
  172.            
  173.             button.leadingAnchor.constraint(equalTo: leftButtonsView.leadingAnchor).isActive = true
  174.             button.trailingAnchor.constraint(equalTo: leftButtonsView.trailingAnchor).isActive = true
  175.             button.heightAnchor.constraint(equalToConstant: noteHeight).isActive = true
  176.            
  177.             if leftpreviousButton == nil {
  178.                 // constrain FIRST button to Top of buttonView
  179.                 button.topAnchor.constraint(equalTo: leftButtonsView.topAnchor).isActive = true
  180.             } else {
  181.                 // constrain other buttons to Bottom of Previous Button
  182.                 button.topAnchor.constraint(equalTo: leftpreviousButton.bottomAnchor).isActive = true
  183.             }
  184.            
  185.             // update previousButton to the current button
  186.             leftpreviousButton = button
  187.            
  188.             button.setTitle("B\(i)", for: .normal)
  189.             button.setTitleColor(.black, for: .normal)
  190.             button.backgroundColor = .green
  191.         }
  192.        
  193.         // constrain bottom of LAST button to bottom of buttonsView
  194.         leftpreviousButton.bottomAnchor.constraint(equalTo: leftButtonsView.bottomAnchor).isActive = true
  195.        
  196.         // Add buttons to the buttonView
  197.         //  we can constrain them vertically to each other
  198.         var previousButton: UIButton!
  199.         for i in 0..<rightPianoRollView.numN {
  200.             let button = UIButton()
  201.             button.translatesAutoresizingMaskIntoConstraints = false
  202.             rightButtonsView.addSubview(button)
  203.            
  204.             button.leadingAnchor.constraint(equalTo: rightButtonsView.leadingAnchor).isActive = true
  205.             button.trailingAnchor.constraint(equalTo: rightButtonsView.trailingAnchor).isActive = true
  206.             button.heightAnchor.constraint(equalToConstant: noteHeight).isActive = true
  207.            
  208.             if previousButton == nil {
  209.                 // constrain FIRST button to Top of buttonView
  210.                 button.topAnchor.constraint(equalTo: rightButtonsView.topAnchor).isActive = true
  211.             } else {
  212.                 // constrain other buttons to Bottom of Previous Button
  213.                 button.topAnchor.constraint(equalTo: previousButton.bottomAnchor).isActive = true
  214.             }
  215.            
  216.             // update previousButton to the current button
  217.             previousButton = button
  218.            
  219.             button.setTitle("Button \(i)", for: .normal)
  220.             button.setTitleColor(.black, for: .normal)
  221.             button.backgroundColor = .red
  222.         }
  223.        
  224.         // constrain bottom of LAST button to bottom of buttonsView
  225.         previousButton.bottomAnchor.constraint(equalTo: rightButtonsView.bottomAnchor).isActive = true
  226.        
  227.        
  228.         rightPianoRollView.noteHeight = noteHeight
  229.         leftPianoRollView.noteHeight = noteHeight
  230.        
  231.         // make sure left PianoRoll has the same .numN
  232.         leftPianoRollView.numN = rightPianoRollView.numN
  233.        
  234.         // Set the background color of the piano roll view to clear
  235.         leftPianoRollView.backgroundColor = UIColor.clear
  236.         rightPianoRollView.backgroundColor = UIColor.clear
  237.         leftButtonsView.backgroundColor = UIColor.purple
  238.        
  239.         // Set the delegate of the scroll view to self
  240.         rightScrollView.delegate = self
  241.         leftScrollView.delegate = self
  242.        
  243.         rightScrollView.maximumZoomScale = 5
  244.         leftScrollView.maximumZoomScale = 5
  245.  
  246.         leftScrollView.showsHorizontalScrollIndicator = false
  247.         // probably also want to not-show the vertical indicator
  248.         //  because it just looks odd
  249.         leftScrollView.showsVerticalScrollIndicator = false
  250.  
  251.         // for better visual result, disable zoom bouncing
  252.         rightScrollView.bouncesZoom = false
  253.         leftScrollView.bouncesZoom = false
  254.        
  255.         // during development, so we can see the scrollView framing
  256.         rightScrollView.backgroundColor = UIColor.blue
  257.         leftScrollView.backgroundColor = UIColor.orange
  258.        
  259.         leftContainerView.backgroundColor = UIColor.yellow
  260.  
  261.     }
  262.    
  263.     override func viewDidAppear(_ animated: Bool) {
  264.         super.viewDidAppear(animated)
  265.        
  266.         let minZoomScale = rightScrollView.frame.height / rightPianoRollView.bounds.height
  267.         rightScrollView.minimumZoomScale = minZoomScale
  268.         leftScrollView.minimumZoomScale = minZoomScale
  269.     }
  270.    
  271.     // Return the container view in the viewForZooming method
  272.     func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  273.         return scrollView.subviews.first
  274.     }
  275.    
  276.     // implement the UIScrollViewDelegate method for tracking the zoom scale
  277.     func scrollViewDidZoom(_ scrollView: UIScrollView) {
  278.        
  279.         if scrollView == rightScrollView {
  280.  
  281.             // Adjust the content offset so that the content stays centered when zooming
  282.             let horizontalInset = max(0, (scrollView.bounds.width - scrollView.contentSize.width) )
  283.             let verticalInset = max(0, (scrollView.bounds.height - scrollView.contentSize.height) )
  284.             scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
  285.            
  286.             leftScrollView.zoomScale = rightScrollView.zoomScale
  287.            
  288.         } else if scrollView == leftScrollView {
  289.  
  290.             rightScrollView.zoomScale = leftScrollView.zoomScale
  291.  
  292.         }
  293.  
  294.         // set leftContainer Width based on zoomScale so it always
  295.         //  fits the leftScrollView Width
  296.         leftContainerWidthConstraint.constant = leftContainerWidth / leftScrollView.zoomScale
  297.        
  298.     }
  299.    
  300.     // implement UIScrollViewDelegate method
  301.     func scrollViewDidScroll(_ scrollView: UIScrollView) {
  302.         if scrollView == self.rightScrollView {
  303.             self.syncScrollView(self.leftScrollView, toScrollView: self.rightScrollView)
  304.         }
  305.         else if scrollView == self.leftScrollView {
  306.             self.syncScrollView(self.rightScrollView, toScrollView: leftScrollView)
  307.         }
  308.     }
  309.    
  310.     func syncScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
  311.         scrollViewToScroll.contentOffset.y = scrolledView.contentOffset.y
  312.     }
  313.    
  314. }
  315.  
  316. class PianoRollView: UIView {
  317.    
  318.     var noteHeight: CGFloat = 0
  319.     var notes: [(note: Int, startTime: Double, duration: Double)] = []
  320.     var numN = 22
  321.     var numT = 4
  322.    
  323.     override func draw(_ rect: CGRect) {
  324.         super.draw(rect)
  325.        
  326.         // Draw the  grid
  327.         let context = UIGraphicsGetCurrentContext()
  328.         context?.setStrokeColor(UIColor.black.cgColor)
  329.         context?.setLineWidth(1.0)
  330.        
  331.         for i in 1..<numN {
  332.             let y = CGFloat(i) * noteHeight
  333.             context?.move(to: CGPoint(x: 0, y: y))
  334.             context?.addLine(to: CGPoint(x: bounds.width, y: y))
  335.         }
  336.        
  337.         let timeSlotWidth = bounds.width / CGFloat(numT)
  338.        
  339.         for i in 1..<numT {
  340.             let x = CGFloat(i) * timeSlotWidth
  341.             context?.move(to: CGPoint(x: x, y: 0))
  342.             context?.addLine(to: CGPoint(x: x, y: bounds.height))
  343.         }
  344.        
  345.         // Add line to right of grid
  346.         context?.move(to: CGPoint(x: bounds.width, y: 0))
  347.         context?.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
  348.         // Add line to left of grid
  349.         context?.move(to: CGPoint(x: 0, y: 0))
  350.         context?.addLine(to: CGPoint(x: 0, y: bounds.height))
  351.        
  352.        
  353.         // Add line to top of grid
  354.         context?.move(to: CGPoint(x: 0, y: 0))
  355.         context?.addLine(to: CGPoint(x: bounds.width, y: 0))
  356.        
  357.         // Draw bottom line
  358.         context?.move(to: CGPoint(x: 0, y: bounds.height))
  359.         context?.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
  360.        
  361.         context?.strokePath()
  362.     }
  363.    
  364. }
  365.  
  366. class PianoRollViewl: UIView {
  367.    
  368.     var noteHeight: CGFloat = 0
  369.    
  370.     var numN = 22
  371.     var numT = 4
  372.    
  373.     override func draw(_ rect: CGRect) {
  374.         super.draw(rect)
  375.        
  376.         // Draw the  grid
  377.         let context = UIGraphicsGetCurrentContext()
  378.         context?.setStrokeColor(UIColor.black.cgColor)
  379.         context?.setLineWidth(1.0)
  380.        
  381.         // UIKit *may* tell us to draw only PART of the view
  382.         //  (the rect), so use bounds instead of rect
  383.        
  384.         // noteHeight will be set by the controller
  385.         //  when it calculates the button heights
  386.         // let noteHeight = rect.height / CGFloat(numN)
  387.        
  388.         for i in 1..<numN {
  389.             let y = CGFloat(i) * noteHeight
  390.             context?.move(to: CGPoint(x: 0, y: y))
  391.             context?.addLine(to: CGPoint(x: bounds.width, y: y))
  392.         }
  393.        
  394.         context?.strokePath()
  395.     }
  396.    
  397. }
  398.  
  399.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement