版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.09.18 星期三 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
开始
今天是个特殊的日子,勿忘国耻,国人当自强,向抵抗侵略的将士们致敬!
首先看下主要内容
了解如何将iOS应用程序拆分为两个部分,并在此
UISplitViewController
教程的每一侧显示视图控制器
接着看一下写作环境
Swift 5, iOS 13, Xcode 11
应用程序通常需要提供拆分视图以提供整洁的导航模型。 这方面的一个例子是Mail.app
,它在iPad上使用左侧有文件夹列表的分割视图,然后是右侧选定的邮件项目。 Apple为我们构建了一个非常方便的视图控制器,称为UISplitViewController
,它可以直接回到iPad
的低端。 在这个UISplitViewController
教程中,您将学习如何使用它! 此外,自iOS 8
起,split view controller
拆分视图控制器可在iPad
和iPhone
上运行。
在本教程中,您将从头开始创建一个通用应用程序,它使用split view controller
来显示Math Ninja中的怪物列表。
您将使用拆分视图控制器来处理导航和显示。 它适用于iPhone
和iPad
。
单击File ▸ New ▸ Project…
,在Xcode中创建一个新项目。 选择 iOS ▸ Application ▸ Single View App
模板。
将项目命名为MathMonsters
。 将Language
保持为Swift
。 将User Interface
设置为Storyboard
。 取消选中所有复选框。 然后单击Next
完成项目的创建。
虽然您可以使用Master-Detail App
模板作为起点,但您将从头开始使用Single View App
模板。 这将使您更好地了解UISplitViewController
的工作原理。 在将来的项目中使用UISplitViewController
时,这些知识将非常有用。
是时候创建UI了,所以打开Main.storyboard
。
删除故事板中的默认初始View Controller Scene
。 同时从项目导航器中删除ViewController.swift
,确保在询问时选择Move to Trash
。
将拆分视图控制器拖到空的故事板中:
这将为您的storyboard
添加几个元素:
- Split View Controller - 拆分视图控制器:此拆分视图将包含应用程序的其余部分,并且是应用程序的根。
-
Navigation Controller - 导航控制器:此
UINavigationController
将是主视图控制器的根视图。 这是拆分视图的左侧窗格,当在iPad
上或在较大的iPhone(如iPhone 8 Plus)上横向显示时。在拆分视图控制器中,您将看到导航控制器具有称为master view controller
的关系segue
。 这允许您在主视图控制器中创建整个导航层次结构,而无需影响详细视图控制器。 -
View Controller - 视图控制器:这将最终显示所有怪物的详细信息。 如果查看拆分视图控制器,您将看到视图控制器具有称为详细视图控制器
(detail view controller)
的关系segue:
-
Table View Controller:这是主
UINavigationController
的根视图控制器。 它最终将显示怪物列表。
注意:Xcode会警告您表视图的原型单元缺少重用标识符
(reuse identifier)
。 暂时不要担心。 你很快就会解决它。
由于您从故事板中删除了默认的初始视图控制器,因此您需要告诉故事板您希望拆分视图控制器成为初始视图控制器。
选择Split View Controller
,然后打开Attributes inspector
。 选中Is Initial View Controller
选项。
您将在分割视图控制器的左侧看到一个箭头。 这告诉你它是这个故事板的初始视图控制器。
在iPad模拟器上构建并运行应用程序。 将模拟器旋转到横向。
您应该看到一个空的拆分视图控制器:
现在可以在任何iPhone
模拟器上运行它,除了一个加大尺寸的手机,它足够大,可以像iPad一样运行。 你会看到它开始全屏显示细节视图。 它还允许您点击导航栏上的后退按钮以弹回主视图控制器:
在除了横向大型Plus
或Max
设备之外的iPhone
上,分割视图控制器将像传统的master-detail
应用程序一样,带有导航控制器来回推出和弹出。这是内置功能,开发人员只需要很少的额外配置。
您需要显示自己的视图控制器而不是这些默认控制器。是时候开始创建它们了。
Creating Custom View Controllers
故事板具有视图控制器层次结构集:拆分视图控制器,其主视图控制器和详细视图控制器作为其子视图。现在,您需要实现代码方面以获取要显示的数据。
转到File ▸ New ▸ File…
,并选择iOS ▸ Source ▸ Cocoa Touch Class
模板。将类命名为MasterViewController
,并使其成为UITableViewController
的子类。确保未选中Also create XIB file
复选框,并将Language
设置为Swift
。单击Next
,然后单击Create
。
打开MasterViewController.swift
。
向下滚动到numberOfSections(in:)
中。删除此方法。只返回一个部分时不需要它。
接下来,找到tableView(_:numberOfRowsInSection :)
并用以下内容替换实现:
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int)
-> Int {
return 10
}
最后,取消注释tableView(_:cellForRowAt :)
并将其实现替换为以下内容:
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
return cell
}
这样,当你稍后测试这个东西时,你将看到十个空行。
打开Main.storyboard
。 选择Root View Controller
并切换Identity
检查器。 将类更改为MasterViewController
。
此外,您需要确保在表视图中为原型单元格提供重用标识符。 如果没有,它会在故事板试图加载时导致崩溃。
在Master View Controller
中,选择Prototype Cell
。 在Attributes inspector
中,将Identifier
更改为Cell
。 同时将单元格Style
更改为Basic
。
在iPad
或iPhone
模拟器中构建和运行。 你会注意到虽然有十行,都标有标题,点击一行不会做任何事情。 这是因为您尚未指定详细视图控制器。
现在,您将为细节方创建视图控制器。
转到File ▸ New ▸ File…
,并选择iOS ▸ Source ▸ Cocoa Touch Class
模板。 将类命名为DetailViewController
,并使其成为UIViewController
的子类。 确保未选中Also create XIB file
复选框,并将Language
设置为Swift
。
单击Next
,然后单击Create
。
打开Main.storyboard
并在View Controller Scene
中选择视图控制器。 在Identity inspector
中,将Class
更改为DetailViewController
。
然后将label
拖到详细视图控制器的中间。 使用“自动布局”将label
固定到容器的水平和垂直中心。
双击label
将其文本更改为Hello,World!
,所以当你稍后测试它时你会知道它正在工作。
构建并运行。 此时,您应该看到自定义视图控制器。
在iPad
上:
在iPhone
上:
您现在已经获得了拆分视图的基础,每个位都有自定义视图控制器。 接下来你需要添加那些讨厌的怪物。
Making Your Model
接下来,您需要为要显示的数据定义模型。 在学习拆分视图控制器的基础知识时,您不希望复杂化,因此您将使用没有数据持久性的简单模型。
首先,创建一个表示要显示的怪物的类。 转到File ▸ New ▸ File…
,选择iOS ▸ Source ▸ Swift File
模板,然后单击Next
。 将文件命名为Monster
,然后单击Create
。
您将创建一个简单的类,其中包含有关要显示的每个怪物的属性属性。 您还将实现一些方法来创建新的怪物并访问每个怪物武器的图像。
用以下内容替换Monster.swift
的内容:
import UIKit
enum Weapon {
case blowgun, ninjaStar, fire, sword, smoke
var image: UIImage {
switch self {
case .blowgun:
return UIImage(named: "blowgun.png")!
case .fire:
return UIImage(named: "fire.png")!
case .ninjaStar:
return UIImage(named: "ninjastar.png")!
case .smoke:
return UIImage(named: "smoke.png")!
case .sword:
return UIImage(named: "sword.png")!
}
}
}
class Monster {
let name: String
let description: String
let iconName: String
let weapon: Weapon
init(name: String, description: String, iconName: String, weapon: Weapon) {
self.name = name
self.description = description
self.iconName = iconName
self.weapon = weapon
}
var icon: UIImage? {
return UIImage(named: iconName)
}
}
这定义了枚举和类。 枚举是为了跟踪不同种类的武器,包括每种武器的图像。 该类将使用简单的初始化程序保存怪物信息以创建Monster
实例。
这是用于定义模型的。 接下来,您将它连接到您的主视图!
Displaying the Monster List
打开MasterViewController.swift
并向该类添加一个新属性:
let monsters = [
Monster(name: "Cat-Bot", description: "MEE-OW",
iconName: "meetcatbot", weapon: .sword),
Monster(name: "Dog-Bot", description: "BOW-WOW",
iconName: "meetdogbot", weapon: .blowgun),
Monster(name: "Explode-Bot", description: "BOOM!",
iconName: "meetexplodebot", weapon: .smoke),
Monster(name: "Fire-Bot", description: "Will Make You Steamed",
iconName: "meetfirebot", weapon: .ninjaStar),
Monster(name: "Ice-Bot", description: "Has A Chilling Effect",
iconName: "meeticebot", weapon: .fire),
Monster(name: "Mini-Tomato-Bot", description: "Extremely Handsome",
iconName: "meetminitomatobot", weapon: .ninjaStar)
]
这可以保存用于填充表视图的怪物数组。
找到tableView(_:numberOfRowsInSection :)
并将return
语句替换为以下内容:
return monsters.count
这将根据数组的大小返回怪物数量。
接下来,找到tableView(_:cellForRowAtIndexPath :)
并在最终的return
语句之前添加以下代码:
let monster = monsters[indexPath.row]
cell.textLabel?.text = monster.name
这将根据正确的怪物配置单元格。 这就是table view
,它只是显示每个怪物的名字。
构建并运行应用程序。
你应该在横屏iPad
的左侧看到怪物机器人列表:
在iPhone
上:
请记住,在compact-width
的iPhone
上,您可以在详细信息屏幕上的导航堆栈中开始一层深度。 您可以点击后退按钮查看table view
。
Updating the Master View Controller’s Title
导航栏自动设置初始视图控制器的标题,即RootViewController
。
打开Main.storyboard
,选择Root View Controller
并双击NavigationBar
。
将其更改为Monster List
。 这比Root View Controller
好得多。
Displaying Bot Details
现在table view
显示了怪物列表,现在是时候按顺序获取详细视图了。
打开Main.storyboard
,选择Detail View Controller
并删除之前放下的label
。
使用下面的屏幕截图作为指导,将以下控件拖到DetailViewController
的视图中(有关要添加的内容的详细列表,请参阅下面的内容):
以下是您需要添加的内容:
- 1) 其余视图将进入的容器视图。这应该与屏幕顶部对齐并在屏幕中水平居中。
- 2) 一个
95×95
的图像视图,距离容器视图顶部8
个像素,距离左侧20
个像素。这是为了显示怪物的图像。 - 3) 与图像视图顶部对齐的
label
,字体System Bold
,大小为30
,文本为Monster Name
。将其顶部与图像的顶部对齐,并将其设置为图像右侧的8
个像素。同时使其尾部边距容器视图的右侧8
像素。 - 4) 下面有两个带有
System
字体的labels
,尺寸为24
。一个label
应与图像视图底部对齐。另一个label
应位于第一个标签下方。它们的左边缘应该对齐,它们应该垂直间隔8个像素。同时将这些标签的尾部设置为距离容器视图右侧8个像素。他们应该有标题Description
和Preferred way to kill
。 - 5) 一个
70×70
的图像视图,与Preferred way to kill
标签左对齐,8像素垂直间距。同时将其底部设置为距离容器视图底部8个像素。
让自动布局使用适当的约束尤其重要,因为这个应用程序是通用的,自动布局确保布局适应iPad和iPhone。
这就是现在的自动布局。 接下来,您需要将这些视图挂钩到某些outlets
。
打开DetailViewController.swift
并将以下属性添加到类的顶部:
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var weaponImageView: UIImageView!
var monster: Monster? {
didSet {
refreshUI()
}
}
在这里,您为刚刚创建的需要动态更改的各种UI元素添加了属性。 您还为此视图控制器应显示的Monster
对象添加了一个属性。
接下来,将以下帮助器方法添加到类中:
private func refreshUI() {
loadViewIfNeeded()
nameLabel.text = monster?.name
descriptionLabel.text = monster?.description
iconImageView.image = monster?.icon
weaponImageView.image = monster?.weapon.image
}
无论何时切换怪物,您都希望UI自行刷新并更新outlets
中显示的详细信息。 你甚至可以在视图加载之前更改monster
并触发方法。 因此,您调用loadViewIfNeeded()
以保证视图已加载且其outlets
已连接。
现在,打开Main.storyboard
。 在Document Outline
中右键单击Detail View Controller
对象以显示插座列表。 从每个项目右侧的圆圈拖动到视图以连接outlets
。
请记住,图标图像视图是左上角的大图像视图。 武器图像视图是Preferred way to kill
标签的方式下面较小的一个。
转到SceneDelegate.swift
并使用以下内容替换scene(_:willConnectTo:options :)
的实现:
guard
let splitViewController = window?.rootViewController as? UISplitViewController,
let leftNavController = splitViewController.viewControllers.first
as? UINavigationController,
let masterViewController = leftNavController.viewControllers.first
as? MasterViewController,
let detailViewController = splitViewController.viewControllers.last
as? DetailViewController
else { fatalError() }
let firstMonster = masterViewController.monsters.first
detailViewController.monster = firstMonster
拆分视图控制器具有一个数组属性viewControllers
,其中包含主控制器和详细视图控制器。 在您的情况下,主视图控制器实际上是导航控制器。 因此,要获取实际的MasterViewController
实例,请使用导航控制器的第一个视图控制器。
要获取详细视图控制器,请查看拆分视图控制器的viewControllers
数组中的第二个视图控制器。
构建并运行应用程序,您应该在右侧看到一些怪物细节。
在iPad
上横屏:
在iPhone
上
请注意,在MasterViewController
上选择一个monster
什么也没做,你就永远陷入了Cat-Bot
。 这就是你接下来要做的事情!
Hooking Up the Master With the Detail
关于如何在这两个视图控制器之间进行最佳通信的策略有很多。 在Master-Detail App
模板中,主视图控制器具有对详细视图控制器的引用。 这意味着主视图控制器可以在选择行时在详细视图控制器上设置属性。
这适用于在详细信息窗格中只有一个视图控制器的简单应用程序。 但是,您将遵循UISplitViewController
类引用中建议的方法来处理更复杂的应用程序并使用委托delegate
。
打开MasterViewController.swift
并在MasterViewController
类定义上面添加以下协议定义:
protocol MonsterSelectionDelegate: class {
func monsterSelected(_ newMonster: Monster)
}
这定义了一个带有单个方法的协议,monsterSelected(_ :)
。 详细视图控制器将实现此方法,并且主视图控制器将在用户选择怪物时向其发送消息。
接下来,更新MasterViewController
以添加符合委托协议的对象的属性:
weak var delegate: MonsterSelectionDelegate?
基本上,这意味着委托属性需要是一个实现了monsterSelected(_ :)
的对象。 在用户选择怪物后,该对象将负责处理其视图中需要发生的事情。
由于您希望DetailViewController
在用户选择怪物时更新,因此您需要实现委托。
打开DetailViewController.swift
并在文件的最后添加一个类扩展:
extension DetailViewController: MonsterSelectionDelegate {
func monsterSelected(_ newMonster: Monster) {
monster = newMonster
}
}
类扩展非常适合分离委托协议并将方法组合在一起。 在此扩展中,您说DetailViewController
符合MonsterSelectionDelegate
。 然后,您实现一个必需的方法。
现在委托方法已准备就绪,您需要从master
方面调用它。
打开MasterViewController.swift
并添加以下方法:
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
let selectedMonster = monsters[indexPath.row]
delegate?.monsterSelected(selectedMonster)
}
实现tableView(_:didSelectRowAt :)
意味着只要用户在表视图中选择一行,您就会收到通知。 您需要做的就是通知新怪物的怪物选择代理。
最后,返回SceneDelegate.swift
。 在scene(_:willConnectTo:options:)
中,在方法的最后添加以下代码:
masterViewController.delegate = detailViewController
这是两个视图控制器之间的最终连接。
在iPad
上构建并运行应用程序。 你现在应该可以在monsters
之间进行选择,如下所示:
到目前为止,拆分视图非常好! 但是还有一个问题:如果你在iPhone
上运行它,从主表视图中选择怪物不会显示详细视图控制器。 您现在需要进行一些小修改,以确保拆分视图也适用于iPhone。
打开MasterViewController.swift
。 找到tableView(_:didSelectRowAt :)
并将以下内容添加到方法的末尾:
if let detailViewController = delegate as? DetailViewController {
splitViewController?.showDetailViewController(detailViewController, sender: nil)
}
首先,您需要确保代理已设置,并且它是一个DetailViewController
实例,正如您所期望的那样。 然后在拆分视图控制器上调用showDetailViewController(_:sender :)
并传入详细视图控制器。 UIViewController
的每个子类都有一个继承属性splitViewController
,它将引用它容器视图控制器(如果存在)。
此新代码仅更改iPhone
上应用程序的行为,导致导航控制器在您选择新怪物时将细节控制器推入堆栈。 它不会改变iPad
实现的行为,因为在iPad上,细节视图控制器始终可见。
进行此更改后,在iPhone上运行它现在应该正常运行。 只需添加几行代码,您就可以在iPad和iPhone上使用功能齐全的分割视图控制器。 不错!
Split View Controller in iPad Portrait
以纵向模式在iPad
中运行应用程序。 起初,似乎没有办法进入左侧菜单。
但请尝试从屏幕左侧滑动。 很酷吧? 点按菜单外的任意位置即可隐藏它。
内置的滑动功能非常酷,但是如果你想在导航栏上方放置一个显示菜单的按钮,类似于它在iPhone
上的表现怎么办? 要做到这一点,您需要对应用程序进行一些小的修改。
首先,打开Main.storyboard
并将Detail View Controller
嵌入到导航控制器中。 您可以通过选择详细视图控制器,然后选择Editor ▸ Embed In ▸ Navigation Controller
来完成此操作。
您的故事板现在看起来像这样:
现在打开MasterViewController.swift
并找到tableView(_:didSelectRowAt :)
。 通过调用showDetailViewController(_:sender :)
将if
块更改为以下内容:
if
let detailViewController = delegate as? DetailViewController,
let detailNavigationController = detailViewController.navigationController {
splitViewController?
.showDetailViewController(detailNavigationController, sender: nil)
}
现在,您将显示详细视图控制器的导航控制器,而不是显示详细视图控制器。 无论如何,导航控制器的根目录是详细视图控制器,因此您仍然可以看到与之前相同的内容,只需将其包含在导航控制器中。
在运行应用程序之前,最后要进行两项更改。
首先,在SceneDelegate.swift
更新scene(_willConnectTo:options:)
中,通过替换初始化detailViewController
的行来解释DetailViewController
现在包含在导航控制器中的事实:
let detailViewController =
(splitViewController.viewControllers.last as? UINavigationController)?
.topViewController as? DetailViewController
由于详细视图控制器包含在导航控制器中,因此现在有两个步骤来访问它。
最后,在方法结束之前添加以下行。
detailViewController.navigationItem.leftItemsSupplementBackButton = true
detailViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem
这告诉详细视图控制器用一个按钮替换其左侧导航项,该按钮将切换拆分视图控制器的显示模式。 在iPhone
上运行时不会改变任何东西,但在iPad
上,你会在左上角看到一个按钮来切换table view
显示。
在iPad
竖屏上运行应用程序并检查:
现在,您可以在纵向和横向上在iPad
和iPhone
上运行良好的效果!
后记
本篇主要讲述了一个UISplitViewController的简单实用示例,感兴趣的给个赞或者关注~~~