哈Ha! 我向您介绍Aly Yaka
撰写的文章《
使用PureLayout以
编程方式创建UI元素》的翻译。

欢迎使用有关使用PureLayout以编程方式创建接口的文章的第二部分。
在第一部分中,我们完全用代码创建了一个简单的移动应用程序的用户界面,而无需使用Storyboards或NIB。 在本指南中,我们将介绍所有应用程序中一些最常用的用户界面元素:
- UINavigationController /栏
- UITableView
- 自定义UITableViewCell
UINavigationController
在我们的应用程序中,您可能需要一个导航栏,以便用户可以从联系人列表转到有关特定联系人的详细信息,然后返回列表。
UINavigationController
可以使用导航栏轻松解决此问题。
UINavigationController
只是一个堆栈,您可以在其中移动许多视图。 用户现在可以看到最上面的视图(上次移动的视图)(除非您在此视图的上方有另一个视图,比如说收藏夹)。 并且,当您按下导航控制器的顶视图控制器时,导航控制器会自动创建一个“后退”按钮(取决于设备当前的语言首选项,位于左上方或右侧),然后按此按钮将返回上一个视图。
所有这些都由导航控制器开箱即用地处理。 如果再添加一行,则只需要增加一行代码(如果您不想自定义导航栏)。
转到AppDelegate.swift并在下面添加以下代码行,让
viewController = ViewController ():
let navigationController = UINavigationController(rootViewController: viewController)
现在更改
self.window? .RootViewController = viewController self.window? .RootViewController = navigationController
self.window? .RootViewController = viewController self.window? .RootViewController = navigationController
self.window? .RootViewController = viewController self.window? .RootViewController = navigationController
。 在第一行中,我们创建了
UINavigationController
的实例,并将其
viewController
作为
rootViewController
传递给了它,它是堆栈最底部的视图控制器,这意味着该视图的导航栏上永远不会有后退按钮。 然后,给我们的窗口一个导航控制器,称为
rootViewController
,因为现在它将包含应用程序中的所有视图。
现在运行您的应用程序。 结果应如下所示:
不幸的是,出了点问题。 导航栏似乎与我们的upperView重叠,并且我们有几种方法可以解决此问题:
- 增大
upperView
的大小以适合导航栏的高度。 - 将导航栏的
isTranslucent
属性设置为false
。 这将使导航栏不透明(如果您没有注意到它有点透明),那么超级视图的顶部边缘将变为导航栏的底部。
我个人将选择第二个选项,但是,您将学习第一个。 我还建议检查并仔细阅读有关
UINavigationController
和
UINavigationBar
Apple文档:
现在转到viewDidLoad方法并添加以下行
self.navigationController? .NavigationBar.isTranslucent = false super.viewDidLoad ()
self.navigationController? .NavigationBar.isTranslucent = false super.viewDidLoad ()
,因此看起来像这样:
override func viewDidLoad() { super.viewDidLoad() self.navigationController?.navigationBar.isTranslucent = false self.view.backgroundColor = .white self.addSubviews() self.setupConstraints() self.view.bringSubview(toFront: avatar) self.view.setNeedsUpdateConstraints() }
您还可以将这一行
self.title = "John Doe" viewDidLoad
添加
self.title = "John Doe" viewDidLoad
,这将在导航栏中添加“个人资料”,以便用户知道他当前所在的位置。 运行应用程序,结果应如下所示:
重构我们的View Controller
在继续之前,我们需要减少
ViewController.swift
文件,
ViewController.swift
我们只能使用真实的逻辑,而不仅仅是用户界面元素的代码。 为此,我们可以创建
UIView
的子类,然后将所有用户界面元素移到那里。 我们这样做的原因是遵循Model-View-Controller或MVC架构模式。 了解有关
iOS上的 MVC
模型视图控制器(MVC)的更多信息
:一种现代方法 。
现在,右键单击Project Navigator中的
ContactCard
文件夹,然后选择“ New File”:

单击可可接触类,然后单击下一步。 现在,写“ ProfileView”作为类名,并在“ Subclass of:”旁边确保输入“ UIView”。 它只是告诉Xcode自动使我们的类继承自
UIView
,并将添加一些样板代码。 现在单击下一步,然后单击创建并删除注释掉的代码:
现在我们可以进行重构了。
将视图控制器中的所有惰性变量剪切并粘贴到我们的新视图中。
在最后一个挂起的变量下面,通过键入
init
并从Xcode中选择第一个自动完成结果来覆盖
init(frame :)
。

将出现一条错误消息,指出“必需的”初始化程序“ init(coder :)”应由“ UIView”的子类提供:

您可以通过单击红色圆圈,然后单击“修复”来解决此问题。

在任何重写的初始化程序中,几乎应该始终调用超类初始化程序,因此应在方法顶部添加以下代码行:
super.init (frame: frame)
。
在初始化程序下剪切并粘贴
addSubviews()
方法,并在每次
addSubview
调用之前删除
self.view
。
func addSubviews() { addSubview(avatar) addSubview(upperView) addSubview(segmentedControl) addSubview(editButton) }
然后从初始化程序调用此方法:
override init(frame: CGRect) { super.init(frame: frame) addSubviews() bringSubview(toFront: avatar) }
对于限制,请覆盖
updateConstraints()
并在此函数的末尾添加一个调用(它将始终保留在其中):
override func updateConstraints() {
覆盖任何方法时,通过访问Apple文档或更简单地按住Option(或Alt)键并单击函数名称来检查其文档总是有用的:

将约束代码从视图控制器中剪切并粘贴到我们的新方法中:
override func updateConstraints() { avatar.autoAlignAxis(toSuperviewAxis: .vertical) avatar.autoPinEdge(toSuperviewEdge: .top, withInset: 64.0) upperView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom) segmentedControl.autoPinEdge(toSuperviewEdge: .left, withInset: 8.0) segmentedControl.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0) segmentedControl.autoPinEdge(.top, to: .bottom, of: avatar, withOffset: 16.0) editButton.autoPinEdge(.top, to: .bottom, of: upperView, withOffset: 16.0) editButton.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0) super.updateConstraints() }
现在回到视图控制器,并通过
viewDidLoad
方法初始化
ProfileView
实例,
let profileView = ProfileView(frame: .zero)
,将其作为子视图添加到
ViewController
。
现在,我们的视图控制器已减少为几行代码!
import UIKit import PureLayout class ViewController: UIViewController { let profileView = ProfileView(frame: .zero) override func viewDidLoad() { super.viewDidLoad() self.navigationController?.navigationBar.isTranslucent = false self.title = "Profile" self.view.backgroundColor = .white self.view.addSubview(self.profileView) self.profileView.autoPinEdgesToSuperviewEdges() self.view.layoutIfNeeded() } }
为确保一切正常,请启动应用程序并检查其外观。
目标是拥有一个瘦弱,整洁的审查控制者。 这可能会花费很多时间,但是它将使您免于维护期间的不必要麻烦。
UITableView
接下来,我们将添加一个UITableView来显示联系信息,例如电话号码,地址等。
如果您尚未这样做,请访问Apple文档以查看UITableView,UITableViewDataSource和UITableViewDelegate。
转到
ViewController.swift
并在
viewDidLoad()
上方为
tableView
添加
lazy var
:
lazy var tableView: UITableView = { let tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self tableView.dataSource = self return tableView }()
如果尝试运行该应用程序,则Xcode会抱怨该类既不是
UITableViewController
的委托也不是数据源,因此我们将这两个协议添加到该类中:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { . . .
再一次,Xcode将抱怨一个不符合
UITableViewDataSource
协议的类,这意味着该协议中有一些未在该类中定义的强制性方法。 要找出在按住Cmd + Control的同时应实现的方法,请单击类定义中的
UITableViewDataSource
协议,然后继续进行协议定义。 对于任何不包含“
optional
”一词的方法,必须实现与该协议相对应的类。
这里我们有两种方法需要实现:
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
此方法告诉表视图我们要显示多少行。public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
此方法查询每行中的单元格。 在这里,我们初始化(或重用)单元格,然后插入要显示给用户的信息。 例如,第一个单元格将显示电话号码,第二个单元格将显示地址,依此类推。
现在回到
ViewController.swift
,开始输入
numberOfRowsInSection
,当自动完成出现时,选择第一个选项。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { <#code#> }
删除密码并立即返回1。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }
在此功能下,开始键入
cellForRowAt
并再次从自动完成中选择第一个方法。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { <#code#> }
再次,现在返回
UITableViewCell
。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return UITableViewCell() }
现在,要将表视图连接到
ProfileView
,我们将定义一个新的初始化程序,该初始化程序将表视图作为参数,以便可以将其添加为子视图并为其设置适当的限制。
转到
ProfileView.swift
并在初始化程序上方添加表视图的属性:
var tableView: UITableView!
因此,我们不确定它是否会保持不变。
现在,将旧的
init (frame :)
实现替换为:
init(tableView: UITableView) { super.init(frame: .zero) self.tableView = tableView addSubviews() bringSubview(toFront: avatar) }
Xcode现在将抱怨缺少
ProfileView
init (frame :)
,因此请返回
ViewController.swift
并将
let profileView = ProfileView (frame: .zero)
替换为
lazy var profileView: UIView = { return ProfileView(tableView: self.tableView) }()
现在,我们的
ProfileView
有一个指向表视图的链接,我们可以将其添加为子视图并为其设置正确的限制。
返回
ProfileView.swift
,在
addSubview(tableView)
的末尾添加
addSubview(tableView)
,并将这些限制设置为相对于
super.updateConstraints
updateConstraints()
:
tableView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) tableView.autoPinEdge(.top, to: .bottom, of: segmentedControl, withOffset: 8)
第一行在表格视图及其父视图之间添加了三个限制:表格视图的右侧,左侧和底部分别连接到纵断面图的右侧,左侧和底部。
第二行将表格视图的顶部附加到分段控件的底部,它们之间有八个点的间隔。 启动应用程序,结果应如下所示:
太好了,现在一切就绪,我们可以开始介绍细胞了。
UITableViewCell
要实现
UITableViewCell
,我们几乎总是需要将该类作为子类,因此在Project Navigator中右键单击
ContactCard
文件夹,然后在“ New file ...”,“ Cocoa Touch Class”和“ Next”上单击鼠标右键。
在“子类:”字段中输入“ UITableViewCell”,Xcode将自动填充类名“ TableViewCell”。 在自动完成之前输入“ ProfileView”,以使最终名称为“ ProfileInfoTableViewCell”,然后单击“下一步”和“创建”。 继续并删除创建的方法,因为我们将不需要它们。 如果需要,您可以先阅读它们的描述,以了解为什么我们现在不需要它们。
如前所述,我们的单元格将包含基本信息,这是字段名称及其描述,因此我们需要为其添加标签。
lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "Title" return label }() lazy var descriptionLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "Description" label.textColor = .gray return label }()
现在,我们将重新定义初始化程序,以便可以配置单元:
override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(titleLabel) contentView.addSubview(descriptionLabel) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
关于限制,我们将做一些不同的操作,但是仍然非常有用:
override func updateConstraints() { let titleInsets = UIEdgeInsetsMake(16, 16, 0, 8) titleLabel.autoPinEdgesToSuperviewEdges(with: titleInsets, excludingEdge: .bottom) let descInsets = UIEdgeInsetsMake(0, 16, 4, 8) descriptionLabel.autoPinEdgesToSuperviewEdges(with: descInsets, excludingEdge: .top) descriptionLabel.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 16) super.updateConstraints() }
在这里,我们开始使用
UIEdgeInsets
设置每个标签周围的间距。 可以使用
UIEdgeInsetsMake(top:, left:, bottom:, right:)
方法创建
UIEdgeInsets
对象。 例如,对于
titleLabel
我们说我们希望上限为4点,左右为8。 我们不在乎底部,因为我们将其排除在描述标记的顶部,因此将其排除在外。 花一点时间阅读并可视化您脑海中的所有约束。
好的,现在我们可以开始在表格视图中绘制单元格了。 让我们继续执行
ViewController.swift
并更改表格视图的延迟初始化,以在表格视图中注册此类单元格并设置每个单元格的高度。
let profileInfoCellReuseIdentifier = "profileInfoCellReuseIdentifier" lazy var tableView: UITableView = { ... tableView.register(ProfileInfoTableViewCell.self, forCellReuseIdentifier: profileInfoCellReuseIdentifier) tableView.rowHeight = 68 return tableView }()
我们还为单元重用标识符添加了一个常量。 此标识符用于在显示单元格时从表格视图中删除它们。 此优化可以(并且应该)用于帮助
UITableView
重用以前显示的用于显示新内容的单元格,而不是从头开始重新绘制新单元格。
现在让我向您展示如何在
cellForRowAt
方法的一行代码中重用单元格:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: profileInfoCellReuseIdentifier, for: indexPath) as! ProfileInfoTableViewCell return cell }
在这里,我们使用标识符将通知可重用单元格从队列中退出的表格视图,在该标识符下我们注册了用户将要出现的单元格路径。 然后,我们将单元格强制给
ProfileInfoTableViewCell
以便能够访问其属性,以便例如可以设置标题和描述。 可以使用以下方法完成此操作:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... switch indexPath.row { case 0: cell.titleLabel.text = "Phone Number" cell.descriptionLabel.text = "+234567890" case 1: cell.titleLabel.text = "Email" cell.descriptionLabel.text = "john@doe.co" case 2: cell.titleLabel.text = "LinkedIn" cell.descriptionLabel.text = "www.linkedin.com/john-doe" default: break } return cell }
现在将
numberOfRowsInSection
设置为返回“ 3”并启动您的应用程序。
对不对?
自定格单元
可能并且很可能会出现一种情况,您希望不同的单元格根据其内部信息具有高度,这是事先未知的。 为此,您需要一个具有自动计算尺寸的表格视图,实际上,有一种非常简单的方法来执行此操作。
首先,在
ProfileInfoTableViewCell
将此行添加到惰性初始化程序
descriptionLabel
:
label.numberOfLines = 0
返回到
ViewController
并将这两行添加到表视图初始化程序中:
lazy var tableView: UITableView = { ... tableView.estimatedRowHeight = 64 tableView.rowHeight = UITableViewAutomaticDimension return tableView }()
在这里,我们告知表格视图,行高应基于其内容具有自动计算的值。
关于估计的行高:
“提供行高度的非负估计可以提高加载表视图的性能。” -Apple文件
在
ViewDidLoad
我们需要重新加载表视图以使这些更改生效:
override func viewDidLoad() { super.viewDidLoad() ... DispatchQueue.main.async { self.tableView.reloadData() } }
现在继续添加另一个单元格,将行数增加到四,并向
cellForRow
添加另一个
switch
:
case 3: cell.titleLabel.text = "Address" cell.descriptionLabel.text = "45, Walt Disney St.\n37485, Mickey Mouse State"
现在运行该应用程序,它看起来应该像这样:
结论
对不对? 为了提醒我们为什么实际编写用户界面,这是我们的移动团队撰写的完整博客文章,内容涉及为什么我们
不在Instabug中使用情节提要 。
您在本课的两部分中做了什么:
- 从您的项目中删除了
main.storyboard
文件。 - 我们以编程方式创建了
UIWindow
并rootViewController
分配了rootViewController
。 - 在代码中创建了各种用户界面元素,例如标签,图像视图,分段控件以及带有其单元格的表格视图。
- 在您的应用程序中嵌套了
UINavigationBar
。 - 创建了一个动态大小的
UITableViewCell
。