web-dev-qa-db-fra.com

Propriété Swift conforme à un protocole et à une classe

@property (strong, nonatomic) UIViewController<UITableViewDelegate> *thing;

Je souhaite implémenter une propriété comme dans ce code Objective-C dans Swift. Alors voici ce que j'ai essayé:

class AClass<T: UIViewController where T: UITableViewDelegate>: UIViewController {
    var thing: T!
}

Cela compile. Mon problème survient lorsque j'ajoute des propriétés à partir du storyboard. La balise @IBOutlet génère une erreur de compilation.

class AClass<T: UIViewController where T: UITableViewDelegate>: UIViewController {
    @IBOutlet weak var anotherThing: UILabel!  // error
    var thing: T!
}

L'erreur:

Variable in a generic class cannot be represented in Objective-C

Est-ce que je mets en œuvre ce droit? Que puis-je faire pour corriger ou contourner cette erreur?

MODIFIER:

Swift 4 a enfin une solution à ce problème. Voir ma réponse mise à jour.

19
keithbhunter

Mise à jour pour Swift 4

Swift 4 prend désormais en charge la représentation d’un type en tant que classe conforme à un protocole. La syntaxe est Class & Protocol. Voici un exemple de code utilisant ce concept de "What's New in Swift" (session 402 de WWDC 2017):

protocol Shakeable {
    func shake()
}
extension UIButton: Shakeable { /* ... */ }
extension UISlider: Shakeable { /* ... */ }

// Example function to generically shake some control elements
func shakeEm(controls: [UIControl & Shakeable]) {
    for control in controls where control.isEnabled {
        control.shake()
    }
}

A partir de Swift 3, cette méthode pose des problèmes car vous ne pouvez pas transmettre les types corrects. Si vous essayez de transmettre [UIControl], il n'a pas la méthode shake. Si vous essayez de transmettre [UIButton], le code est compilé, mais vous ne pouvez pas transmettre de UISliders. Si vous passez [Shakeable], vous ne pourrez pas cocher control.state, car Shakeable ne l’a pas. Swift 4 a finalement abordé le sujet.

Old Answer

Je contourne ce problème pour le moment avec le code suivant:

// This class is used to replace the UIViewController<UITableViewDelegate> 
// declaration in Objective-C
class ConformingClass: UIViewController, UITableViewDelegate {}

class AClass: UIViewController {
    @IBOutlet weak var anotherThing: UILabel!
    var thing: ConformingClass!
}

Cela me semble ridicule. Si l'une des méthodes de délégation était requise, je devrais alors implémenter ces méthodes dans ConformingClass (ce que je ne veux PAS faire) et les remplacer dans une sous-classe.

J'ai posté cette réponse au cas où quelqu'un d'autre rencontrerait ce problème et que ma solution les aiderait, mais je ne suis pas satisfait de la solution. Si quelqu'un publie une meilleure solution, j'accepterai sa réponse.

17
keithbhunter

Ce n'est pas la solution idéale, mais vous pouvez utiliser une fonction générique au lieu d'une classe générique, comme ceci:

class AClass: UIViewController {
  @IBOutlet weak var anotherThing: UILabel!
  private var thing: UIViewController?

  func setThing<T: UIViewController where T: UITableViewDelegate>(delegate: T) {
    thing = delegate
  }
}
6
Farlei Heinen

Je suis tombé sur le même problème et j'ai également essayé l'approche générique. Finalement, l'approche générique a brisé toute la conception. 

Après avoir repensé ce problème, j'ai constaté qu'un protocole qui ne peut pas être utilisé pour spécifier complètement un type (en d'autres termes, doit être accompagné d'informations de type supplémentaires telles qu'un type de classe) a peu de chances d'être complet. De plus, bien que le style Objc de déclaration ClassType<ProtocolType> soit utile, il ignore l'avantage de l'abstraction fourni par protocole, car ce protocole n'élève pas réellement le niveau d'abstraction. De plus, si une telle déclaration apparaît à plusieurs endroits, elle doit être dupliquée. Pire encore, si plusieurs déclarations de ce type sont interdépendantes (un seul objet leur sera éventuellement transmis), le programme devient fragile et difficile à maintenir car plus tard, si la déclaration à un endroit doit être modifiée, toutes les déclarations associées doivent être changé aussi bien.

Solution

Si le cas d'utilisation d'une propriété implique à la fois un protocole (disons ProtocolX) et certains aspects d'une classe (disons ClassX), l'approche suivante pourrait être prise en compte:

  1. Déclarez un protocole supplémentaire héritant de ProtocolX avec les exigences de méthode/propriété ajoutées que ClassX satisfont automatiquement. Comme dans l'exemple ci-dessous, une méthode et une propriété sont les exigences supplémentaires, que UIViewController satisfait automatiquement.

    protocol CustomTableViewDelegate: UITableViewDelegate {
        var navigationController: UINavigationController? { get }
        func performSegueWithIdentifier(identifier: String, sender: AnyObject?)
    }
    
  2. Déclarez un protocole supplémentaire héritant de ProtocolX avec une propriété supplémentaire en lecture seule de type ClassX. Non seulement cette approche autorise-t-elle l'utilisation de ClassX dans son intégralité, mais elle offre également la souplesse nécessaire pour ne pas nécessiter d'implémentation dans la sous-classe ClassX. Par exemple:

    protocol CustomTableViewDelegate: UITableViewDelegate {
        var viewController: UIViewController { get }
    }
    
    // Implementation A
    class CustomViewController: UIViewController, UITableViewDelegate {
    
        var viewController: UIViewController { return self }
    
        ... // Other important implementation
    }
    
    // Implementation B
    class CustomClass: UITableViewDelegate {
    
        private var _aViewControllerRef: UIViewController // Could come from anywhere e.g. initializer
        var viewController: UIViewController { return _aViewControllerRef } 
    
        ... // UITableViewDelegate methods implementation
    }
    

PS. L'extrait ci-dessus est pour démonstration uniquement. Il n'est pas recommandé de mélanger UIViewController et UITableViewDelegate.

Edit for Swift 2+: Merci pour le commentaire de @ Shaps, les éléments suivants pourraient être ajoutés pour éviter de devoir implémenter la propriété souhaitée partout.

extension CustomTableViewDelegate where Self: UIViewController { 
    var viewController: UIViewController { return self } 
}
5
Fujia

vous pouvez déclarer un délégué dans Swift comme ceci: 

weak var delegate : UITableViewDelegate?

Cela fonctionnera même avec un projet hybride (Objective-c et Swift). Delegate devrait être facultatif et faible, car sa disponibilité n’est pas garantie et que faible ne crée pas de cycle de conservation. 

Vous obtenez cette erreur car il n'y a pas de génériques dans Objective-C et cela ne vous permettra pas d'ajouter la propriété @IBOutlet. 

Modifier: 1. Forcer un type sur un délégué

Pour forcer ce délégué à toujours être un UIViewController, vous pouvez implémenter le configurateur personnalisé et lever une exception s'il ne s'agit pas d'UIViewController.

weak var _delegate : UITableViewDelegate? //stored property
var delegate : UITableViewDelegate? {
    set {
        if newValue! is UIViewController {
            _delegate = newValue
        } else {
            NSException(name: "Inavlid delegate type", reason: "Delegate must be a UIViewController", userInfo: nil).raise()
        }
    }
    get {
        return _delegate
    }
}
0
kmithi