如何在Swift中创建UITableViewController的子类

40

我想要子类化UITableViewController并且能够通过调用默认初始化方法而无需任何参数来实例化它。

class TestViewController: UITableViewController {
    convenience init() {
        self.init(style: UITableViewStyle.Plain)
    }
}

从Xcode 6 Beta 5起,上述示例不再有效。

Overriding declaration requires an 'override' keyword
Invalid redeclaration of 'init()'

这个漏洞已经在iOS 9中修复。 - matt
9个回答

65

注意:此问题已在iOS 9中得到解决,因此此事将毫无意义。以下讨论仅适用于特定系统和版本的Swift。


显然,这是一个bug,但是也有一个非常简单的解决方案。我会先解释问题,然后给出解决方案。请注意,我是针对Xcode 6.3.2和Swift 1.2编写的;自从Swift首次推出以来,Apple一直在这方面混乱不堪,所以其他版本的行为会有所不同。

存在的理由

您将通过手动实例化UITableViewController(即通过在代码中调用其初始化器)来进行操作。并且您想要子类化UITableViewController,因为您有要赋予它的实例属性。

问题

因此,您首先拥有一个实例属性:

class MyTableViewController: UITableViewController {
    let greeting : String
}

没有默认值,因此您必须编写初始化程序:

class MyTableViewController: UITableViewController {
    let greeting : String
    init(greeting:String) {
        self.greeting = greeting
    }
}

但这不是一个合法的初始化程序 - 你必须调用super。假设你对super的调用是调用init(style:)

class MyTableViewController: UITableViewController {
    let greeting : String
    init(greeting:String) {
        self.greeting = greeting
        super.init(style: .Plain)
    }
}

但是你仍然无法编译,因为你需要实现init(coder:)方法。因此,你可以这样做:

class MyTableViewController: UITableViewController {
    let greeting : String
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    init(greeting:String) {
        self.greeting = greeting
        super.init(style: .Plain)
    }
}

您的代码现在已经可以编译了!您现在可以通过调用您编写的初始化器实例化这个表视图控制器子类,感到非常高兴(您认为是这样):

let tvc = MyTableViewController(greeting:"Hello there")

一切看起来都很美好,直到你运行这个应用程序,此时你会遇到崩溃并显示以下信息:

致命错误:未实现初始化器init(nibName:bundle :)

崩溃的原因以及你为什么无法解决它

崩溃是由Cocoa中的一个bug引起的。你不知道的是,init(style :) 本身调用了init(nibName:bundle :) 。而它在self上调用。也就是说,这是你-MyTableViewController。但是MyTableViewController没有实现init(nibName:bundle:)。而且也没有继承init(nibName:bundle:),因为你已经提供了指定的初始化器,从而断开了继承。

你唯一的解决方案是实现init(nibName:bundle:)。但你不能这样做,因为那样的实现将要求你设置实例属性greeting,而你不知道该设置为什么。

简单的解决方案

简单的解决方案 - 几乎太简单了,这就是为什么它很难想到 - 是:不要子类化UITableViewController。为什么这是一个合理的解决方案?因为你实际上从来没有需要子类化它。UITableViewController是一个基本上没用的类;它为你做的事情都可以自己完成。

所以,现在我们将把我们的类重写为UIViewController的子类。我们仍然需要作为我们的视图的表视图,所以我们将在loadView中创建它,并在那里连接它。更改标记为星号注释:

class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { // *
    let greeting : String
    weak var tableView : UITableView! // *
    init(greeting:String) {
        self.greeting = greeting
        super.init(nibName:nil, bundle:nil) // *
    }
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func loadView() { // *
        self.view = UITableView(frame: CGRectZero, style: .Plain)
        self.tableView = self.view as! UITableView
        self.tableView.delegate = self
        self.tableView.dataSource = self
    }
}

当然,您还需要添加最小必需的数据源方法。我们现在这样实例化我们的类:

let tvc = MyViewController(greeting:"Hello there")

我们的项目编译并运行得很顺利。问题解决了!

一个反驳 - 不是

你可能会认为,通过不使用UITableViewController,我们失去了从Storyboard获取原型单元格的能力。但这不是反驳,因为我们一开始就从未有过这样的能力。请记住,我们的假设是我们正在子类化并调用自己子类的初始化程序。如果我们从Storyboard获取原型单元格,Storyboard将通过调用 init(coder:) 实例化我们,而问题根本不会出现。


谢谢,非常好的答案!我现在明白问题所在了。我同意,没有真正需要子类化UITableViewController。 - sdduursma
不要忘记在deinit中将delegate和dataSource设为nil,否则可能会崩溃。 在iOS9及以上版本中,这不是必需的。deinit { self.tableView?.delegate = nil self.tableView?.dataSource = nil } - Igor Palaguta
还要注意,UITableViewController 还有一些额外的功能需要重新实现(或者更多或少地重复)。比如在某些时候清除选择、闪烁滚动指示器等等。这不是什么大问题,但仅仅创建一个表视图并将其加载为视图并不是全部。 - Scott Berrevoets
1
@JeffV 请阅读我的回答。请查看我回答的原始日期。该漏洞已在iOS 9中得到修复。该漏洞存在于iOS 8中。因此,在iOS 9中运行的代码可能无法在iOS 8中正常工作。 - matt
@ Matt,我已经子类化了UITableViewController,但应用程序在初始化时崩溃,指出这是一个错误的指令。我已经按照以下方式进行了初始化: convenience override init(style: UITableViewStyle) { self.init(style: .Grouped) // 自定义初始化 self.title = NSLocalizedString("mdm.agent.common.desktopCentral", comment : "") } - AnxiousMan
显示剩余4条评论

20

Xcode 6 Beta 5

看起来您不再能为UITableViewController子类声明无参数的便利初始化程序。相反,您需要重写默认初始化程序。

class TestViewController: UITableViewController {
    override init() {
        // Overriding this method prevents other initializers from being inherited.
        // The super implementation calls init:nibName:bundle:
        // so we need to redeclare that initializer to prevent a runtime crash.
        super.init(style: UITableViewStyle.Plain)
    }

    // This needs to be implemented (enforced by compiler).
    required init(coder aDecoder: NSCoder!) {
        // Or call super implementation
        fatalError("NSCoding not supported")
    }

    // Need this to prevent runtime error:
    // fatal error: use of unimplemented initializer 'init(nibName:bundle:)'
    // for class 'TestViewController'
    // I made this private since users should use the no-argument constructor.
    private override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
}

1
这太疯狂了,我得到了大约20个这样的错误。有没有办法放宽编译器所施加的限制,使其像beta 5之前那样运行? - izzy
@user2237853 我还没有找到更好的方法。 - Nick Snyder
截至Xcode 6.3.1,这仍然是一个问题。现在对于UITableViewController子类来说,无法拥有自定义的init方法。它不会调用您指定的初始化方法,而是调用init(nibname:bundle)。 - Ryan Poolos

3

感谢Matt的出色解释。我同时使用了matt@Nick Snyder的解决方案,但遇到了一个问题,因为我需要(1)初始化let字段,(2)使用init(style: .Grouped)(而不会出现运行时错误),以及(3)使用UITableViewController中内置的refreshControl。我的解决方法是在ObjC中引入一个中间类MyTableViewController,然后将该类用作我的表视图控制器的基础。

MyTableViewController.h

#import <UIKit/UIKit.h>
// extend but only override 1 designated initializer
@interface MyTableViewController : UITableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER;
@end

MyTableViewController.m:

#import "MyTableViewController.h"
// clang will warn about missing designated initializers from
// UITableViewController without the next line.  In this case
// we are intentionally doing this so we disregard the warning.
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@implementation MyTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style {
    return [super initWithStyle:style];
}
@end

将以下内容添加到项目的Bridging-Header.h文件中。
#import "MyTableViewController.h"

然后在Swift中使用。示例:“PuppyViewController.swift”:
class PuppyViewController : MyTableViewController {
    let _puppyTypes : [String]
    init(puppyTypes : [String]) {
        _puppyTypes = puppyTypes // (1) init let field (once!)
        super.init(style: .Grouped) // (2) call super with style and w/o error
        self.refreshControl = MyRefreshControl() // (3) setup refresh control
    }
    // ... rest of implementation ...
}

2
我是这样做的。
class TestViewController: UITableViewController {

    var dsc_var: UITableViewController?

    override convenience init() {
        self.init(style: .Plain)

        self.title = "Test"
        self.clearsSelectionOnViewWillAppear = true
    }
}

UISplitViewController中创建并显示TestViewController的实例对我来说是有效的,具体代码如下。也许这是不好的做法,请告诉我是否是(我刚开始学习Swift)。
对于我来说,当存在非可选变量时仍然有问题,而Nick Snyder的解决方案是唯一适用的。
只有一个问题:变量被初始化了2次。
例如:
var dsc_statistcs_ctl: StatisticsController?

var dsrc_champions: NSMutableArray

let dsc_search_controller: UISearchController
let dsrc_search_results: NSMutableArray


override init() {
    dsrc_champions = dsrg_champions!

    dsc_search_controller = UISearchController(searchResultsController: nil)
    dsrc_search_results = NSMutableArray.array()

    super.init(style: .Plain) // -> calls init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) of this class
}

required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

private override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
    // following variables were already initialized when init() was called and now initialized again
    dsrc_champions = dsrg_champions!

    dsc_search_controller = UISearchController(searchResultsController: nil)
    dsrc_search_results = NSMutableArray.array()

    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

这真的很疯狂。唉。 - aehlke

2
马特的回答是最完整的,但如果您确实想在.plain样式中使用tableViewController(例如出于兼容性原因),那么您只需要调用 super.init(nibName: nil, bundle: nil),而不是 super.init(style: UITableViewStyle.Plain)self.init(style: UITableViewStyle.Plain)。请注意保留HTML标签。

0

不确定这是否与您的问题相关,但如果您想使用xib初始化UITableView控制器,则Xcode 6.3 beta 4发布说明提供了一种解决方法:

  • 在您的Swift项目中,创建一个新的空iOS Objective-C文件。这将触发一个工作表,询问您“是否要配置Objective-C桥接标头。”
  • 点击“Yes”以创建桥接标头
  • 在[YOURPROJECTNAME]-Bridging-Header.h中添加以下代码:
@import UIKit;
@interface UITableViewController() // Extend UITableViewController to work around 19775924
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER ;
@end

0

我想要子类化UITableViewController并添加一个非可选属性,这需要重写初始化程序并处理上述所有问题。

如果您可以使用可选变量而不是UITableViewController子类中的非可选变量,则使用Storyboard和segue会为您提供更多选项

通过调用performSegueWithIdentifier并覆盖presenting view controller中的prepareForSegue,您可以获取UITableViewController子类的实例并在初始化完成之前设置可选变量:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "segueA"{
            var viewController : ATableViewController = segue.destinationViewController as ATableViewController
            viewController.someVariable = SomeInitializer()
        }

        if segue.identifier == "segueB"{
            var viewController : BTableViewController = segue.destinationViewController as BTableViewController
            viewController.someVariable = SomeInitializer()
        }

    }

0

我注意到在使用静态表视图单元时会出现类似的错误,你需要实现以下内容:

init(coder decoder: NSCoder) {
    super.init(coder: decoder)
}

如果你实现:

required init(coder aDecoder: NSCoder!) {
        // Or call super implementation
        fatalError("NSCoding not supported")
}

我刚刚遇到了一个崩溃……这有点意料之中。希望这能帮到你。


0
class ExampleViewController: UITableViewController {
    private var userName: String = ""

    static func create(userName: String) -> ExampleViewController {
        let instance = ExampleViewController(style: UITableViewStyle.Grouped)
        instance.userName = userName
        return instance
    }
}

let vc = ExampleViewController.create("John Doe")

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