如何在Swift中继承自定义UIViewController?

14
我想创建一个可重复使用的视图控制器UsersViewControllerBaseUsersViewControllerBase继承自UIViewController,并实现了两个代理(UITableViewDelegateUITableViewDataSource),并拥有两个视图(UITableViewUISegmentedControl)。
目标是继承UsersViewControllerBase的实现,并在UsersViewController类中自定义分段控件的分段项。
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类分配给它)

let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController

看起来视图没有被继承。

我期望在UsersViewControllerBase中有一个init方法,该方法从storyboard获取带有视图和输出口的控制器:

  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()

但不幸的是,那并没有起作用。

好的,它们确实是继承而来的,所以可能出了其他问题(比如你的故事板场景设置)。你记得连接那些插座了吗? - matt
我没有在UsersViewController中连接outlets,因为我认为它们已经在UsersViewControllerBase中连接(所以可能是继承的)。而且,这个想法不是在UsersViewController中重新创建相同的视图,而是从UsersViewControllerBase中继承它。 - Alexey
3个回答

7

所以,您有自己的基类:

class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var segmentedControl: UISegmentedControl!
    @IBOutlet weak var tableView: UITableView!
    //implementation of delegates
}

[A] 你的子类: class UsersViewController: UsersViewControllerBase { var text = "Hello!" }

[B] 一个你的子类将要扩展的协议:

protocol SomeProtocol {
    var text: String? { get set }
}

[C] 还需要一些处理数据的类。例如,一个单例类:
class MyDataManager {

    static let shared = MyDataManager()
    var text: String?

    private init() {}

    func cleanup() {
        text = nil
    }
}

[D] 还有你的子类:
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
        }
    }
}

为了正确地使用子类,您需要执行以下操作(类似于此):
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)
    }
}

编辑:

Swift中的object_setClass

... setClass无法向已经创建的对象添加实例变量。

[B],[C]和[D]是一种解决方法。另一个选项是将[C]作为UsersViewController的私有内部类,以便只有它可以访问该单例。


你自己试过吗?它的效果一般般,似乎允许你覆盖现有方法,但是不允许添加变量,如[CustomObject],否则会出现EXC_BAD_ACCESS错误。 - Vyachaslav Gerchicov
@VyachaslavGerchicov,你好!我之前使用过这种方法,它有效。我不确定为什么它对你不起作用。你能否创建一个新的问题来解决你的问题?也许你可以链接到这个问题。 - yoninja
只需在 UsersViewController 中声明 var items: [YourCustomObject]! 并在 doSomething() 中进行赋值即可。但是在赋值时会失败。已尝试使用 String 和其他系统类进行相同的操作 - 成功率为50/50,有时会出现不可预测的错误。 - Vyachaslav Gerchicov

7
当您通过instantiateViewControllerWithIdentifier实例化视图控制器时,该过程基本上如下所示:
  • 它找到具有该标识符的场景;
  • 它确定该场景的基类;和
  • 它返回该类的一个实例。
然后,当您首次访问view时,它将:
  • 按照故事板场景中概述的方式创建视图层次结构;和
  • 连接插座。
(实际上,这个过程比这更复杂,但我试图将其简化为此工作流程中的关键要素。)
这个工作流程的含义是,插座和基类由您传递给instantiateViewControllerWithIdentifier的唯一故事板标识符确定。因此,对于基类的每个子类,您都需要一个单独的故事板场景,并将插座连接到该特定子类。

有一种方法可以实现您的要求。与其为视图控制器使用故事板场景,您可以让视图控制器实现loadView(不要与viewDidLoad混淆),并通过编程方式创建视图控制器类所需的视图层次结构。苹果曾在其iOS应用程序的视图控制器编程指南中对此过程进行了很好的介绍,但已经取消了该讨论,但仍然可以在他们的遗留文档中找到。

说了这么多,个人认为除非有非常强烈的理由,否则我不会被迫回到编程创建视图的旧世界。我可能更倾向于放弃视图控制器子类方法,并采用单一类的方式(这意味着我回到了storyboards的世界),然后传递一些标识符来指示我想要从该场景的特定实例中获得什么行为。如果您希望保持一些面向对象的优雅,您可以基于此视图控制器类中设置的某些属性来实例化数据源和代理自定义类。
如果您需要真正动态的视图控制器行为,而不是程序创建的视图层次结构,我更倾向于走这条路。或者,更简单一些,您可以采用原始的视图控制器子类方法,并接受需要每个子类在storyboard中拥有单独场景的事实。

非常好的解释,非常感谢。我倾向于使用“子类化方法”并为每个子类单独创建场景。至少我可以避免代码重复问题,并且在故事板中复制相等场景也不是什么大问题。 - Alexey
如果您仍然想要具有带有outlets的VC子类的能力,可以采用.xib方法。 创建一个包含所有视图和outlets的.xib,并为其创建一个类。 现在,“UserViewControllerBase”将在“viewDidLoad()”中将此.xib添加到其“self.view”中,并将自定义视图作为成员。然后,“UsersViewController”将继承基类,并从其超类获取自定义视图,并通过成员自定义视图访问所有outlets。 个人而言,我不喜欢这种解决方案,但它比复制storyboard VC要好。 - gutte

0
问题在于你在故事板中创建了一个场景,但是你没有给视图控制器的视图添加任何子视图或连接任何输出口,因此界面是空白的。
如果你的目标是在几个不同的视图控制器类实例中重用一组视图和子视图,最简单的方法(如果你不想在代码中创建它们)就是将它们放在一个 .xib 文件中,并在视图控制器自己的视图加载过程之后(例如在 viewDidLoad 中)通过代码加载它。
但是,如果目标仅仅是在这个视图控制器的不同实例中“自定义分段控件的分段项”,最简单的方法是只有一个视图控制器类和一个相应的接口设计,并在代码中执行自定义。然而,如果你希望在视觉上设计它,你可以在每种情况下仅从它自己的 .xib 中加载该分段控件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接