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
将被扩展到它的全部内容。 最后一个因素似乎是我最近偶然发现的各种问题的一个重复的主题。
方法:
- 我们将从一个新的Xcode项目(Xcode 8.2.1)开始,并使用Swift 3.0.2作为语言。
- 我们将一步一步的创buildtesting
UIView
,UIViewController
和其他必需的项目。 - 在某些时候,我们有一个“模板”,可以用来使内容能够dynamic扩展的内容。
第1步:项目创build
打开Xcode(8.2.1),开始一个新的项目。
- select选项卡式应用程序 我们将在第一个选项卡中创build
UIScrollView
。 - 将产品名称设置为DynamicScrollablePage。
- select位置并创build项目。
步骤2:对项目进行初始更改
UI的更改将在第一个选项卡中完成。 该过程深受这个答案的影响,但我们将添加一些更多的项目和一个ContainerView
为我们的dynamic内容。
- 在
Main.storyboard
,First View
(即选项卡1)删除这两个标签。 - 单击
UIViewController
(名为第一个)。 转到其大小检查器,从Fixed
更改为Freeform
,并将高度更改为1500.这只是故事板中的视觉更改。 - 将其余的
UIView
重命名为RootView
。 - 在
RootView
添加一个UIScrollView
。 将其命名为ScrollView
。 约束:- ScrollView [顶部,底部,引导,尾随,宽度] = RootView [顶部,底部,引导,尾随,宽度]。 根据我的经验,还必须设置宽度约束,以确保以后覆盖整个屏幕。
- 在
ScrollView
添加一个UIView
,并命名为ContentView
。 约束:- ContentView [领先,尾随,顶部,底部,宽度] = 滚动视图 [领先,尾随,顶部,底部,宽度]。 故事板现在会抱怨滚动高度。 下面的步骤后,它不会抱怨。
- 将项目添加到
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内容的实际UIViewController
和xib
文件。 我们将在xib
创build一个UITableView
,因此为了简单起见,我们还需要一个带有简单标签的UITableViewCell
。
-
创build一个Swift文件,内容为
TableViewCell.swift
:import UIKit class TableViewCell : UITableViewCell { }
-
创build一个名为
TableViewCell.xib
的xib
/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! }
- 删除默认的
-
使用以下内容创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 } }
-
创build一个名为
DynamicEmbeddedView.xib
的xib
/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
。 - 将
TableView
的IBOutlet
TableView
到DynamicEmbeddedViewController
类。
- 添加一个
-
在
xib
连接创build的xib
和UIViewController
。- 将
ContainerView
的输出视图控制器的自定义类设置为DynamicEmbeddedViewController
。 - 删除
ContainerViews
输出视图控制器中的现有View
。 我不确定这是否真的需要。
- 将
当前形势的图片:
第4步:运行应用程序:
运行应用程序,并一直滚动到底部,包括反弹区域,这是结果:
由此我们可以得出结论:
-
ContainerView
的位置是正确的(即在SecondLabel
和BottomLabel
之间),但BottomLabel
不遵守它的约束在ContainerView
之下。 -
TableView
的高度显然是0.这也可以看出,因为没有func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
。 如果我们在TableView
上设置高度约束,项目将显示出来。 - 如果
ContainerView
的大小增加,则ScrollView
的内容大小不会增加。 - 也希望在dynamic
TableView
中始终显示所有项目,只需滚动它们就好像它们只是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含糊不清的可滚动内容(宽度和高度)。
现在添加一个顶部,底部,领先和尾随20。
没有警告。
testing2)删除UILabel并拖动一个UIView到滚动视图上,并添加顶部,底部,引导和尾部说20。
警告! 警告! 警告!
问题是,一个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上,但是最好在你开始的回购中进行。