iOS:使用ContainerView的dynamic内容的UIScrollView(一步一步)

即使有这个问题的多个问题和答案,所以我不能创build一个UIScrollView同时具有静态和dynamic内容(通过使用一个ContainerView ),并使大小正常工作。 因此,我将提供一步一步的指导,直到我无法取得进展,有人可以提供解决scheme。 这样我们将有一个可行的样本,可以一步一步地使其工作。

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

更新:在@ agibson007的答案后,最后有几个步骤将原始步骤修复为工作解决scheme。 错误是指出错误,看最后的步骤

目标:

有一个很长的可滚动的UIView页面,包含各种静态的UIView和一个带有dynamic内容的ContainerView 。 为了完整起见,dynamic内容将由一些静态的UIView和一个UITableView组成,这个UITableView将被扩展到它的全部内容。 最后一个因素似乎是我最近偶然发现的各种问题的一个重复的主题。

方法:

  1. 我们将从一个新的Xcode项目(Xcode 8.2.1)开始,并使用Swift 3.0.2作为语言。
  2. 我们将一步一步的创buildtestingUIViewUIViewController和其他必需的项目。
  3. 在某些时候,我们有一个“模板”,可以用来使内容能够dynamic扩展的内容。

第1步:项目创build

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

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

步骤2:对项目进行初始更改

UI的更改将在第一个选项卡中完成。 该过程深受这个答案的影响,但我们将添加一些更多的项目和一个ContainerView为我们的dynamic内容。

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

第3步:使用dynamic内容创build一个ViewController

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

  1. 创build一个Swift文件,内容为TableViewCell.swift

     import UIKit class TableViewCell : UITableViewCell { } 
  2. 创build一个名为TableViewCell.xibxib / View文件。 请执行下列操作:

    • 删除默认的UIView并将其replace为UITableViewCell
    • UILabel添加到该单元格,将其命名为DataLabel (它也将添加一个内容视图UIView )。
    • UITableViewCell的自定义类设置为TableViewCell
    • 将Table View Cell标识符设置为TableViewCellId
    • 在双视图模式下,按Ctrl +拖动标签到TableViewCell类。 结果应该是:

       import UIKit class TableViewCell : UITableViewCell { @IBOutlet weak var dataLabel: UILabel! } 
  3. 使用以下内容创build一个文件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. 创build一个名为DynamicEmbeddedView.xibxib / View文件。 将主UIView重命名为ContentView并在ContentView添加三个项目:

    • 添加一个UIView ,将其命名为GreenView 。 设置GreenView [Leading,Trailing,Top] = ContentView [Leading,Trailing,Top]。 设置GreenView [Height] = [150]。
    • 添加一个UITableView ,将其命名为TableView 。 设置TableView [Leading,Trailing] = ContentView [Leading,Trailing]。 设置TableView [Top] = GreenView [Bottom]。 设置固有大小=占位符。 我不确定这是否是正确的方法错误,看最后的步骤
    • TableView下面添加一个UIView ,将其命名为PurpleView 。 设置PurpleView [Leading,Trailing] = ContentView [Leading,Trailing]。 设置PurpleView [Top] = TableView [Bottom]。
    • 注意:在这一点上,我们可能需要更多的xib约束,但我不确定什么和如何,如果有的话。
    • File's Owner的自定义类设置为DynamicEmbeddedViewController
    • File's Owner的视图出口设置为ContainerView
    • 设置TableView的dataSource和委托给File's Owner
    • TableViewIBOutlet TableViewDynamicEmbeddedViewController类。
  5. xib连接创build的xibUIViewController

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

当前形势的图片:

Main.storyboard

DynamicEmbeddedView.xib

第4步:运行应用程序:

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

在这里输入图像说明

由此我们可以得出结论:

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

第5步:问题!

  • 我们怎样才能使ScrollView的内容正确地包装所有的内容,包括生活在ContainerView中的TableView的dynamic数据?
  • 限制是否正确?
  • 在哪里以及如何计算适当的高度/内容大小?
  • 这一切都是非常必要的。 有没有更容易的方法来实现这一点?

第6步:解决@ agibson007的答案后的解决scheme:

  • 添加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的高度约束。 该值必须大于一个单元格的高度。
  • 将高度约束作为IBOutlet添加到DynamicEmbeddedViewController
  • 添加代码来计算并设置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的高度约束。 该值将在代码中更新。
  • FirstViewController中将高度约束作为IBOutlet添加到ContainerView中。
  • FirstViewController ContainerView添加为IBOutlet
  • FirstViewController创build对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 。 testingXcode项目可以从那里获取,并进一步黑客入侵。

我确信之前已经回答了这个问题,但是我不记得是否回答了为什么人们对scrollview有太多的麻烦。 关于UIScrollView和Autolayout,你必须知道两件事情。

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

2)一些视图有一个基于内容的大小。 示例UILabel具有“内部”内容大小。 这意味着,如果您将其拖出到故事板上,则无需设置高度或宽度,除非您试图对其进行限制。 具有内在大小的视图的其他例子将是一个UIButton,UIImageView(如果它有一个图像),UITextView(禁用滚动)和其他可能有文本或图像的控件。

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

现在把一个UILabel拖到滚动视图上,并在警告处做一个快速的高峰。 ScrollView含糊不清的可滚动内容(宽度和高度)。

UILabelNoConstraints

现在添加一个顶部,底部,领先和尾随20。

没有警告。

UILabelConstraints

testing2)删除UILabel并拖动一个UIView到滚动视图上,并添加顶部,底部,引导和尾部说20。

警告! 警告! 警告!

UIViewWarnings

问题是,一个UIView没有能力调整自己的大小,滚动视图不知道它的内容有多大,所以它不能设置。


如果这使得SENSE == True继续ELSE goBack

现在,这里将变得更加复杂,取决于我们走多远,但上面的概念支配整个过程。

调查你的项目和设置,你很好地学习UIScrollview。

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

从上面的报价

“从这里我们可以得出结论:ContainerView的位置是正确的(即在SecondLabel和BottomLabel之间),但是BottomLabel并没有将它的约束限制在ContainerView的下面。

*** – >你总结这里其实是不正确的。 转到标签上方的容器,在界面生成器中勾选剪辑到界限,然后重新运行项目,标签将位于底部,但不会显示绿色视图。 为什么? 它不知道它应该有多大。 当它在最好的状态下运行时,它可能和uilabels是清楚的,所以当它超出界限时,它看起来是不正确的。

“TableView的高度显然是0.这也可以看出,因为func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)没有被调用,如果我们在TableView上设置高度约束,项目会显示出来。

*** – >这是最棘手的处理。 你将不得不加载tableview,并得到tableview的内容大小,并在代码中更新桌面视图的高度约束,以获得更新的滚动视图来调整持有控制器的容器的大小。

另一种select是一个stackview,它可以确定它的高度和宽度,如果它拥有的内容具有内在的内容大小。

但是回到tableview。 你需要设置一个> = 40的tableview //你的行高从dynamic视图开始。 如果计数为0,则检查数据源之后,将约束更新为0,并使用委托让scrollview知道更新它对dynamicviewcontroller的约束,以不显示表。 我希望这是有道理的。 然后相反,如果计数是说数据源中的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,并根据内容加载不同的单元格。 我希望这个post清楚一点。 我相信我们可以继续补充,但希望涵盖的概念是足够的。 如果这回答你的问题,请显示一些爱的时间来做到这一点。 如果你愿意的话,我也可以把它上传到GitHub上,但是最好在你开始的回购中进行。