web-dev-qa-db-fra.com

Comment sous-classer UITableViewController dans Swift

Je veux sous-classer UITableViewController et pouvoir l'instancier en appelant un initialiseur par défaut sans argument.

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

Depuis le Xcode 6 Beta 5, l'exemple ci-dessus ne fonctionne plus.

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

NOTE Ce bogue est corrigé dans iOS 9, donc tout le problème sera discuté à ce stade. La discussion ci-dessous ne s'applique qu'au système et à la version particuliers de Swift auxquels il est explicitement adapté.


Il s'agit clairement d'un bug, mais il existe également une solution très simple. Je vais expliquer le problème et ensuite donner la solution. Veuillez noter que j'écris ceci pour Xcode 6.3.2 et Swift 1.2; Apple a été partout sur la carte depuis le jour Swift est sorti en premier, donc les autres versions se comporteront différemment.

La terre d'être

Vous allez instancier UITableViewController à la main (c'est-à-dire en appelant son initialiseur dans le code). Et vous voulez sous-classer UITableViewController car vous avez des propriétés d'instance que vous souhaitez lui donner.

Le problème

Donc, vous commencez avec une propriété d'instance:

class MyTableViewController: UITableViewController {
    let greeting : String
}

Cela n'a pas de valeur par défaut, vous devez donc écrire un initialiseur:

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

Mais ce n'est pas un initialiseur légal - vous devez appeler super. Supposons que votre appel à super consiste à appeler init(style:).

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

Mais vous ne pouvez toujours pas compiler, car vous devez implémenter init(coder:). Vous faites donc:

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

Votre code se compile maintenant! Vous êtes maintenant heureux (vous pensez) d'instancier cette sous-classe de contrôleur de vue de table en appelant l'initialiseur que vous avez écrit:

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

Tout semble joyeux et rose jusqu'à ce que vous exécutiez l'application, à quel point vous plantez avec ce message:

erreur fatale: utilisation d'un initialiseur non implémenté init(nibName:bundle:)

Quelles sont les causes de l'accident et pourquoi vous ne pouvez pas le résoudre

Le crash est provoqué par un bug dans Cocoa. À votre insu, init(style:) lui-même appelle init(nibName:bundle:). Et il l'appelle sur self. C'est vous - MyTableViewController. Mais MyTableViewController n'a pas d'implémentation de init(nibName:bundle:). Et n'hérite pas init(nibName:bundle:), non plus, car vous avez déjà fourni un initialiseur désigné, coupant ainsi l'héritage.

Votre seule solution serait d'implémenter init(nibName:bundle:). Mais vous ne pouvez pas, car cette implémentation vous obligerait à définir la propriété d'instance greeting - et vous ne savez pas à quoi la définir.

La solution simple

La solution simple - presque trop simple, c'est pourquoi il est si difficile d'y penser - est: ne pas sous-classer UITableViewController. Pourquoi est-ce une solution raisonnable? Parce que vous n'avez jamais réellement besoin de le sous-classer en premier lieu. UITableViewController est une classe largement inutile; cela ne fait rien pour vous que vous ne pouvez pas faire pour vous-même.

Donc, maintenant, nous allons réécrire notre classe en tant que sous-classe UIViewController à la place. Nous avons toujours besoin d'une vue de table comme vue, nous allons donc la créer dans loadView, et nous la raccorderons également là-bas. Les modifications sont marquées comme des commentaires suivis:

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

Vous voudrez aussi, bien sûr, ajouter les méthodes minimales requises de source de données. Nous instancions maintenant notre classe comme ceci:

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

Notre projet se compile et s'exécute sans accroc. Problème résolu!

Une objection - pas

Vous pourriez objecter qu'en n'utilisant pas UITableViewController, nous avons perdu la possibilité d'obtenir une cellule prototype du storyboard. Mais ce n'est pas une objection, car nous n'avons jamais eu cette capacité en premier lieu. Rappelez-vous, notre hypothèse est que nous sous-classons et appelons l'initialiseur de notre propre sous-classe. Si nous obtenions la cellule prototype du storyboard, le storyboard nous instancierait en appelant init(coder:) et le problème ne se serait jamais posé en premier lieu.

59
matt

Xcode 6 Beta 5

Il semble que vous ne puissiez plus déclarer un initialiseur de commodité sans argument pour une sous-classe UITableViewController. Au lieu de cela, vous devez remplacer l'initialiseur par défaut.

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)
    }
}
20
Nick Snyder

Je l'ai fait comme ça

class TestViewController: UITableViewController {

    var dsc_var: UITableViewController?

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

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

La création et l'affichage d'une instance de TestViewController dans un UISplitViewController a fonctionné pour moi avec ce code. C'est peut-être une mauvaise pratique, dites-moi si c'est le cas (tout juste commencé avec Swift).

Pour moi, il y a toujours un problème quand il y a des variables non optionnelles et la solution de Nick Snyder est la seule qui fonctionne dans cette situation
Il n'y a qu'un seul problème: les variables sont initialisées 2 fois.

Exemple:

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)
}
3
Manuel Wa

Les accessoires au mat pour une excellente explication. J'ai utilisé à la fois les solutions de matt et de @Nick Snyder, mais j'ai rencontré un cas dans lequel aucun ne fonctionnerait tout à fait, car je devais (1) initialiser les champs let, (2) utiliser la fonction init(style: .Grouped) (sans obtenir d'erreur d'exécution), et (3) utilisez le refreshControl intégré (de UITableViewController). Ma solution de contournement consistait à introduire une classe intermédiaire MyTableViewController dans ObjC, puis à utiliser cette classe comme base de mes contrôleurs de vue de table.

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

Ajoutez ce qui suit à Bridging-Header.h du projet

#import "MyTableViewController.h"

Ensuite, utilisez dans Swift. Exemple: "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
ɲeuroburɳ

la réponse de Matt est la plus complète, mais si vous souhaitez utiliser un tableViewController dans le style .plain (par exemple pour des raisons héritées). Il ne vous reste plus qu'à appeler

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

au lieu de

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

1
John Stricker

Je ne suis pas sûr que cela soit lié à votre question, mais si vous souhaitez lancer le contrôleur UITableView avec xib, les notes de version Xcode 6.3 beta 4 fournissent une solution de contournement:

  • Dans votre projet Swift, créez un nouveau fichier iOS Objective-C vide. Cela déclenchera une feuille vous demandant "Voulez-vous configurer un en-tête de pontage Objective-C".
  • Appuyez sur "Oui" pour créer un en-tête de pontage
  • À l'intérieur de [YOURPROJECTNAME] -Bridging-Header.h, ajoutez le code suivant:
@import UIKit;
@interface UITableViewController() // Extend UITableViewController to work around 19775924
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER ;
@end
0
Cfr

J'ai remarqué une erreur similaire lors de l'utilisation de cellules statiques tableview et vous devez implémenter ceci:

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

si vous implémentez:

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

J'étais en train de faire un crash là-bas ... Un peu comme prévu. J'espère que cela t'aides.

0
C0D3
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")
0
neoneye

Je voulais sous-classer UITableViewController et ajouter une propriété non facultative qui nécessite de remplacer l'initialiseur et de traiter tous les problèmes décrits ci-dessus.

L'utilisation d'un Storyboard et d'un enchaînement vous offre plus d'options si vous pouvez travailler avec un var facultatif plutôt qu'avec une entrée non facultative dans votre sous-classe de UITableViewController

En appelant performSegueWithIdentifier et en remplaçant prepareForSegue dans votre contrôleur de vue de présentation, vous pouvez obtenir l'instance de la sous-classe UITableViewController et définir les variables facultatives avant la fin de l'initialisation:

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
user3030674