我的代码中存在约束问题。我看到以下类型的错误:
会尝试通过打破约束来恢复
在 UIViewAlertForUnsatisfiableConstraints 处创建一个符号断点以在调试器中捕获它。
中列出的 UIView 的 UIConstraintBasedLayoutDebugging 类别中的方法也可能有帮助。
我认为限制是问题所在。
我遇到的另一个问题是按钮视图中的按钮。它们有一点偏移,有时当 numN 改变时它不适合屏幕。
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)
}
}
部分问题是您将显式框架设置与约束混合在一起。
通常在使用
UIScrollView
时,我们要么设置框架,要么 .contentSize
OR 我们使用约束并让自动布局为我们完成所有工作。
将代码组织成“相关”任务会很有帮助。例如:
对于您的项目,
scrollView
/ pianoRollView
/ containerView
被创建为类属性,因此 viewDidLoad()
中的第一件事将是:
// 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),
])
请注意,我们确实NOT在
heightAnchor
上设置了pianoRollView
。你会明白为什么......
您还向
numN
添加了buttonView
按钮...在对上述视图设置约束后 更有意义。
您的代码表明您希望 pianoView(以及 buttonView)的高度为 2000。然后您希望用
numN
按钮均匀填充该高度。
在您发布的代码中,您正在像这样计算按钮的高度:
let noteHeight = Int(round(pianoRollView.frame.height / CGFloat(pianoRollView.numN)) )
所以,高度是 2000,numN 是 127:
2000.0 / 127.0 == 15.748031496062993
你正在使用
round()
,这很好,因为不使用部分点是个好主意。
如果我们使用
round()
,我们得到:
round(2000.0 / 127.0) == 16.0
但是,如果我们有 127 个按钮,每个按钮的高度都是 16...
16 * 127 == 2032
所以我们不会看到底部的两个按钮!
我们可以尝试使用
floor()
...我们得到:
floor(2000.0 / 127.0) == 15
但是,如果我们有 127 个按钮,每个按钮的高度都是 15...
15 * 127 == 1905
所以我们在底部有一个95分的“差距”!
您可能想要做的是使用
floor()
和 ceil()
,然后比较这些值以查看哪个 最接近 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
现在等于 16
和 totalHeight
等于 2032
... about 2000.
与其在
buttonView
上设置高度然后循环并设置按钮的框架,不如继续利用自动布局。
我们将创建约束的“垂直链”:
buttonView
buttonView
现在的约束链:
您遇到的另一个问题是您对按钮的
noteHeight
使用不同的计算:
let noteHeight = Int(round(pianoRollView.frame.height / CGFloat(pianoRollView.numN)) )
对于
pianoRollView
中的水平线:
let noteHeight = rect.height / CGFloat(numN)
所以你的每个按钮都有
16-points
的高度,但是你的线间隔为15.748031496062993
。
更好的选择是使
noteHeight
成为pianoRollView
的属性......并在您计算noteHeight
(按钮高度)时设置它viewDidLoad()
。不需要再计算了。
这是完整的东西,还有一些额外的修改。通读评论,以便您了解发生了什么:
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)
}
}
如果有任何不清楚的地方——或者即使你现在已经完全理解了——你会通过学习一堆自动布局/
UIScrollView
教程来真正受益...
编辑
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
}