在上一教程中 ,我们讨论了导航控制器如何使用户能够通过管理视图控制器堆栈来导航分层内容或复杂数据。 标签栏控制器还管理一系列视图控制器。 区别在于,选项卡栏控制器的视图控制器不一定具有相互关系。 在本教程中,我们将通过从头开始创建选项卡式应用程序来更详细地研究选项卡栏控制器。
介绍
UITabBarController
是另一个UIViewController
子类。 导航控制器管理一堆相关的视图控制器,而标签栏控制器则管理一系列彼此之间没有明确关系的视图控制器。
iOS上的Clock and Music应用程序是选项卡栏控制器的两个主要示例。 就像任何其他UIViewController
子类一样,选项卡栏控制器管理UIView
实例。
标签栏控制器的视图由两个子视图组成:
- 视图底部的标签栏
- 标签栏控制器管理的视图控制器之一的视图
开始之前
使用选项卡栏控制器时,需要注意一些注意事项。 即使UITabBar
实例只能显示五个选项卡, UITabBarController
可以管理更多的视图控制器。 如果一个标签栏控制器管理着五个以上的视图控制器,则该标签栏的最后一个标签标题为More 。
可以通过此选项卡访问其他视图控制器,甚至可以在选项卡栏中编辑选项卡的位置。
即使使用标签栏控制器管理视图,也不应将您的应用程序直接与标签栏控制器的视图进行交互。 标签栏控制器必须是应用程序窗口的根视图控制器。 换句话说,应用程序窗口的根视图始终是选项卡栏控制器的视图。 禁止将选项卡栏控制器作为另一个视图控制器的子级安装。 这是与导航控制器的主要区别之一。
标签式图书馆
在本文中,我们将回顾上一篇文章中构建的Library应用程序。 这样做可以让我们重用几个类,并加快速度。 此外,它将向您显示导航控制器和选项卡栏控制器有很大不同,并且它们在不同的情况和用例中使用。
因为我们在本课中构建的应用程序基于UITabBarController
类,所以它将为应用程序提供非常特定的外观,从而在用户界面和体验方面几乎没有灵活性。 标签栏控制器非常有用,但是您必须接受它们在一定程度上限制了应用程序的用户界面。
打开Xcode,创建一个新项目( File> New> Project ... ),然后选择Single View Application模板。
将项目命名为选项卡式库 ,分配组织名称和标识符,将Language设置为Swift ,并将Devices设置为iPhone 。 告诉Xcode您要将项目保存到哪里,然后单击Create 。
即使Xcode包含选项卡式应用程序模板,我还是更喜欢从基本的应用程序模板开始,以便您了解难题的各个部分如何组合在一起。 您会注意到,标签栏控制器并不那么复杂。
抢先一步
当选项卡式库应用程序完成时,该应用程序的选项卡栏控制器将管理六个视图控制器。 与其从头创建每个视图控制器类,不如通过重用上一篇文章中创建的视图控制器类来作弊。 此外,我们将创建同一视图控制器类的多个实例,以节省一些时间。 本文的目的不是创建一堆视图控制器类。 在这一点上,您应该非常熟悉它的工作原理。
从上一篇文章下载源代码,并在新的Finder窗口中打开源文件中包含的Xcode项目。 找到AuthorsViewController
, BooksViewController
和BookCoverViewController
类,然后将它们拖到新项目中。 确保通过选中“ 复制项目 ”复选框( 如果需要)将文件复制到新项目中,不要忘记将文件添加到“ 选项卡式库”目标中。
除了这三个类外,我们还需要将包含Books.plist和图像文件的资源文件夹复制到我们的新项目中。 将名为Resources的文件夹拖到我们的项目中,并使用与复制类文件相同的设置。 现在,我们可以实例化应用程序的选项卡栏控制器,并使用其第一个视图控制器填充它。
添加标签栏控制器
如果打开Main.storyboard ,则会注意到情节提要包含ViewController
类的实例。 选择视图控制器,然后按Delete或Backspace。 打开右侧的对象库 ,然后将选项卡栏控制器拖到工作区中。
Xcode自动将两个子视图控制器添加到选项卡栏控制器。 因为我想展示如何将子视图控制器手动添加到标签栏控制器,所以我们将删除Xcode为我们创建的子视图控制器。 选择子视图控制器,然后按Delete或Backspace删除它们。
选择选项卡栏控制器,打开“ 属性”检查器 ,然后选中“初始视图控制器 ”复选框。 如果我们不将标签栏控制器设置为初始视图控制器,则应用程序将在启动时崩溃。
如果您在模拟器中运行该应用程序,则应该在底部看到一个标签栏,背景为黑色。 这似乎并不重要,但是它显示了标签栏控制器的工作方式。 标签栏控制器管理一系列视图控制器,类似于导航控制器管理视图控制器堆栈的方式。
我们需要将一些视图控制器添加到情节viewControllers
,并将它们添加到选项卡栏控制器的viewControllers
属性中。 让我们看看它是如何工作的。
添加视图控制器
将UITableViewController
实例从“ 对象库 ” AuthorsViewController
工作区, AuthorsViewController
在Identity Inspector中将其类设置为AuthorsViewController
。 选择视图控制器的表视图,然后在Attributes Inspector中将Prototype Cells设置为0 。
要将作者视图控制器添加到标签栏控制器的视图控制器数组中,请在按住Control键的同时从标签栏控制器拖动到作者视图控制器。 从出现的菜单中选择Relationship Segue> 视图控制器 。
具有一个选项卡的选项卡栏控制器不是那么有用,所以让我们向混合添加另一个视图控制器。 从对象库中拖动另一个UITableViewController
实例,将其类设置为BooksViewController
,并将Prototype Cells设置为0 。 像我们对作者视图控制器一样,创建关系序列。
将UIViewController
实例添加到工作区, BookCoverViewController
在Identity Inspector中将其类设置为BookCoverViewController
。 将UIImageView
实例添加到视图控制器,将其与视图控制器的bookCoverView
出口连接,然后应用必要的布局约束。 就像我们对表格视图控制器所做的那样,使用标签栏控制器创建关系segue。
情节提要有时会变得有些混乱。 如果在视图控制器之间创建Segue时遇到问题,请知道您也可以在左侧的导航器中创建连接(例如Segue)。 这通常更容易且不那么笨拙。
生成并运行该应用程序。 此时,选项卡栏包含三个选项卡。 轻按第二个或第三个选项卡会使应用程序崩溃。 这是为什么? 现在该进行一些调试了。
修复错误
修复作者视图控制器
当您在作者视图控制器中点击作者的姓名时,应用程序崩溃。 遇到崩溃时,您应该做的第一件事就是检查Xcode的控制台。 这就是告诉我的。
Tabbed Library[1141:54864] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Receiver (<Tabbed_Library.AuthorsViewController: 0x7fde3943d050>) has no segue with identifier 'BooksViewController''
Xcode在控制台中显示的消息并不难破解。 我们告诉视图控制器使用标识BooksViewController
进行segue,但是该segue不存在。 结果是崩溃。
通过将作者视图控制器嵌入导航控制器,为BooksViewController
实例创建segue并命名segue BooksViewController ,可以轻松修复此错误。 我将其留给您作为任务,因为我们在上一篇文章中已经做了。 本教程的源文件包含解决方案。
在此示例中,更重要的问题是要了解链接到选项卡栏控制器的三个视图控制器不会相互通信。 标签栏控制器和导航控制器背后的想法是非常不同的。 导航控制器维护一系列视图控制器,即导航堆栈。 导航堆栈中的视图控制器彼此之间是一种隐式关系,因为它们是同一导航堆栈的一部分。
标签栏控制器还管理一系列视图控制器,但是视图控制器彼此之间并不了解。 他们无法通过标签栏控制器相互通信。 在上一篇文章中,当用户点击作者时,我们将数据从作者视图控制器传递到书籍视图控制器。 此模式不适用于标签栏控制器。 当然,我们可以实现一个解决方案,以便在作者视图控制器中点按作者时向用户显示书本视图控制器,但是重要的是要了解这不是标签栏控制器的目标。
iOS上的Clock and Music应用程序是如何使用标签栏控制器的很好的例子。 标签栏控制器在“音乐”应用程序中管理的视图控制器彼此无关,除了它们显示歌曲。
在继续之前,请确保您了解导航控制器和标签栏控制器的概念。 它们在本系列的后面很重要。
修复Books View控制器
当您点击选项卡栏控制器的第二个选项卡时,应用程序也会崩溃。 这就是我在Xcode控制台中看到的。
fatal error: unexpectedly found nil while unwrapping an Optional value
这也不足为奇,因为我们没有设置books视图控制器的author
属性。 让我们更新books视图控制器,以便它显示Books.plist中的每本书。
我们需要进行三个更改。 让我们先从最简单的更改开始。 打开BooksViewController.swift并将author
属性的类型更改为[String: AnyObject]?
。 现在,author属性是可选的,而不是强制展开的可选。
var author: [String: AnyObject]?
对于第二个更改,我们需要更新viewDidLoad()
的实现。 如果author
不为nil
则显示作者的姓名。 如果author为nil
,则将标题设置为"Books"
。
override func viewDidLoad() {
super.viewDidLoad()
if let author = author, let name = author["Author"] as? String {
title = name
} else {
title = "Books"
}
tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
}
最后的更改涉及更多。 我们将计算的books
属性转换为惰性存储属性。 惰性存储属性在首次访问时被初始化。 换句话说,初始化视图控制器时不会初始化该属性。 这就是实现的样子。
lazy var books: [AnyObject] = {
// Initialize Books
var buffer = [AnyObject]()
if let author = self.author, let books = author["Books"] as? [AnyObject] {
buffer += books
} else {
let filePath = NSBundle.mainBundle().pathForResource("Books", ofType: "plist")
if let path = filePath {
let authors = NSArray(contentsOfFile: path) as! [AnyObject]
for author in authors {
if let books = author["Books"] as? [AnyObject] {
buffer += books
}
}
}
}
return buffer
}()
延迟存储的属性由lazy
关键字标记。 我们没有给属性分配一个普通的值,而是分配了一个闭包。 但请注意,闭包以两个括号()
结尾。 这表示在首次访问books
时执行了关闭。 这与其他语言(例如JavaScript)中的匿名函数非常相似。
在闭包中,我们创建一个[AnyObject]
类型的空数组。 如果author
不为nil
,则从author
属性加载书籍。 如果author
没有值,我们加载Books.plist并将每位作者的每本书添加到buffer
。 请注意,我们在关闭的结尾处返回书籍数组。 运行该应用程序,以查看它是否解决了由books视图控制器引起的崩溃。
如果打开应用程序的第二个选项卡,然后从列表中点击一本书,则应用程序崩溃的原因与作者的视图控制器导致崩溃的原因相同。 您可以通过以下方法解决此问题:将books view控制器嵌入导航控制器中,并为BookCoverViewController
实例创建一个标识符为BookCoverViewController的segue。
修复书套视图控制器
当您点击第三个选项卡时,应用程序也会崩溃。 该解决方案类似于我们应用于BooksViewController
类的解决方案。 首先将BookCoverViewController
的book
属性BookCoverViewController
可选。
var book: [String: String]?
我们声明另一个懒惰的存储属性, UIImage?
类型的bookCoverImage
UIImage?
。 在bookCoverImage
我们从存储在Books.plist书随机书,如果book
属性没有值。 如果book
具有价值,我们将显示该书籍的书籍封面。
lazy var bookCoverImage: UIImage? = {
var image: UIImage?
if self.book == nil {
// Initialize Buffer
var buffer = [AnyObject]()
let filePath = NSBundle.mainBundle().pathForResource("Books", ofType: "plist")
if let path = filePath {
let authors = NSArray(contentsOfFile: path) as! [AnyObject]
for author in authors {
if let books = author["Books"] as? [AnyObject] {
buffer += books
}
}
}
if buffer.count > 0 {
let random = Int(arc4random()) % buffer.count
if let book = buffer[random] as? [String: String] {
self.book = book
}
}
}
if let book = self.book, let fileName = book["Cover"] {
image = UIImage(named: fileName)
}
return image
}()
在viewDidLoad()
,我们使用bookCoverImage
属性更新书籍封面视图控制器的图像视图。
override func viewDidLoad() {
super.viewDidLoad()
if let bookCoverImage = bookCoverImage {
bookCoverView.image = bookCoverImage
bookCoverView.contentMode = .ScaleAspectFit
}
}
如果您已实现上述更改,则现在应该可以运行该应用程序而不会看到任何崩溃。 如果您希望了解详细信息,则可能已经注意到第二个和第三个选项卡的标题仅在选中该选项卡时出现。 你能猜出为什么吗?
标签栏项目
这种奇怪的怪癖的原因实际上很简单。 通常,除非绝对必要,否则视图不会加载到内存中。 通常,这意味着将要向用户显示视图时将其加载到内存中。
启动选项卡式库应用程序时,默认情况下选择第一个选项卡。 只要用户或以编程方式未选择第二个或第三个选项卡,就无需加载该视图控制器的视图。 结果,直到选择了选项卡, viewDidLoad()
才被调用。 这意味着在选择选项卡之前不会设置标题。
解决方案很简单。 每个UIViewController
实例(或其子类)都具有UITabBarItem!
类型的tabBarItem
属性UITabBarItem!
。 选项卡栏控制器使用此属性在其选项卡栏中配置选项卡。 从视图控制器的tabBarItem
属性中获取视图控制器的选项卡的tabBarItem
。
若要解决上述问题,我们需要在初始化标签栏控制器的视图控制器时设置tabBarItem
属性的title
属性。 让我们看看这对BooksViewController
类如何工作。
打开BooksViewController.swift并添加以下初始化程序。 在看一下实现之前,我想指出关于初始化程序的三个有趣的细节。 在Swift中,初始化器是一种特殊的函数。
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Initialize Tab Bar Item
tabBarItem = UITabBarItem(title: "Books", image: UIImage(named: "icon-books"), tag: 1)
}
-
required
关键字指示此初始化程序要求每个子类都实现。 但是,如果子类使用其父类继承的初始化器满足要求,则不必实现必需的初始化器。 初始化是Swift中一个非常复杂的主题,在开发人员之间引起了很多混乱。 由于它的复杂性,我将不在本系列中对其进行详细介绍。 -
init
关键字后的问号表示初始化可能失败。 这被称为故障初始化器 。 如果由于某种原因而无法实例化视图控制器,则初始化程序将返回nil
而不是该类的实例。 - 您可能还已经注意到,初始化程序未使用
func
关键字。func
关键字不用于初始化程序。
在初始化程序中,我们调用父类的初始化程序。 super
关键字引用超类。 然后,我们创建一个标签栏项目并将其分配给tabBarItem
属性。 为了创建标签栏项目,我们调用init(title:image:tag:)
,传入标签栏项目的标题, UIImage
实例和标签。 标签用于识别选项卡栏项。
请注意,您可以省略文件名的扩展名。 即使我们使用名称为icon-authors.png的图像,我们传递给UIImage
初始化程序的值为 "icon-authors"
。 我使用的图像包含在GitHub的源文件中,如果您想在自己的项目中使用它们,也可以在GraphicRiver上找到它们。
每张图片都有两个文件,分别是icon-authors.png和icon-authors@2x.png 。 操作系统为没有视网膜屏幕的设备选择第一个文件。 第二个文件用于具有视网膜屏幕的设备。 请注意,您不需要在传递给UIImage
初始化程序的字符串中包含@ 2x说明符。 操作系统足够聪明,可以解决这个问题。
您可以在iPhone 6 / 6S Plus的第三个文件中添加@ 3x说明符。 该文件的像素密度应该是第一个文件的像素密度的三倍。 请看以下示例,以更好地了解其在实践中的工作方式。
- icon-authors.png:30像素x 30像素
- icon-authors@2x.png: 60像素x 60像素
- icon-authors@3x.png:90像素 x 90像素
我们还为AuthorsViewController
和BookCoverViewController
类创建一个标签栏项。
// Authors View Controller
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Initialize Tab Bar Item
tabBarItem = UITabBarItem(title: "Authors", image: UIImage(named: "icon-authors"), tag: 0)
}
// Book Cover View Controller
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Initialize Tab Bar Item
tabBarItem = UITabBarItem(title: "Cover", image: UIImage(named: "icon-cover"), tag: 2)
}
标签栏项目也可以具有徽章。 例如,如果您要构建电子邮件客户端,则可以在标签上添加未读电子邮件的数量。 您可以通过设置其badgeValue
属性将其添加到选项卡栏项目。 如下所示更新books view控制器的初始化程序。
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Initialize Tab Bar Item
tabBarItem = UITabBarItem(title: "Books", image: UIImage(named: "icon-books"), tag: 1)
// Configure Tab Bar Item
tabBarItem.badgeValue = "8"
}
这是在模拟器中运行应用程序时的结果。
在上一课中,我写道,导航堆栈上的每个视图控制器都会引用管理该堆栈的导航控制器。 对于由标签栏控制器管理的视图控制器也是如此。 由标签栏控制器管理的视图控制器在其tabBarController
属性中保留对标签栏控制器的引用,该属性的类型为UITabBarController?
。
使用标签栏控制器时,请记住,是每个标签的根视图控制器决定了各个标签的标签栏项目的外观。 例如,如果选项卡栏控制器使用多个视图控制器来管理导航控制器,则选项卡栏控制器的选项卡栏将使用导航控制器的根视图控制器的选项卡栏项。 UITabBarItem
类具有其他一些方法,可以进一步自定义选项卡栏项目的外观。
更多视图控制器
在结束本文之前,我想向您展示当标签栏控制器管理五个以上的视图控制器时,标签栏的外观。 如前所述,一次只能显示五个选项卡,但是选项卡栏控制器为管理五个以上的子视图控制器提供了支持。
打开主故事板,并添加两个UITableViewController
实例和一个UIViewController
实例。 在Identity Inspector中 ,将这些视图控制器的类设置为AuthorsViewController
, BooksViewController
和BookCoverViewController
分别。 运行应用程序以查看结果。 如果您从右边按第一个选项卡,则应该在选项卡栏上看到两个视图控制器。
您甚至可以通过点击右上角的“ 编辑”按钮来修改选项卡栏项目的位置。
我们添加的额外视图控制器不是很有用,但是它们显示了标签栏控制器如何管理五个以上的子视图控制器。
结论
重要的是要了解UITabBarController
和UINavigationController
分别代表唯一的用户界面范例。 在本文中,您了解了一旦了解了所涉及的组件,就不难掌握标签栏控制器。 在下一篇文章中,我们将研究iOS上的数据持久性以及开发人员具有的选项。
如果您有任何问题或意见,可以将其留在下面的评论中,或通过Twitter与我联系。