40

I want to subclass UITableViewController and be able to instantiate it by calling a default initializer with no arguments.

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

As of the Xcode 6 Beta 5, the example above no longer works.

Overriding declaration requires an 'override' keyword
Invalid redeclaration of 'init()'
Nick Snyder
  • 2,966
  • 1
  • 21
  • 23

9 Answers9

65

NOTE This bug is fixed in iOS 9, so the entire matter will be moot at that point. The discussion below applies only to the particular system and version of Swift to which it is explicitly geared.


This is clearly a bug, but there's also a very easy solution. I'll explain the problem and then give the solution. Please note that I'm writing this for Xcode 6.3.2 and Swift 1.2; Apple has been all over the map on this since the day Swift first came out, so other versions will behave differently.

The Ground of Being

You are going to instantiate UITableViewController by hand (that is, by calling its initializer in code). And you want to subclass UITableViewController because you have instance properties you want to give it.

The Problem

So, you start out with an instance property:

class MyTableViewController: UITableViewController {
    let greeting : String
}

This has no default value, so you have to write an initializer:

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

But that's not a legal initializer - you have to call super. Let's say your call to super is to call init(style:).

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

But you still can't compile, because you have a requirement to implement init(coder:). So you do:

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

Your code now compiles! You now happily (you think) instantiate this table view controller subclass by calling the initializer you wrote:

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

Everything looks merry and rosy until you run the app, at which point you crash with this message:

fatal error: use of unimplemented initializer init(nibName:bundle:)

What Causes the Crash and Why You Can't Solve It

The crash is caused by a bug in Cocoa. Unknown to you, init(style:) itself calls init(nibName:bundle:). And it calls it on self. That's you - MyTableViewController. But MyTableViewController has no implementation of init(nibName:bundle:). And does not inherit init(nibName:bundle:), either, because you already provided a designated initializer, thus cutting off inheritance.

Your only solution would be to implement init(nibName:bundle:). But you can't, because that implementation would require you to set the instance property greeting - and you don't know what to set it to.

The Simple Solution

The simple solution - almost too simple, which is why it is so difficult to think of - is: don't subclass UITableViewController. Why is this a reasonable solution? Because you never actually needed to subclass it in the first place. UITableViewController is a largely pointless class; it doesn't do anything for you that you can't do for yourself.

So, now we're going to rewrite our class as a UIViewController subclass instead. We still need a table view as our view, so we'll create it in loadView, and we'll hook it up there as well. Changes are marked as starred comments:

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
    }
}

Also you'll want, of course, to add the minimal required data source methods. We now instantiate our class like this:

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

Our project compiles and runs without a hitch. Problem solved!

An Objection - Not

You might object that by not using UITableViewController we have lost the ability to get a prototype cell from the storyboard. But that is no objection, because we never had that ability in the first place. Remember, our hypothesis is that we are subclassing and calling our own subclass's initializer. If we were getting the prototype cell from the storyboard, the storyboard would be instantiating us by calling init(coder:) and the problem would never have arisen in the first place.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thanks, excellent answer! I now understand what the problem is. I agree, there's no real need to subclass UITableViewController. – sdduursma Aug 20 '15 at 16:58
  • Don't forget to nullify delegate and dataSource in deinit, it may crash. In iOS9 and higher it is not required deinit { self.tableView?.delegate = nil self.tableView?.dataSource = nil } – Igor Palaguta Sep 21 '15 at 15:04
  • Also note that UITableViewController does additional things that you have to reimplement (and more or less duplicate). Think of clearing selection at certain times, flashing scroll indicators, stuff like that. No big deal, but simply creating a table view and loading it as its view is not all there is to it. – Scott Berrevoets Nov 23 '15 at 05:41
  • Was this bug introduced by the iOS 9 SDK? I had code that was running fine on iOS 8 before I released an update built with the iOS 9 SDK. Now I'm seeing crashes on iOS 8 due to [super initWithStyle:] calling [self initWithNibName:bundle:]. – Jeff V Feb 08 '16 at 22:38
  • 1
    @JeffV _Read_ my answer, please. Look at the original _date_ of my answer. The bug is _fixed_ in iOS 9. The bug is _in_ iOS 8. Thus, code that works in iOS 9 may not work in iOS 8. – matt Feb 08 '16 at 23:31
  • @matt understood, but this bug didn't happen for me when compiling against the iOS 8 SDK. I've sorted out a workaround, but was just surprised to be getting crash logs when I never touched this part of my code during my iOS 9 update. – Jeff V Feb 08 '16 at 23:34
  • @JeffV It sounds to me like you've got something valuable to contribute, so please feel free to add an answer! – matt Feb 08 '16 at 23:35
  • @ matt , I have sub classed UITableViewController and the app crashes stating its a bad instruction at the initialisation . I have initialised as convenience override init(style: UITableViewStyle) { self.init(style: .Grouped) // Custom initialization self.title = NSLocalizedString("mdm.agent.common.desktopCentral", comment : "") } – AnxiousMan Mar 28 '16 at 04:45
  • How do we move this answer to the top?! – Luiz Dias Dec 07 '17 at 03:37
20

Xcode 6 Beta 5

It appears that you can no longer declare a no-argument convenience initializer for a UITableViewController subclass. Instead, you need to override the default initializer.

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)
    }
}
Nick Snyder
  • 2,966
  • 1
  • 21
  • 23
  • 1
    This is insane, I am getting like 20 of these errors. Is there any way to loosen the restriction imposed by the compiler to make it run as it does pre beta 5? – izzy Aug 07 '14 at 14:28
  • @user2237853 I have not found a better way. – Nick Snyder Aug 08 '14 at 21:31
  • As of Xcode 6.3.1 this is still an issue. Its impossible to have a custom init method now for a UITableViewController subclass. It doesn't call your designated initialized instead calling init(nibname:bundle). – Ryan Poolos May 16 '15 at 17:49
3

Props to matt for a great explanation. I've made use of both matt's and @Nick Snyder's solutions, however I ran into a case in which neither would quite work, because I needed to (1) initialize let fields, (2) use init(style: .Grouped) (without getting a runtime error), and (3) use the built-in refreshControl (from UITableViewController). My workaround was to introduce an intermediate class MyTableViewController in ObjC, then use that class as the base of my table view controllers.

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

Add the following to Project's Bridging-Header.h

#import "MyTableViewController.h"

Then use in swift. Example: "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 ...
}
leanne
  • 7,940
  • 48
  • 77
ɲeuroburɳ
  • 6,990
  • 3
  • 24
  • 22
2

I did it like this

class TestViewController: UITableViewController {

    var dsc_var: UITableViewController?

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

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

Creating and displaying a instance of TestViewController in a UISplitViewController did work for me with this code. Maybe this is bad practice, please tell me if it is (just started with swift).

For me there's still a problem when there are non optional variables and the solution of Nick Snyder is the only one working in this situation
There's just 1 problem: The variables are initialized 2 times.

Example:

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)
}
Community
  • 1
  • 1
manuelwaldner
  • 551
  • 1
  • 5
  • 24
2

matt's answer is the most complete, but if you do want to use a tableViewController in the .plain style (say for legacy reasons). Then all you need to do is call

super.init(nibName: nil, bundle: nil)

instead of

super.init(style: UITableViewStyle.Plain) or self.init(style: UITableViewStyle.Plain)

leanne
  • 7,940
  • 48
  • 77
John Stricker
  • 383
  • 3
  • 6
0

I wanted to subclass UITableViewController and add a non-optional property which requires overriding the initializer and dealing with all the problems described above.

Using a Storyboard and a segue gives you more options if you can work with an optional var rather than a non-optional let in your subclass of UITableViewController

By calling performSegueWithIdentifier and overriding prepareForSegue in your presenting view controller, you can get the instance of the UITableViewController subclass and set the optional variables before initialization is completed:

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

I've noticed a similar error when using static tableview cells and you gotta implement this:

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

if you implement:

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

I was just getting a crash there... Kind of as expected. Hope this helps.

C0D3
  • 6,440
  • 8
  • 43
  • 67
0

Not sure it is related to your question, but in case you want to init UITableView controller with xib, Xcode 6.3 beta 4 Release Notes provide a workaround:

  • In your Swift project, create a new empty iOS Objective-C file. This will trigger a sheet asking you “Would you like to configure an Objective-C bridging header.”
  • Tap “Yes” to create a bridging header
  • Inside [YOURPROJECTNAME]-Bridging-Header.h add the following code:
@import UIKit;
@interface UITableViewController() // Extend UITableViewController to work around 19775924
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER ;
@end
Cfr
  • 5,092
  • 1
  • 33
  • 50
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")
neoneye
  • 50,398
  • 25
  • 166
  • 151