【问题标题】:Embedd StackView in ScrollView that is embedded in a main StackView在主 StackView 中嵌入的 ScrollView 中嵌入 StackView
【发布时间】:2020-03-09 14:42:28
【问题描述】:

在主 StackView 中嵌入的 ScrollView 中嵌入 StackView

我想以编程方式执行一个相当复杂的详细视图时遇到问题。我的视图层次结构如下所示:

由于这可能会更好地解释可视化,我在这里有一个屏幕截图:

我的问题是我不知道如何在descriptionTextView 上设置高度约束——现在它设置为 400。但我想要的是它占用了所有可用空间作为 main 的中间项堆栈视图。将一个或多个 cmets 添加到 contentStackView 后,文本字段应缩小。

我不确定我必须为哪些视图设置哪些约束才能实现此目的...

到目前为止,这是我的看法:

import UIKit

class DetailSampleViewController: UIViewController {

    lazy var mainStackView: UIStackView = {
        let m = UIStackView()
        m.axis = .vertical
        m.alignment = .fill
        m.distribution = .fill
        m.spacing = 10
        m.translatesAutoresizingMaskIntoConstraints = false

        m.addArrangedSubview(titleTextField)
        m.addArrangedSubview(contentScrollView)
        m.addArrangedSubview(footerStackView)

        return m
    }()

    lazy var titleTextField: UITextField = {
        let t = UITextField()
        t.borderStyle = .roundedRect
        t.placeholder = "Some Fancy Placeholder"
        t.text = "Some Fancy Title"

        t.translatesAutoresizingMaskIntoConstraints = false

        return t
    }()

    lazy var contentScrollView: UIScrollView = {
        let s = UIScrollView()
        s.contentMode = .scaleToFill
        s.keyboardDismissMode = .onDrag

        s.translatesAutoresizingMaskIntoConstraints = false

        s.addSubview(contentStackView)

        return s
    }()

    lazy var contentStackView: UIStackView = {
        let s = UIStackView()
        s.translatesAutoresizingMaskIntoConstraints = false
        s.axis = .vertical
        s.alignment = .fill
        s.distribution = .equalSpacing
        s.spacing = 10
        s.contentMode = .scaleToFill

        s.addArrangedSubview(descriptionTextView)
        s.addArrangedSubview(getCommentLabel(with: "Some fancy comment"))
        s.addArrangedSubview(getCommentLabel(with: "Another fancy comment"))
        s.addArrangedSubview(getCommentLabel(with: "And..."))
        s.addArrangedSubview(getCommentLabel(with: "..even..."))
        s.addArrangedSubview(getCommentLabel(with: "...more..."))
        s.addArrangedSubview(getCommentLabel(with: "...comments..."))
        s.addArrangedSubview(getCommentLabel(with: "Some fancy comment"))
        s.addArrangedSubview(getCommentLabel(with: "Another fancy comment"))
        s.addArrangedSubview(getCommentLabel(with: "And..."))
        s.addArrangedSubview(getCommentLabel(with: "..even..."))
        s.addArrangedSubview(getCommentLabel(with: "...more..."))
        s.addArrangedSubview(getCommentLabel(with: "...comments..."))

        return s
    }()

    lazy var descriptionTextView: UITextView = {
       let tv = UITextView()
        tv.font = UIFont.systemFont(ofSize: 17.0)
        tv.clipsToBounds = true
        tv.layer.cornerRadius = 5.0
        tv.layer.borderWidth = 0.25

        tv.translatesAutoresizingMaskIntoConstraints = false

        tv.text = """
        Some fancy textfield text,
        spanning over multiple

        lines

        ...
        """

        return tv
    }()

    lazy var footerStackView: UIStackView = {
        let f = UIStackView()
        f.axis = .horizontal
        f.alignment = .fill
        f.distribution = .fillEqually

        let commentLabel = UILabel()
        commentLabel.text = "Comments"

        let addCommentButton = UIButton(type: UIButton.ButtonType.system)

        addCommentButton.setTitle("Add Comment", for: .normal)

        f.addArrangedSubview(commentLabel)
        f.addArrangedSubview(addCommentButton)

        return f
    }()

    override func loadView() {
        view = UIView()
        view.backgroundColor = . systemBackground

        navigationController?.isToolbarHidden = true

        view.addSubview(mainStackView)

        NSLayoutConstraint.activate([
            mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
            mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
            mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
            mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),

            titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height),

            contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
            contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
            contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
            contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),

            descriptionTextView.heightAnchor.constraint(equalToConstant: 400),
            descriptionTextView.leadingAnchor.constraint(equalTo: mainStackView.leadingAnchor),
            descriptionTextView.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
        ])
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Detail View"
    }

    func getCommentLabel(with text: String) -> UILabel {
        let l = UILabel()
        l.layer.borderWidth = 0.25
        l.translatesAutoresizingMaskIntoConstraints = false
        l.text = text
        return l
    }
}

【问题讨论】:

  • 您希望您的描述文本视图具有最小高度并且可以滚动吗?或者,您是否希望其最小高度基于其不可滚动的文本?
  • 其实这两个问题很好... cmets 在所有。一旦有,我猜内容滚动视图文本视图都需要是可滚动的(这样用户就无法看到 cmets 的存在)。

标签: ios


【解决方案1】:

你很接近,但有几点注意事项:

在使用堆栈视图时 - 特别是在滚动视图中 - 您有时需要明确定义哪些元素可以拉伸,哪些元素可以压缩或不压缩。

要在滚动视图有足够的内容之前填充它,您需要设置约束,使组合的内容高度等于滚动视图框架的高度,但是给出该约束的优先级较低,因此当您有足够的垂直内容时,自动布局可以“破坏”它。

个人喜好:我通常不喜欢在 lazy var 声明中添加子视图。尝试设置约束时可能会变得混乱。

我已经重新设计了您发布的代码,以至少接近您的目标。它从没有评论标签开始...点击“添加评论”按钮将添加“编号评论标签”,每三个评论将换行。

改变的方式并没有那么多……而且我认为我添加了足够多的 cmets 以使事情变得清晰。

class DetailSampleViewController: UIViewController {

    lazy var mainStackView: UIStackView = {
        let m = UIStackView()
        m.axis = .vertical
        m.alignment = .fill
        m.distribution = .fill
        m.spacing = 10
        m.translatesAutoresizingMaskIntoConstraints = false
        // don't add subviews here
        return m
    }()

    lazy var titleTextField: UITextField = {
        let t = UITextField()
        t.borderStyle = .roundedRect
        t.placeholder = "Some Fancy Placeholder"
        t.text = "Some Fancy Title"
        t.translatesAutoresizingMaskIntoConstraints = false
        return t
    }()

    lazy var contentScrollView: UIScrollView = {
        let s = UIScrollView()
        s.contentMode = .scaleToFill
        s.keyboardDismissMode = .onDrag
        s.translatesAutoresizingMaskIntoConstraints = false
        // don't add subviews here
        return s
    }()

    lazy var contentStackView: UIStackView = {
        let s = UIStackView()
        s.translatesAutoresizingMaskIntoConstraints = false
        s.axis = .vertical
        s.alignment = .fill
        // distribution needs to be .fill (not .equalSpacing)
        s.distribution = .fill
        s.spacing = 10
        s.contentMode = .scaleToFill
        // don't add subviews here
        return s
    }()

    lazy var descriptionTextView: UITextView = {
        let tv = UITextView()
        tv.font = UIFont.systemFont(ofSize: 17.0)
        tv.clipsToBounds = true
        tv.layer.cornerRadius = 5.0
        tv.layer.borderWidth = 0.25

        tv.translatesAutoresizingMaskIntoConstraints = false

        tv.text = """
        Some fancy textfield text,
        spanning over multiple lines.

        This textView now has a minimum height of 160-pts.
        """

        return tv
    }()

    lazy var footerStackView: UIStackView = {
        let f = UIStackView()
        f.axis = .horizontal
        f.alignment = .fill
        f.distribution = .fillEqually

        let commentLabel = UILabel()
        commentLabel.text = "Comments"

        let addCommentButton = UIButton(type: UIButton.ButtonType.system)

        addCommentButton.setTitle("Add Comment", for: .normal)

        // add a target so we can add comment labels
        addCommentButton.addTarget(self, action: #selector(addCommentLabel(_:)), for: .touchUpInside)

        // don't allow button height to be compressed
        addCommentButton.setContentCompressionResistancePriority(.required, for: .vertical)

        f.addArrangedSubview(commentLabel)
        f.addArrangedSubview(addCommentButton)

        return f
    }()

    // just for demo - numbers the added comment labels
    var commentIndex: Int = 0

    // do all this in viewDidLoad(), not in loadView()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = . systemBackground

        navigationController?.isToolbarHidden = true

        title = "Detail View"

        // add the mainStackView
        view.addSubview(mainStackView)

        // add elements to mainStackView
        mainStackView.addArrangedSubview(titleTextField)
        mainStackView.addArrangedSubview(contentScrollView)
        mainStackView.addArrangedSubview(footerStackView)

        // add contentStackView to contentScrollView
        contentScrollView.addSubview(contentStackView)

        // add descriptionTextView to contentStackView
        contentStackView.addArrangedSubview(descriptionTextView)

        // tell contentStackView to be the height of contentScrollView frame
        let contentStackHeight = contentStackView.heightAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.heightAnchor)
        // but give it a lower priority do it can grow as comment labels are added
        contentStackHeight.priority = .defaultLow

        NSLayoutConstraint.activate([

            // constrain mainStackView top / bottom / leading / trailing to safe area
            mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
            mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
            mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
            mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),

            // title text field
            titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height),

            // minimum height for descriptionTextView
            descriptionTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 160.0),

            // constrain contentStackView top / leading / trailing / bottom to contentScrollView
            contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
            contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
            contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
            contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),

            // constrain contentStackView width to contentScrollView frame
            contentStackView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor),

            // activate contentStackHeight constraint
            contentStackHeight,

        ])

        // during dev, give some background colors so we can see the frames
        contentScrollView.backgroundColor = .cyan
        descriptionTextView.backgroundColor = .yellow

    }

    @objc func addCommentLabel(_ sender: Any?) -> Void {
        // commentIndex is just used to number the added comments
        commentIndex += 1

        // let's make every third label end up with multiple lines, just to
        // confirm variable-height labels won't mess things up
        var s = "This is label \(commentIndex)"
        if commentIndex % 3 == 0 {
            s += ", and it has enough text that it should need to wrap onto multiple lines, even in landscape orientation."
        }
        let v = getCommentLabel(with: s)
        // don't let comment labels stretch vertically
        v.setContentHuggingPriority(.required, for: .vertical)
        // don't let comment labels get compressed vertically
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        contentStackView.addArrangedSubview(v)

        // auto-scroll to bottom to show newly added comment label
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
            let r = CGRect(x: 0.0, y: self.contentScrollView.contentSize.height - 1.0, width: 1.0, height: 1.0)
            self.contentScrollView.scrollRectToVisible(r, animated: true)
        }
    }

    func getCommentLabel(with text: String) -> UILabel {
        let l = UILabel()
        l.layer.borderWidth = 0.25
        l.translatesAutoresizingMaskIntoConstraints = false
        l.text = text
        // allow wrapping / multi-line comments
        l.numberOfLines = 0
        return l
    }
}

【讨论】:

  • 哇,这看起来棒极了?。一直在忙于实施一个适合我的解决方案(通过计算viewDidAppeardescriptionTextView 的高度(后来的原因似乎只有其他对象所需的高度可用......) - 但是你的解决方案更优雅!我会在有时间的时候继续重构您的解决方案。到目前为止非常感谢;一旦我完全确认它有效,就会标记答案。
  • 可以确认现在可以正常工作,再次感谢。
猜你喜欢
  • 2019-08-19
  • 2018-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-09
相关资源
最近更新 更多