Contrôleur, allez-y doucement! Nous sortons le code dans UIView

Avez-vous un gros UIViewController? Pour beaucoup, oui. D'une part, cela fonctionne avec les données, d'autre part - avec l'interface.

Les tâches de séparation de la logique de l'interface sont décrites dans des centaines d'articles sur l'architecture: MVP, MVVM, VIPER. Ils résolvent le problème du flux de données, mais ne répondent pas à la question de savoir comment travailler avec l'interface: à un endroit il reste la création d'éléments, la mise en page, la configuration, le traitement des entrées et l'animation.

Séparons la vue du contrôleur et voyons comment loadView () nous aide.



L'interface d'application pour iOS est la hiérarchie UIView . Les tâches de chaque view : créer des éléments, personnaliser, organiser par endroits, animer. Cela peut être vu à partir des méthodes qui sont dans la classe UIView: addSubview(), drawRect(), layoutSubviews().

Si vous regardez les méthodes de la classe UIViewController , vous pouvez voir qu'elle gère la view: charge, répond aux écrans de chargement et aux actions de l'utilisateur et affiche de nouveaux écrans. Souvent, le code qui devrait être dans UIView , nous écrivons dans les sous-classes de UIViewController , cela le rend trop volumineux. Séparez-le.

loadView ()


Le cycle de vie d'un UIViewController commence par loadView() . Une implémentation simplifiée ressemble à ceci:

 // CustomViewController.swift func loadView() { self.view = UIView() } 

Nous pouvons remplacer la méthode et spécifier notre classe.

super.loadView() n'a pas besoin d'être appelé!

 // CustomViewController.swift override func loadView() { self.view = CustomView() } 

Implémentation de CustomView.swift
 // CustomView.swift final class CustomView { let square: UIView = UIView() init() { super.init() square.backgroundColor = .red addSubview(square) } } 


Le contrôleur charge CustomView, ajoute à la hiérarchie, expose le .frame . La propriété .view sera la classe dont nous avons besoin:

 // CustomViewController.swift print(view) // CustomView 

Mais alors que le compilateur ne connaît pas la classe et pense qu'il existe une UIView normale. Corrigeons cela avec une fonction de conversion de type:

 // CustomViewController.swift func view() -> CustomView { return self.view as! CustomView } 

Vous pouvez maintenant voir les variables CustomView :

 // CustomViewController.swift func viewDidLoad() { super.viewDidLoad() view().square //  } 

Simplifiez avec le type associé
Ruslan Kavetsky a proposé de supprimer la duplication de code en utilisant l'extension de protocole:

 protocol ViewSpecificController { associatedtype RootView: UIView } extension ViewSpecificController where Self: UIViewController { func view() -> RootView { return self.view as! RootView } } 

Pour chaque nouveau contrôleur, il vous suffit de spécifier le protocole et la sous-classe pour son UIView via des typealias :

 // CustomViewController.swift final class CustomViewController: UIViewController, ViewSpecificController { typealias RootView = CustomView func viewDidLoad() { super.viewDidLoad() view().square //  } } 

Code dans une sous-classe de UIView


Création et configuration de contrôles


Les polices, les couleurs, les constantes et la hiérarchie peuvent être définies directement dans le constructeur CustomView:

 // CustomView.swift init() { super.init() backgroundColor = .lightGray addSubview(square) } 

layoutSubviews ()


Le meilleur endroit pour une mise en page manuelle est la méthode layoutSubviews() . Il est appelé à chaque fois que la taille de la view est modifiée, vous pouvez donc vous fier à la taille des bounds pour les calculs corrects:

 // CustomView.swift override func layoutSubviews() { super.layoutSubviews() square.frame = CGRect(x: 0, y: 0: width: 200, height: 200) square.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) } 

Contrôles privés, propriétés publiques


S'il reste du temps, je rend les contrôles de property privés, mais je les gère via des variables ou fonctions publiques «dans le domaine de la connaissance». Un exemple plus simple:

 // CustomView.swift private let square = UIView() var squarePositionIsValid: Bool { didSet { square.backgroundColor = squarePositionIsValid? .green : .red } } func moveSquare(to newCenter: CGPoint) { square.center = newCenter } 

L'avantage de l'encapsulation: la logique interne est cachée derrière l'interface. Par exemple, la validité d'un objet peut être indiquée par la couleur de la zone, pas par le carré, mais le contrôleur n'en saura rien.

Que reste-t-il dans viewDidLoad ()?


Si vous utilisez Interface Builder, alors viewDidLoad() souvent vide. Si vous créez une view dans le code, vous devez alors lier leurs actions via le modèle cible-action, ajouter un UIGestureRecognizer ou lier des délégués.

Personnalisable via Interface Builder


La sous-classe à view peut être configurée via Interface Builder (ci-après IB).

Vous devez sélectionner l'objet de view (pas le contrôleur) et définir sa classe. Il n'est pas nécessaire d'écrire votre propre loadView() , le contrôleur le fera lui-même. Mais UIView toujours UIView type UIView .



IBOutlet dans UIView


Si vous sélectionnez le contrôle dans la view , l'Assistant Éditeur reconnaît la classe UIView et la propose comme deuxième fichier en mode automatique. Vous pouvez donc transférer IBOutlet pour view .



Si ne fonctionne pas
Ouvrez la classe CustomView manuellement, écrivez IBOutlet . Vous pouvez maintenant faire glisser le marqueur et survoler un élément dans IB.



Si vous créez une interface dans le code, tous les objets sont accessibles après init() , mais lorsque vous travaillez avec IB, l'accès à IBOutlet apparaît uniquement après le chargement de l'interface depuis UIStoryboard dans la méthode awakeFromNib() :

 // CustomView.swift func awakeFromNib() { super.awakeFromNib() square.layer.cornerRadius = 8 } 

IBAction dans UIViewController


À mon goût, le contrôleur devrait laisser toutes les actions de l'utilisateur. De standard:

  • action-cible à partir des contrôles
  • déléguer l'implémentation dans UIViewController
  • mise en œuvre de bloc
  • réaction à la Notification

Dans ce cas, l' UIViewController contrôle uniquement l'interface. Tout ce qui concerne la logique métier doit être retiré du contrôleur, mais c'est un choix: MVP, VIPER, etc.

Objectif-c


Dans Objective-C, vous pouvez remplacer complètement le type UIView . Pour ce faire, déclarez la propriété avec la classe souhaitée, remplacez setter et getter , en spécifiant la classe:

 // CustomViewController.m @interface CustomViewController @property (nonatomic) CustomView *customView; @end @implementation - (void)setView:(CustomView *)view{ [super setView:view]; } - (CustomView *)view { return (CustomView *)super.view; } @end 

La fin


Dans l'exemple sur GitHub, vous pouvez regarder la séparation des classes pour une tâche simple: la couleur du carré dépend de sa position (dans la zone verte c'est vert, à l'extérieur c'est rouge).

Plus l'écran est complexe, meilleur est l'effet: le contrôleur est réduit, le code est transféré à sa place. Le code est simplement porté pour être view , mais l'encapsulation facilite l'interaction et la lecture du code. Parfois, la view peut être réutilisée avec un autre contrôleur. Par exemple, différents contrôleurs pour iPhone et iPad réagissent à leur manière à l'apparence du clavier, mais cela ne change pas le code d' view .

J'ai utilisé ce code dans différents projets et avec différentes personnes, chaque fois que l'équipe saluait la simplification et reprenait la pratique. J'espère que ça vous plaira aussi. Tout facile UIViewController !

Source: https://habr.com/ru/post/fr432718/


All Articles