swift 我无法修复此复杂视图中的滚动视图约束

relj7zay  于 2023-03-07  发布在  Swift
关注(0)|答案(1)|浏览(113)

我的代码中存在约束问题。我看到以下类型的错误:
将尝试通过打破限制来恢复〈NSLayoutConstraint:0x600000ea6440 UI视图:0x7fc684509760.height == UI滚动视图:0x7fc685818000.height(活动)〉
在UIViewAlertForUnsatisfiableConstraints处创建一个符号断点,以便在调试器中捕获此错误。〈UIKitCore/UIView. h〉中列出的UIView上的UISconstraintBasedLayoutDebugging类别中的方法也可能会有所帮助。
我认为约束是问题所在。
我遇到的另一个问题是buttonview中的按钮。它们有点偏移,有时当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)
    }
}
u5i3ibmn

u5i3ibmn1#

问题的一部分是您将显式帧设置与约束混合在一起。
作为一项规则,当使用UIScrollView时,我们要么设置框架和.contentSize***,要么***使用约束,让自动布局为我们完成所有工作。
将代码组织到“相关”任务中会非常有帮助。例如:

  • 创建视图并设置属性
  • 将所有视图添加到层次结构
  • 集合约束
  • 执行任何其他任务

在您的项目中,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),
        
    ])

注意,我们pianoRollView上设置heightAnchor
您还可以将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现在等于16totalHeight等于2032......大约是2000。
与其在buttonView上设置高度,然后循环并设置按钮的框架,不如继续利用自动布局。
我们将创建约束的“垂直链”:

  • first 按钮的顶部约束到buttonView的顶部
  • 将后面每个按钮的顶部约束到其上方按钮的底部
  • last 按钮的底部约束到buttonView的底部

该约束链现在:

  • 确定buttonView的高度
  • 它还确定了pianoRollView的高度
  • 它还确定containerView的高度
  • 它定义了.contentSize --“可滚动区域”

您遇到的另一个问题是,您对按钮的noteHeight使用了不同的计算方法:

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

对于pianoRollView中的水平行:

let noteHeight = rect.height / CGFloat(numN)

因此,每个按钮的高度为16-points,但线条的间距为15.748031496062993
一个更好的选择是将noteHeight设为pianoRollView的一个属性......并在计算viewDidLoad()中的noteHeight(按钮高度)时设置它。
下面是完整的内容,还有一些额外的修改。通读评论,这样你就明白了发生了什么:

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)
    }
}

如果其中任何一个不清楚--或者即使您现在已经全部理解了--您可以通过学习一系列Auto-Layout /UIScrollView教程来真正受益...

相关问题