【问题标题】:UIScrollView content inset defined by a height of a view outside of the subtreeUIScrollView 内容插图由子树外视图的高度定义
【发布时间】:2020-06-03 12:23:22
【问题描述】:

给定视图层次结构:

- View `container`
    - UIScrollView `scrollView`
        - UIView `content`
    - UIView `footer`

我希望UIScrollView.contentInset.bottom 等于footer.bounds.height

问题:这可以用自动布局来表达吗?

现在,我知道有一种非常明显的蛮力方法,并且有效:

  1. 观察footerbounds 属性的变化
  2. scrollView.contentInset.bottom = -footer.bounds.height 一旦footer 的父母完成了layoutSubviews()

或者,我可以在content.bottomscrollView.bottom 之间设置一个约束(我敢肯定,你知道,它表示底部内容插入的大小不明确的内容),并且每个常量都改变了footer 边界改变的时间。

但关键是所有这些方法都非常流行,真的让我对它们产生的糟糕代码感到不舒服,所以我想知道:

这可以用自动布局来表达吗?

我已尝试执行以下操作:

content.bottomAnchor.constraint(equalTo: footer.topAnchor)

希望content.bottomAnchor 将被视为滚动视图内容的底部插图,但不——自动布局实际上将其视为我将内容的底部限制到页脚的顶部。

【问题讨论】:

  • 如果没有更多上下文,您的方法似乎令人困惑。我最初的想法是:为什么不将滚动视图的底部限制在页脚视图的顶部?
  • @DonMag 哦,不,这是不可能的,那样就太容易了。问题是页脚是透明的,我需要滚动视图位于其下方,以便滚动视图的内容位于其下方,以便在那里可见,因为这是设计要求。它的方式类似于带有自动内容插入的 UITabBar - 您可以通过模糊查看底层内容。
  • 好的 - 我有一个自动布局解决方案给你。您是在 Storyboard / IB 中进行布局吗?还是全部通过代码完成?
  • @DonMag 我们正在通过代码进行操作。 NSLayoutConstraint, NSLayoutAnchor, SnapKit - 我们都做。

标签: swift uiscrollview autolayout


【解决方案1】:

好的 - 一种方法...

从 iOS 11 开始(我假设您不需要更早的定位),UIScrollView 的子视图可以限制为滚动视图的 Frame Layout Guide。这使得将非滚动 UI 元素添加到滚动视图层次结构变得很容易。

基于此层次结构:

- view
    - scrollView
        - contentView
            - element1
            - element2
            - element3
            - UILayoutGuide
        - footerView

我们要做的是:

  • 将所有“可滚动”元素添加到 contentView
  • 加号在 contentView 中添加一个UILayoutGuide,作为或“底部”可滚动元素
  • 将 footerView 添加到 scrollView last,使其位于 z 顺序的顶部
  • 将footerView 约束到scrollView 的Frame Layout Guide,使其保持不动
  • 约束UILayoutGuide的heightAnchor等于footerView的heightAnchor

因为UILayoutGuide 是一个非渲染视图,所以它是不可见的,但它会创建从我们最后一个 viewable 元素底部到 contentView 底部的空间——并且如果/当footerView改变高度时,它会自动改变高度。

这是一个完整的例子——scrollView / contentView / 3 imageViews / layout guide / translucent footerView:

class ExampleViewController: UIViewController {

    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.backgroundColor = .lightGray
        return v
    }()

    let contentView: UIView = {
        let v = UIView()
        v.backgroundColor = .cyan
        return v
    }()

    let footerView: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.textColor = .white
        v.font = UIFont.systemFont(ofSize: 24.0, weight: .bold)
        v.text = "Footer View"
        v.backgroundColor = UIColor.black.withAlphaComponent(0.65)
        return v
    }()

    var imgView1: UIImageView = {
        let v = UIImageView()
        v.backgroundColor = .red
        v.image = UIImage(systemName: "1.circle")
        v.tintColor = .white
        return v
    }()

    var imgView2: UIImageView = {
        let v = UIImageView()
        v.backgroundColor = .green
        v.image = UIImage(systemName: "2.circle")
        v.tintColor = .white
        return v
    }()

    var imgView3: UIImageView = {
        let v = UIImageView()
        v.backgroundColor = .blue
        v.image = UIImage(systemName: "3.circle")
        v.tintColor = .white
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        // add 3 image views as the content we want to see
        contentView.addSubview(imgView1)
        contentView.addSubview(imgView2)
        contentView.addSubview(imgView3)

        // add contentView to srollView
        scrollView.addSubview(contentView)

        // add footer view to scrollView last so it's at the top of the z-order
        scrollView.addSubview(footerView)

        view.addSubview(scrollView)

        [scrollView, contentView, footerView, imgView1, imgView2, imgView3].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        // "spacer" for bottom of scroll content
        //  we'll constrain it to the height of the footer view
        let spacerGuide = UILayoutGuide()
        contentView.addLayoutGuide(spacerGuide)

        let g = view.safeAreaLayoutGuide
        let svCLG = scrollView.contentLayoutGuide
        let scFLG = scrollView.frameLayoutGuide

        NSLayoutConstraint.activate([

            // constrain scrollView view 40-pts on all 4 sides to view (safe-area)
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),

            // contentView view 0-pts top / leading / trailing / bottom to scrollView contentLayoutGuide
            contentView.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
            contentView.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
            contentView.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
            contentView.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),

            // contentView width == scrollView frameLayoutGuide width
            contentView.widthAnchor.constraint(equalTo: scFLG.widthAnchor, constant: 0.0),


            // imgView1 to top of contentView
            imgView1.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),

            // imgView1 width / height
            imgView1.widthAnchor.constraint(equalToConstant: 240.0),
            imgView1.heightAnchor.constraint(equalToConstant: 240.0),

            // imgView1 centerX to contentView centerX
            imgView1.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),


            // imgView2 top to bottom of imgView1 + 20-pt spacing
            imgView2.topAnchor.constraint(equalTo: imgView1.bottomAnchor, constant: 20.0),

            // imgView2 width / height
            imgView2.widthAnchor.constraint(equalToConstant: 200.0),
            imgView2.heightAnchor.constraint(equalToConstant: 280.0),

            // imgView2 centerX to contentView centerX
            imgView2.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),


            // imgView3 top to bottom of imgView2 + 20-pt spacing
            imgView3.topAnchor.constraint(equalTo: imgView2.bottomAnchor, constant: 20.0),

            // imgView3 width / height
            imgView3.widthAnchor.constraint(equalToConstant: 280.0),
            imgView3.heightAnchor.constraint(equalToConstant: 320.0),

            // imgView3 centerX to contentView centerX
            imgView3.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),


            // spacerGuide top to bottom of actual content
            // spacerGuide top to imgView3 bottom
            spacerGuide.topAnchor.constraint(equalTo: imgView3.bottomAnchor, constant: 0.0),

            // spacerGuide to leading / trailing / bottom of contentView
            spacerGuide.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0),
            spacerGuide.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0),
            spacerGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),

            // footerView to leading / trailing / bottom of scrollView frameLayoutGuide
            //  (constrained to frameLayoutGuide so it won't scroll)
            footerView.leadingAnchor.constraint(equalTo: scFLG.leadingAnchor, constant: 0.0),
            footerView.trailingAnchor.constraint(equalTo: scFLG.trailingAnchor, constant: 0.0),
            footerView.bottomAnchor.constraint(equalTo: scFLG.bottomAnchor, constant: 0.0),

            // footerView height == scrollView height with 0.25 multiplier
            //  (so it will change height when scrollView changes height, such as device rotation)
            footerView.heightAnchor.constraint(equalTo: scFLG.heightAnchor, multiplier: 0.25),

            // finally, spacerGuide height equal to footerView height
            spacerGuide.heightAnchor.constraint(equalTo: footerView.heightAnchor),

        ])

    }
}

结果:

滚动到底部:

并旋转(所以我们看到 footerView 高度变化)一直滚动到底部:


编辑

具体问题的答案是:你不能。

滚动视图的contentInset 不是可以添加约束的对象...它是滚动视图的属性。就像您不能将滚动视图的 .backgroundColor 约束为自动布局约束一样。

【讨论】:

  • 很好的解决方法!惊人的想法 - 将虚拟视图或布局指南嵌入到 contentInset 应该在的位置,并使其高度与footer 同步。干得好,我真的希望无论你在哪里工作,他们都能给你很好的报酬! @DonMag 可悲的是,我暂时不会将此答案标记为正确,因为它没有回答我的问题。它围绕 contentInset 的使用工作,但问题是如何准确地使用 contentInset 并通过自动布局管理它。
  • @IsaacCarolWeisberg - 我添加了一个 Edit 来回答 specific 问题。我可能应该首先澄清这一点。
  • 是的,我知道了。在我自己做了很少的研究之后,我只会将您的答案标记为正确,因为很有可能是这样。
猜你喜欢
  • 1970-01-01
  • 2019-04-08
  • 1970-01-01
  • 1970-01-01
  • 2019-06-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-14
相关资源
最近更新 更多