【问题标题】:iOS: UIScrollView with dynamic content using ContainerView (step by step)iOS:使用 ContainerView 动态内容的 UIScrollView(一步一步)
【发布时间】:2017-07-29 19:17:17
【问题描述】:

即使这里有多个问题和答案,我也无法创建包含静态和动态内容的UIScrollView(通过使用ContainerView)并使尺寸正常工作。因此,我将提供分步指南,直到我无法取得任何进展并且有人可以提供解决方案。这样我们就有了一个可行的示例,可以一步一步地进行操作。

请注意:为方便起见,所有步骤的输出都上传到https://github.com/oysteinmyrmo/DynamicScrollablePage。可以从那里获取测试 Xcode 项目并进一步破解。

更新:在@agibson007 的回答之后,最后有几个步骤可以将原始步骤修复为有效的解决方案。通过声明ERROR,SEE FINAL STEPS来记录错误。

目标:

拥有一个可滚动的长 UIView 页面,其中包含各种静态 UIViews 和一个带有动态内容的 ContainerView。为了完整起见,动态内容将包含一些静态的UIViews 和一个UITableView,它们将扩展到其全部内容。最后一个元素似乎是我最近偶然发现的各种问题中反复出现的主题。

方法:

  1. 我们将从一个新的 Xcode 项目 (Xcode 8.2.1) 开始,并使用 Swift 3.0.2 作为语言。
  2. 我们将逐步创建测试UIViews、UIViewControllers 和其他必填项。
  3. 在某些时候,我们有一个“模板”,可以让有能力的人动态扩展内容。

第 1 步:创建项目

打开 Xcode (8.2.1),开始一个新项目。

  1. 选择选项卡式应用程序。我们将在第一个选项卡中创建UIScrollView
  2. 将产品名称设置为 DynamicScrollablePage。
  3. 选择位置并创建项目。

第 2 步:项目的初始更改

对 UI 的更改将在第一个选项卡中完成。该过程受this answer 的影响很大,但我们将添加更多项目和ContainerView 用于我们的动态内容。

  1. Main.storyboardFirst View(即标签1)中删除两个标签。
  2. 单击UIViewController(首先命名)。转到它的尺寸检查器,从 Fixed 更改为 Freeform 并将高度更改为 1500。这只是情节提要中的视觉更改。
  3. 将剩余的UIView 重命名为RootView
  4. RootView 内添加UIScrollView。将其命名为ScrollView。约束:
    • ScrollView[顶部、底部、前导、尾随、宽度] = RootView[顶部、底部、前导、尾随、宽度]。 根据我的经验,还必须设置宽度限制以确保稍后覆盖整个屏幕。
  5. ScrollView 中添加UIView 并将其命名为ContentView。约束:
    • ContentView[Leading, Trailing, Top, Bottom, Width] = ScrollView[Leading, Trailing, Top, Bottom, Width]。故事板现在将抱怨滚动高度。完成以下步骤后它不会抱怨。
  6. 将项目添加到ContentView
    • 首先添加一个UIView,命名为RedView。设置RedView[前导、尾随、顶部] = ContentView[前导、尾随、顶部]。设置RedView[高度,背景颜色] = [150,红色]。
    • RedView 下添加UILabel,将其名称/文本设置为FirstLabel。设置FirstLabel[前导,尾随] = ContentView[前导,尾随]。设置FirstLabel[Top] = RedView[Bottom]。
    • FirstLabel下方添加UIView,命名为BlueView。设置BlueView[前导,尾随] = ContentView[前导,尾随]。设置BlueView[顶部] = FirstLabel[底部]。设置BlueView[高度,背景颜色] = [450,蓝色]。
    • BlueView 下方添加UILabel,将其名称/文本设置为SecondLabel。设置SecondLabel[前导,尾随] = ContentView[前导,尾随]。设置SecondLabel[Top] = BlueView[Bottom]。
    • SecondLabel下方添加UIContainerView,命名为ContainerView。设置ContainerView[前导,尾随] = ContentView[前导,尾随]。设置ContainerView[顶部] = SecondLabel[底部]。设置 ContainerView[Intrinsic size] = [Placeholder](请参阅尺寸检查器了解 ContainerView)。 将固有大小设置为占位符告诉 Xcode 它的大小由其子视图定义(据我所知)。 错误,请参阅最终步骤
    • 在末尾添加UILabel,命名为BottomLabel。设置BottomLabel[前导,尾随] = ContentView[前导,尾随]。设置BottomView[Top] = ContainerView[Bottom]。
    • 最后,控制并从ScrollView拖动到BottomView并选择Bottom Space to ScrollView。这将确保ScrollView 的高度正确。

第 3 步:创建具有动态内容的 ViewController

现在我们将创建实际的UIViewControllerxib 文件,用于显示动态内容。我们将在xib 中创建一个UITableView,因此为了简单起见,我们还需要一个带有简单标签的UITableViewCell

  1. 创建一个 Swift 文件,TableViewCell.swift,内容如下:

    import UIKit
    
    class TableViewCell : UITableViewCell {
    }
    
  2. 创建一个xib/View 文件,命名为TableViewCell.xib。执行以下操作:

    • 删除默认的UIView 并将其替换为UITableViewCell
    • 向该单元格添加UILabel,将其命名为DataLabel(它还会添加内容视图UIView)。
    • UITableViewCell 的自定义类设置为TableViewCell
    • 将表格视图单元格标识符设置为TableViewCellId
    • 在双视图模式下,ctrl+拖动标签到TableViewCell类。结果应该是:

      import UIKit
      
      class TableViewCell : UITableViewCell {
          @IBOutlet weak var dataLabel: UILabel!
      }
      
  3. 使用内容创建一个文件DynamicEmbeddedViewController.swift

    import UIKit
    
    class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
    {
        @IBOutlet weak var tableView: UITableView!
        let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]
    
        override func viewDidLoad() {
            super.viewDidLoad()
            tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
        }
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return data.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
            cell.dataLabel.text = data[indexPath.row]
            return cell
        }
    }
    
  4. 创建一个xib/View 文件,命名为DynamicEmbeddedView.xib。将主要的UIView 重命名为ContentView 并在ContentView 中添加三个项目:

    • 添加UIView,将其命名为GreenView。设置GreenView[前导、尾随、顶部] = ContentView[前导、尾随、顶部]。设置GreenView[高度] = [150]。
    • 添加UITableView,将其命名为TableView。设置TableView[前导,尾随] = ContentView[前导,尾随]。设置TableView[顶部] = GreenView[底部]。设置内在大小 = 占位符。 我不确定这是否是正确的方法错误,查看最后步骤
    • TableView下方添加UIView,命名为PurpleView。设置PurpleView[前导,尾随] = ContentView[前导,尾随]。设置PurpleView[Top] = TableView[Bottom]。
    • 注意:此时我们可能需要在 xib 中添加更多约束,但我不确定是什么以及如何进行(如果有的话)。
    • File's Owner 的自定义类设置为DynamicEmbeddedViewController
    • File's Owner的View outlet设置为ContainerView
    • 设置TableView的数据源并委托给File's Owner
    • TableViewIBOutlet 添加到DynamicEmbeddedViewController 类中。
  5. Main.storyboard中连接创建的xibUIViewController

    • ContainerView的输出视图控制器的自定义类设置为DynamicEmbeddedViewController
    • 删除ContainerViews 输出视图控制器中现有的View我不确定这是否真的需要。

现状图片:

第 4 步:运行应用:

运行应用程序并一直滚动到底部,包括反弹区域,结果如下:

由此我们可以得出结论:

  • ContainerView 的位置是正确的(即在SecondLabelBottomLabel 之间),但BottomLabel 不遵守其低于ContainerView 的约束。
  • TableView的高度明显为0。这也可以看出,因为func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)没有被调用。如果我们在TableView 上设置高度限制,就会显示项目。
  • 如果ContainerView 的大小增加,ScrollView 的内容大小不会增加。
  • 还希望始终显示动态TableView 中的所有 项,然后滚动它们,就好像它们只是ScrollView 中的静态数据一样。
  • 这东西真是乱七八糟。

第 5 步:问题!

  • 我们怎样才能让ScrollView的内容正确地包裹所有的内容,包括TableView中的动态数据在ContainerView里面?
  • 约束设置是否正确?
  • 我们应该在哪里以及如何计算合适的高度/内容大小?
  • 这一切真的有必要吗?有没有更简单的方法来实现这一点?

第 6 步:在@agibson007 的回答之后修复解决方案:

  • 像这样添加static let CELL_HEIGHT = 44

    import UIKit
    
    class TableViewCell : UITableViewCell {
        @IBOutlet weak var dataLabel: UILabel!
        static let CELL_HEIGHT = 44
    }
    
  • TableView 的固有大小从Placeholder 还原为Default

  • TableView 上设置例如150 的高度约束。该值必须大于一个单元格的高度。
  • 将高度约束添加到DynamicEmbeddedViewController 作为IBOutlet
  • 添加代码以计算和​​设置TableView 高度约束。最后一课:

    import UIKit
    
    class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
    {
        @IBOutlet weak var tableView: UITableView!
        @IBOutlet weak var tableViewHeight: NSLayoutConstraint!
    
        let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]
    
        override func viewDidLoad() {
            super.viewDidLoad()
            tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
    
            // Resize our constraint
            let totalHeight = data.count * TableViewCell.CELL_HEIGHT
            tableViewHeight.constant = CGFloat(totalHeight)
            self.updateViewConstraints()
            //in a real app a delegate call back would be good to update the constraint on the scrollview
        }
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return data.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
            cell.dataLabel.text = data[indexPath.row]
            return cell
        }
    
    }
    
  • ContainerView 的固有大小从Placeholder 还原为Default

  • ContainerView 上设置例如150 的高度约束。此值将在代码中更新。
  • 将高度约束添加到ContainerView,作为FirstViewController 中的IBOutlet
  • FirstViewController 中添加ContainerView 作为IBOutlet
  • FirstViewController 中创建对DynamicEmbeddedViewController 的引用,以便在计算高度时引用它。
  • 添加代码以计算和​​设置ContainerView 高度约束。最后FirstViewController类:

    import UIKit
    
    class FirstViewController: UIViewController {
    
        @IBOutlet weak var containerView: UIView!
        @IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!
        var dynamicView: DynamicEmbeddedViewController?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
            if dynamicView != nil{
                dynamicView?.tableView.reloadData()
                let size = dynamicView?.tableView.contentSize.height
                //cheating on the 300 because the other views in that controller at 150 each
                containerViewHeightConstraint.constant = size! + 300
                self.view.updateConstraintsIfNeeded()
            }
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if (segue.identifier == "ContainerViewSegue") {
                dynamicView = segue.destination as? DynamicEmbeddedViewController
            }
        }
    

    }

最后一切都按预期进行!

请注意:为方便起见,所有步骤的输出都上传到https://github.com/oysteinmyrmo/DynamicScrollablePage。可以从那里获取测试 Xcode 项目并进一步破解。

【问题讨论】:

    标签: ios uitableview uiview uiscrollview


    【解决方案1】:

    我确信这个问题之前已经回答过了,但我不记得是否已经回答了为什么人们在滚动视图上遇到这么多麻烦。它归结为关于 UIScrollView 和 Autolayout 的 2 件事。

    1) 滚动视图需要能够计算内部内容的宽度和高度。这有助于确定它是否真的需要滚动。

    2) 一些视图的大小取决于里面的内容。示例 UILabel 具有“固有”内容大小。这意味着如果您将其拖到情节提要上,则无需设置高度或宽度,除非您试图以某种方式对其进行约束。其他具有固有尺寸的视图示例是 UIButton、UIImageView(如果它有图像)、UITextView(禁用滚动)和其他可能包含文本或图像的控件。

    所以让我们开始非常简单,将 UIScrollView 拖到故事板上并将其固定到超级视图。一切都很好,没有警告。

    现在将 UILabel 拖到滚动视图上并快速查看警告。 ScrollView 模糊的可滚动内容(宽度和高度)。

    现在为所有人添加一个顶部、底部、前导和尾随 20。

    没有警告。

    测试 2) 删除 UILabel 并将 UIView 拖到滚动视图上并添加顶部、底部、前导和尾随,例如 20。

    警告!警告!警告!

    问题是 UIView 无法自行调整大小,并且滚动视图不知道其内容有多大,因此无法设置。


    如果这有意义 == 真 继续 别的 返回

    现在根据我们的进展情况,它会变得更加复杂,但上述概念支配着整个过程。

    调查您的项目和设置,您在学习 UIScrollview 方面做得很好。

    现在让我们回顾一下你的总结,我会就一些事情发表评论。

    根据您上面的报价

    “由此我们可以得出结论: ContainerView 的位置是正确的(即在 SecondLabel 和 BottomLabel 之间),但 BottomLabel 没有遵守其低于 ContainerView 的约束。"

    ***-> 你这里的总结其实是不正确的。转到标签上方的容器并在界面生成器中选中剪辑到边界并重新运行项目,标签将在底部,但不会有绿色视图。为什么?它不知道它应该有多大。当你在它加载之前运行它时,它可以并且 uilabels 是清晰的,所以当它超出它的边界时它看起来不正确。

    "TableView的高度明显是0,这也是因为没有调用func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath),如果我们给TableView设置了高度约束,item就会出现。

    ***-> 这是最难处理的。您必须加载 tableview 并获取 tableview 内容大小并在代码中更新 tableview 的高度约束以使其更新,以便滚动视图调整容纳控制器的容器的大小。

    如果它包含的内容具有内在内容大小,则另一种方法是可以确定其高度和宽度的堆栈视图。

    但回到表格视图。您需要在 tableview//您的行高上设置 >= 40 才能从动态视图开始。如果计数为 0,则检查数据源后,您会将约束更新为 0 并使用委托让滚动视图知道更新它对动态视图控制器的约束以不显示表。我希望这是有道理的。然后相反,如果计数是数据源中的 10 个项目,则将 dynamicviewcontroller tableview 高度约束的约束更新为 10 * 40,就像这样

    import UIKit
    
        class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
        {
            @IBOutlet weak var tableViewConstraint: NSLayoutConstraint!
            @IBOutlet weak var tableView: UITableView!
    
            let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]
    
            override func viewDidLoad() {
                super.viewDidLoad()
                tableView.delegate = self
                tableView.dataSource = self
                tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
    
                //resize our constraint
                let count = data.count
                let constant = count * 40
                tableViewConstraint.constant = CGFloat(constant)
                self.updateViewConstraints()
               //in a real app a delegate call back would be good to update the    constraint on the scrollview
            }
    

    在第一个控制器中它看起来像这样。

    import UIKit
    
    class FirstViewController: UIViewController {
    
        @IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!
        @IBOutlet weak var containerView: UIView!
        @IBOutlet weak var scrollView: UIScrollView!
        var dynamicView : DynamicEmbeddedViewController?
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
            if dynamicView != nil{
                dynamicView?.tableView.reloadData()
                //get size 
                let size = dynamicView?.tableView.contentSize.height
                //cheating on the 300 because i see you set the other views in that controller at 150 each
                containerViewHeightConstraint.constant = size! + 300
                self.view.updateConstraintsIfNeeded()
            }
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if (segue.identifier == "ContainerViewSegue") {
                dynamicView = segue.destination as? DynamicEmbeddedViewController
            }
        }
    
    }
    

    你可以看到我们调整了所有东西的大小以适应。除此之外,你做对了。现在到生产有价值的代码。大多数时候,您会知道将显示哪些内容,以便您可以控制它。另一种方法是对所有内容使用 UITableView 或 UICollectionView,并根据内容加载不同的单元格。我希望这篇文章能澄清一点。我相信我们可以继续添加它,但希望所涵盖的概念就足够了。如果这回答了您的问题,请对这样做所花费的时间表示一些爱。如果你愿意,我也可以将它上传到 GitHub,但最好放在你开始的 repo 中。

    【讨论】:

    • 我不得不说,在被这个问题困扰了几个星期之后,我有点期望解决方案像这样简单。互联网上流传的各种解决方案正在做各种对我不起作用的事情。所以这就是我要做的:1)将您的问题标记为答案,2)逐步更新您的步骤并更新 git repo 和 3)开始并奖励您花时间帮助我的赏金(和其他人)与此。我知道这不是在几分钟内完成的。我本来打算做赏金的,但你太快了! :-)
    • 在您完成这些步骤后,如果您需要一份我所做的副本,请告诉我。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-27
    • 1970-01-01
    • 2020-12-22
    • 2011-04-20
    • 2021-04-02
    • 2018-08-06
    • 1970-01-01
    相关资源
    最近更新 更多