如何在 Swift 中继承自定义 UIViewController?
How to subclass custom UIViewController in Swift?
我想创建一个可重复使用的视图控制器UsersViewControllerBase
。
UsersViewControllerBase
扩展了 UIViewController
,并实现了两个委托(UITableViewDelegate
、UITableViewDataSource
),并且有两个视图(UITableView
、UISegmentedControl
)
目标是继承UsersViewControllerBase
的实现,自定义UsersViewController
class.
中分段控件的分段项
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
class UsersViewController: UsersViewControllerBase {
}
情节提要中存在 UsersViewControllerBase
并且所有出口都已连接,指定了标识符。
问题是如何初始化 UsersViewController 以继承 UsersViewControllerBase
的所有视图和功能
当我创建 UsersViewControllerBase
的实例时一切正常
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
但是当我创建 UsersViewController
的实例时,我得到了 nil
个出口
(我创建了一个简单的 UIViewController
并在故事板中将 UsersViewController class 分配给它)
let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController
看来视图没有被继承。
我希望 UsersViewControllerBase
中的 init 方法能够从故事板获取具有视图和出口的控制器:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var tableView: UITableView!
init(){
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
self = usersViewControllerBase //but that doesn't compile
}
}
我会初始化 UsersViewController
:
let usersViewController = UsersViewController()
但不幸的是,这不起作用
问题是你在故事板中创建了一个场景,但你没有给视图控制器的视图任何子视图或连接任何出口,所以界面是空白的。
如果您的目标是重用与多个不同视图控制器class实例相关的视图和子视图的集合,如果您不想在代码中创建它们,最简单的方法是将它们放在 .xib 文件中,并在视图控制器自己的视图加载过程之后将其加载到代码中(例如在 viewDidLoad
)。
但是如果目标仅仅是"customise the segmented items of segmented control"在这个视图控制器的不同实例中,最简单的方法是有一个视图控制器class和一个相应的界面设计,并在代码。但是,在每种情况下,您 可以 从它自己的 .xib 加载那个分段控件,如果它对您的视觉设计很重要的话。
当你通过instantiateViewControllerWithIdentifier
实例化一个view controller时,过程基本上如下:
- 它找到具有该标识符的场景;
- 它确定该场景的基础class;和
- 它 returns 是 class 的一个实例。
然后,当您第一次访问 view
时,它将:
- 创建故事板场景中概述的视图层次结构;和
- 连接插座。
(这个过程实际上比这更复杂,但我试图将它简化为这个工作流程中的关键要素。)
此工作流的含义是出口和基地 class 由您传递给 instantiateViewControllerWithIdentifier
的唯一情节提要标识符确定。因此,对于您的基地 class 的每个子 class,您需要一个单独的情节提要场景并将插座连接到该特定子class。
不过,有一种方法可以完成您的要求。您可以让视图控制器实现 loadView
(不要与 viewDidLoad
混淆),并让它以编程方式创建视图控制器所需的视图层次结构,而不是将故事板场景用于视图控制器 class。 Apple 曾经在他们的 View Controller Programming Guide for iOS, 中很好地介绍了这个过程,但后来停止了该讨论,但仍然可以在他们的 legacy documentation.
话虽如此,我个人不会被迫回到以编程方式创建视图的旧世界,除非有一个非常令人信服的案例。我可能更倾向于放弃视图控制器 subclass 方法,并采用类似单个 class 的方法(这意味着我回到了故事板的世界)然后将一些标识符传递给它指示我想要从该场景的特定实例中获得的行为。 如果您想保持一些面向对象的优雅,您可以为数据源实例化自定义 classes 并根据您在此视图控制器中设置的一些 属性 进行委托 [=51] =].
如果您需要真正动态的视图控制器行为,而不是以编程方式创建的视图层次结构,我更倾向于走这条路。 或者,甚至更简单,继续采用您原来的视图控制器子classing 方法,并接受您需要在情节提要中为每个子class.
所以,你有你的基础 class:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
[A] 还有你的子class:
class UsersViewController: UsersViewControllerBase { var text = "Hello!" }
[B] 您的 subclass 将扩展的协议:
protocol SomeProtocol {
var text: String? { get set }
}
[C] 还有一些 class 来处理您的数据。比如单例class:
class MyDataManager {
static let shared = MyDataManager()
var text: String?
private init() {}
func cleanup() {
text = nil
}
}
[D] 还有你的子class:
class UsersViewController: UsersViewControllerBase {
deinit {
// Revert
object_setClass(self, UsersViewControllerBase.self)
MyDataManager.shared.cleanup()
}
}
extension UsersViewController: SomeProtocol {
var text: String? {
get {
return MyDataManager.shared.text
}
set {
MyDataManager.shared.text = newValue
}
}
}
要正确使用 subclass,您需要这样做(类似):
class TestViewController: UIViewController {
...
func doSomething() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
//Instantiate as base
let usersViewController = storyboard.instantiateViewControllerWithIdentifier("UsersViewControllerBase") as! UsersViewControllerBase
//Replace the class with the desired subclass
object_setClass(usersViewController, UsersViewController.self)
//But you also need to access the property 'text', so:
let subclassObject = usersViewController as! UsersViewController
subclassObject.text = "Hello! World."
//Use UsersViewController object as desired. For example:
navigationController?.pushViewController(subclassObject, animated: true)
}
}
编辑:
正如@VyachaslavGerchicov 所指出的,原始答案并非始终有效,因此标记为 [A] 的部分被划掉了。正如这里的答案所解释的那样:
... setClass cannot add instance variables to an object that has already been created.
[B]、[C] 和 [D] 作为变通方法添加。 [C] 的另一种选择是使其成为 UsersViewController
的私有内部 class,以便只有它可以访问该单例。
我想创建一个可重复使用的视图控制器UsersViewControllerBase
。
UsersViewControllerBase
扩展了 UIViewController
,并实现了两个委托(UITableViewDelegate
、UITableViewDataSource
),并且有两个视图(UITableView
、UISegmentedControl
)
目标是继承UsersViewControllerBase
的实现,自定义UsersViewController
class.
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
class UsersViewController: UsersViewControllerBase {
}
情节提要中存在 UsersViewControllerBase
并且所有出口都已连接,指定了标识符。
问题是如何初始化 UsersViewController 以继承 UsersViewControllerBase
当我创建 UsersViewControllerBase
的实例时一切正常
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
但是当我创建 UsersViewController
的实例时,我得到了 nil
个出口
(我创建了一个简单的 UIViewController
并在故事板中将 UsersViewController class 分配给它)
let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController
看来视图没有被继承。
我希望 UsersViewControllerBase
中的 init 方法能够从故事板获取具有视图和出口的控制器:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var tableView: UITableView!
init(){
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
self = usersViewControllerBase //but that doesn't compile
}
}
我会初始化 UsersViewController
:
let usersViewController = UsersViewController()
但不幸的是,这不起作用
问题是你在故事板中创建了一个场景,但你没有给视图控制器的视图任何子视图或连接任何出口,所以界面是空白的。
如果您的目标是重用与多个不同视图控制器class实例相关的视图和子视图的集合,如果您不想在代码中创建它们,最简单的方法是将它们放在 .xib 文件中,并在视图控制器自己的视图加载过程之后将其加载到代码中(例如在 viewDidLoad
)。
但是如果目标仅仅是"customise the segmented items of segmented control"在这个视图控制器的不同实例中,最简单的方法是有一个视图控制器class和一个相应的界面设计,并在代码。但是,在每种情况下,您 可以 从它自己的 .xib 加载那个分段控件,如果它对您的视觉设计很重要的话。
当你通过instantiateViewControllerWithIdentifier
实例化一个view controller时,过程基本上如下:
- 它找到具有该标识符的场景;
- 它确定该场景的基础class;和
- 它 returns 是 class 的一个实例。
然后,当您第一次访问 view
时,它将:
- 创建故事板场景中概述的视图层次结构;和
- 连接插座。
(这个过程实际上比这更复杂,但我试图将它简化为这个工作流程中的关键要素。)
此工作流的含义是出口和基地 class 由您传递给 instantiateViewControllerWithIdentifier
的唯一情节提要标识符确定。因此,对于您的基地 class 的每个子 class,您需要一个单独的情节提要场景并将插座连接到该特定子class。
不过,有一种方法可以完成您的要求。您可以让视图控制器实现 loadView
(不要与 viewDidLoad
混淆),并让它以编程方式创建视图控制器所需的视图层次结构,而不是将故事板场景用于视图控制器 class。 Apple 曾经在他们的 View Controller Programming Guide for iOS, 中很好地介绍了这个过程,但后来停止了该讨论,但仍然可以在他们的 legacy documentation.
话虽如此,我个人不会被迫回到以编程方式创建视图的旧世界,除非有一个非常令人信服的案例。我可能更倾向于放弃视图控制器 subclass 方法,并采用类似单个 class 的方法(这意味着我回到了故事板的世界)然后将一些标识符传递给它指示我想要从该场景的特定实例中获得的行为。 如果您想保持一些面向对象的优雅,您可以为数据源实例化自定义 classes 并根据您在此视图控制器中设置的一些 属性 进行委托 [=51] =].
如果您需要真正动态的视图控制器行为,而不是以编程方式创建的视图层次结构,我更倾向于走这条路。 或者,甚至更简单,继续采用您原来的视图控制器子classing 方法,并接受您需要在情节提要中为每个子class.
所以,你有你的基础 class:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
[A] 还有你的子class:
class UsersViewController: UsersViewControllerBase { var text = "Hello!" }
[B] 您的 subclass 将扩展的协议:
protocol SomeProtocol {
var text: String? { get set }
}
[C] 还有一些 class 来处理您的数据。比如单例class:
class MyDataManager {
static let shared = MyDataManager()
var text: String?
private init() {}
func cleanup() {
text = nil
}
}
[D] 还有你的子class:
class UsersViewController: UsersViewControllerBase {
deinit {
// Revert
object_setClass(self, UsersViewControllerBase.self)
MyDataManager.shared.cleanup()
}
}
extension UsersViewController: SomeProtocol {
var text: String? {
get {
return MyDataManager.shared.text
}
set {
MyDataManager.shared.text = newValue
}
}
}
要正确使用 subclass,您需要这样做(类似):
class TestViewController: UIViewController {
...
func doSomething() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
//Instantiate as base
let usersViewController = storyboard.instantiateViewControllerWithIdentifier("UsersViewControllerBase") as! UsersViewControllerBase
//Replace the class with the desired subclass
object_setClass(usersViewController, UsersViewController.self)
//But you also need to access the property 'text', so:
let subclassObject = usersViewController as! UsersViewController
subclassObject.text = "Hello! World."
//Use UsersViewController object as desired. For example:
navigationController?.pushViewController(subclassObject, animated: true)
}
}
编辑:
正如@VyachaslavGerchicov 所指出的,原始答案并非始终有效,因此标记为 [A] 的部分被划掉了。正如这里的答案所解释的那样:... setClass cannot add instance variables to an object that has already been created.
[B]、[C] 和 [D] 作为变通方法添加。 [C] 的另一种选择是使其成为 UsersViewController
的私有内部 class,以便只有它可以访问该单例。