// 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
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎