【发布时间】:2017-07-29 19:17:17
【问题描述】:
即使这里有多个问题和答案,我也无法创建包含静态和动态内容的UIScrollView(通过使用ContainerView)并使尺寸正常工作。因此,我将提供分步指南,直到我无法取得任何进展并且有人可以提供解决方案。这样我们就有了一个可行的示例,可以一步一步地进行操作。
请注意:为方便起见,所有步骤的输出都上传到https://github.com/oysteinmyrmo/DynamicScrollablePage。可以从那里获取测试 Xcode 项目并进一步破解。
更新:在@agibson007 的回答之后,最后有几个步骤可以将原始步骤修复为有效的解决方案。通过声明ERROR,SEE FINAL STEPS来记录错误。
目标:
拥有一个可滚动的长 UIView 页面,其中包含各种静态 UIViews 和一个带有动态内容的 ContainerView。为了完整起见,动态内容将包含一些静态的UIViews 和一个UITableView,它们将扩展到其全部内容。最后一个元素似乎是我最近偶然发现的各种问题中反复出现的主题。
方法:
- 我们将从一个新的 Xcode 项目 (Xcode 8.2.1) 开始,并使用 Swift 3.0.2 作为语言。
- 我们将逐步创建测试
UIViews、UIViewControllers 和其他必填项。 - 在某些时候,我们有一个“模板”,可以让有能力的人动态扩展内容。
第 1 步:创建项目
打开 Xcode (8.2.1),开始一个新项目。
- 选择选项卡式应用程序。我们将在第一个选项卡中创建
UIScrollView。 - 将产品名称设置为 DynamicScrollablePage。
- 选择位置并创建项目。
第 2 步:项目的初始更改
对 UI 的更改将在第一个选项卡中完成。该过程受this answer 的影响很大,但我们将添加更多项目和ContainerView 用于我们的动态内容。
- 在
Main.storyboard、First View(即标签1)中删除两个标签。 - 单击
UIViewController(首先命名)。转到它的尺寸检查器,从Fixed更改为Freeform并将高度更改为 1500。这只是情节提要中的视觉更改。 - 将剩余的
UIView重命名为RootView。 - 在
RootView内添加UIScrollView。将其命名为ScrollView。约束:- ScrollView[顶部、底部、前导、尾随、宽度] = RootView[顶部、底部、前导、尾随、宽度]。 根据我的经验,还必须设置宽度限制以确保稍后覆盖整个屏幕。
- 在
ScrollView中添加UIView并将其命名为ContentView。约束:- ContentView[Leading, Trailing, Top, Bottom, Width] = ScrollView[Leading, Trailing, Top, Bottom, Width]。故事板现在将抱怨滚动高度。完成以下步骤后它不会抱怨。
- 将项目添加到
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
现在我们将创建实际的UIViewController 和xib 文件,用于显示动态内容。我们将在xib 中创建一个UITableView,因此为了简单起见,我们还需要一个带有简单标签的UITableViewCell。
-
创建一个 Swift 文件,
TableViewCell.swift,内容如下:import UIKit class TableViewCell : UITableViewCell { } -
创建一个
xib/View文件,命名为TableViewCell.xib。执行以下操作:- 删除默认的
UIView并将其替换为UITableViewCell。 - 向该单元格添加
UILabel,将其命名为DataLabel(它还会添加内容视图UIView)。 - 将
UITableViewCell的自定义类设置为TableViewCell。 - 将表格视图单元格标识符设置为
TableViewCellId。 -
在双视图模式下,ctrl+拖动标签到
TableViewCell类。结果应该是:import UIKit class TableViewCell : UITableViewCell { @IBOutlet weak var dataLabel: UILabel! }
- 删除默认的
-
使用内容创建一个文件
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 } } -
创建一个
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。 - 将
TableView的IBOutlet添加到DynamicEmbeddedViewController类中。
- 添加
-
在
Main.storyboard中连接创建的xib和UIViewController。- 将
ContainerView的输出视图控制器的自定义类设置为DynamicEmbeddedViewController。 - 删除
ContainerViews输出视图控制器中现有的View。 我不确定这是否真的需要。
- 将
现状图片:
第 4 步:运行应用:
运行应用程序并一直滚动到底部,包括反弹区域,结果如下:
由此我们可以得出结论:
-
ContainerView的位置是正确的(即在SecondLabel和BottomLabel之间),但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