Une sorte d'architecture MVVM modulaire côté iOS basée sur les données et basée sur le mécanisme d'actualisation de hachage

Avant-propos :

Avis de non-responsabilité : cet article est mon humble opinion, les faits énumérés sont une existence objective et il n'y a pas de critique absolue. Si vos idées sont différentes des miennes, merci de laisser un message pour en discuter ensemble. Si vous avez des suggestions, n'hésitez pas à me corriger. Je ne suis qu'une recrue, et je m'excuse pour la gêne occasionnée.

Les benchmarks architecturaux côté iOS ne sont rien d'autre que le MVC recommandé par Apple, le MVVM classique, le MVP avec un excellent découplage, la nouvelle génération de VIPER et Ribs lancée par Uber, et bien sûr le fameux CCC (tous les codes sont placés dans ViewController) . La norme d'implémentation de l'architecture n'est rien de plus que la résolution de deux problèmes importants en génie logiciel : comment accélérer le développement logiciel ; comment résoudre les problèmes de maintenance ultérieurs du génie logiciel. En prenant CCC comme exemple, la maintenance post-projet de cette architecture est définitivement un casse-tête, et peut même affecter l'itération de nouvelles fonctions.Bien que le découplage MVP soit bon, la logique est plus compliquée. L'architecture populaire restante est MVVM. En plus d'être plus difficile à écrire, cette architecture présente des avantages évidents dans les itérations ultérieures et est relativement claire. Cependant, l'architecture MVVM dans certains projets avec lesquels j'ai été en contact a également des problèmes évidents, c'est-à-dire que le ViewModel est trop gonflé.Bien que le ViewController soit très propre, la logique métier complexe et le code de collage dans le ViewModel en font le deuxième, et c'est encore difficile à maintenir ViewController.

Réflexions provoquées par l'architecture MVVM existante :

  • ViewModel surdimensionné
    L'intention initiale de ViewModel est de résoudre le problème de ViewController surdimensionné, de remplacer une partie des fonctions de ViewController et de connecter la couche View et la couche Model. Cependant, l'abus quelle que soit l'occasion a fait que le ViewModel est devenu de plus en plus grand, ce qui devient un problème dans les itérations ultérieures.
  • Direction logique complexe et code de collage
    Certains événements spéciaux ne peuvent pas être générés par ViewModel, tels que les événements de rappel système. Les ViewModels que j'ai vus sont essentiellement hérités de NSObject, et cette classe elle-même n'a pas trop d'intersection avec le système et l'application. L'exemple le plus évident est que ViewController rappellera après l'initialisation.Certains viewDidLoad()services spéciaux doivent être exécutés dans cette méthode de rappel, tels que les demandes réseau.Cependant, ViewModel lui-même n'a pas cette méthode et il ne peut pas se connecter à cette méthode de rappel dans ViewController Vous ne pouvez écrire qu'une seule viewModelDidLoad()méthode manuellement à rappeler, chacun a son propre style de codage, et au fil du temps ces événements spéciaux rendent ViewModel de plus en plus compliqué.
  • Transmission de flux d'événements
    La transmission de flux d'événements est plus riche sous iOS que sous Android, mais elle crée également une variété de méthodes d'écriture. Le plus simple est que les attributs de la couche View sont exposés en tant qu'attributs publics, puis donnés au contrôle addTarget; il peut également être utilisé delegate, mais la situation de plusieurs à un doit être considérée ; et les caractéristiques qui semblent être liées à MVVM et la liaison directe se reflète dans ios C'est RAC et RxSwift; vous pouvez également utiliser Notification, mais vous devez considérer la scène embarrassante où la notification vole partout dans le ciel et vous ne pouvez pas trouver la tête; le dernier est le bloc.
  • Ecriture compliquée
    Ce problème a toujours été l'inconvénient de MVVM, depuis la couche View, la couche Model, la couche ViewModel, la couche ViewController, c'est un problème délicat.

Idées de conception :

L'idée de conception suit essentiellement la pensée ci-dessus.

La plupart du développement habituel est le ViewController sous la forme d'une longue page et le ViewController affiché dans un espace fixe.Pour cela, vous pouvez vous référer à l'interface de commande et de détails de commande de Meituan Are You Hungry. Le contenu de l'interface est trop long et doit être balayé de haut en bas. Cela nous donne une idée : peut-on utiliser des composants roulants pour gérer ces sous-métiers ? La réponse est oui, et il existe un framework prêt à l'emploi (IGListKit). Donc, si vous y réfléchissez spécifiquement, pouvez-vous le comprendre de cette façon : considérez l'interface comme la majorité de nombreuses UICollectionViewCells, et certaines cellules du même type d'entreprise sont résumées dans une section, qui est gérée par un conteneur (UICollectionView). Cette approche s'est reflétée dans de nombreux produits de l'entreprise. Si je me souviens bien, la « salle d'étude diligente » de Byte et Vipshop sont exactement cette idée.

Est-ce juste pour réécrire directement UICollectionView ? Ma réponse est oui, mais ses détails internes sont inconnus.Il est préférable d'implémenter vous-même un composant de flux de liste et d'itérer en fonction de celui-ci.

Par rapport au développement Android, il a Activity (similaire à ViewController dans iOS), Fragment (classe d'affichage de fragment attachée à Activity), Item (similaire à UICollectionViewCell), Bundle (classe spéciale utilisée pour enregistrer des données), et a trouvé que c'est juste pour prendre ces Répondre aux critères.
L'activité est considérée comme UICollectionView, le fragment est considéré comme le nœud Section dans UICollectionView et l'élément est directement hérité de UIView (la disposition interne de Cell comporte de nombreuses couches, donc une UIView propre est directement sélectionnée ici).

À l'heure actuelle, l'une des choses les plus populaires est appelée "pilotée par les données", c'est-à-dire que l'objet actualisé lui-même n'est pas actualisé directement, mais indirectement via d'autres classes à l'aide de méthodes et de paramètres spécifiés. Ensuite, ce paramètre peut être compris comme le Bundle ci-dessus. Ici, le Bundle est conçu non seulement comme une classe de stockage de données (Modèle typique), mais également pour enregistrer la trame du contrôle, et la variable de bloc est utilisée pour le traitement des événements. Voici le traitement du flux d'événements mentionné dans les considérations ci-dessus. Les avantages de l'utilisation de blocs sont évidents. Semblable à l'écouteur de classe anonyme dans Android, les parties d'amarrage sont un à un, et il n'y a pas de plusieurs à un situation. Cela peut être rapidement Trouver le traitement logique à partir de la couche Vue. En plus des références circulaires, vous devez écrire plus de blocs. Je pense que c'est une meilleure méthode de traitement jusqu'à présent.

Comment faire face à la direction logique complexe? En raison de la nature fermée d'iOS, nous ne connaissons pas les détails d'implémentation interne du système, et nous ne pouvons pas obtenir le contexte spécifique, nous utilisons donc ici une manière approximative de le gérer : c'est-à-dire que le ViewController appelle manuellement la méthode de déclaration de l'activité, et l'activité appelle alors manuellement la méthode de déclaration du fragment. Bien qu'il soit très simple, l'effet réel est très bon et les effets courants tels que viewDidLoad(), viewWillAppear(), viewDidAppear()etc. peuvent être capturés. Bien que la méthode soit simple et grossière, elle est très efficace et les noms de ces méthodes de rappel sont unifiés, donc ça a l'air mieux.

Quant au problème plus lourd de l'écriture, j'y ai longuement réfléchi, mais je n'ai pas trouvé comment obtenir le meilleur en termes de vitesse de développement et de maintenabilité ultérieure, je ne peux donc choisir que la maintenabilité. S'il y a des grands gars qui ont une meilleure façon de gérer cela, veuillez laisser un message pour en discuter ensemble.

Gestion des problèmes :

  • Comment résoudre le problème que le ViewModel est trop gonflé ?
    La solution ici consiste à placer les étapes requises pour l'initialisation dans des sous-éléments View spécifiques. Par exemple, chaque élément divisé transforme et affecte le bundle via le protocole piloté par les données, traite le bloc, etc. Cela atténue le problème d'initialisation de la couche View dans le ViewModel.

  • Comment résoudre le problème de rafraichissement ? Il est en outre dérivé de la manière de résoudre le problème de rafraîchissement partiel ?
    Item définit une propriété bundle, qui est la clé pour actualiser l'Item. Étant donné que NSObject a un attribut de hachage, l'unicité du bundle peut être jugée par le hachage. Si le hachage du bundle de l'élément initial est le même que celui à actualiser, il est considéré comme étant le même hachage et ne doit être actualisé. S'il s'agit d'une valeur différente, l'ensemble est réputé être mis à jour. S'il doit être actualisé, s'il n'y a pas de valeur, on considère que l'élément est la première fois à effectuer une opération d'actualisation, et la méthode pilotée par les données doit être appelée. De plus, la valeur de hachage du bundle doit être stockée dans une file d'attente spéciale pour garantir que la longueur maximale de la file d'attente de hachage est de 2. C'est l'origine du mécanisme de rafraîchissement du hachage dans le titre. La différence avec IGList est que iglist implémente l'algorithme diff pour l'ajout et la modification. Cela permet de considérer le cas de la réutilisation des cellules. Cet article ne considère pas la situation de réutilisation, donc l'algorithme diff n'est pas nécessaire (en fait, la page d'affichage du flux de données diif sera utilisé) .

  • Comment résoudre le problème d'adaptation aux écrans horizontaux et verticaux ?
    La solution ici est de remplacer cette méthode dans ViewController :

    override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
    
    
        super.willRotate(to: toInterfaceOrientation, duration: duration)
        activity.activityRotate(rawValue: toInterfaceOrientation.rawValue)
    }

On peut voir qu'il s'agit d'appeler manuellement la méthode de rotation, puis de la transmettre au calque d'élément, en réécrivant la méthode de rotation pour traiter le cadre de l'écran horizontal et de l'écran vertical.

Projet final:

Enfin, cet article réalise les fonctionnalités suivantes :

  • Division hiérarchique de ViewControllerActivity-Fragment-Item-Bundle-Model.
  • contenu basé sur les données
  • Actualisation du hachage
  • Adaptation à la rotation de l'écran
  • cycle de vie de l'hôte
  • Ajouts et suppressions au niveau des fragments.

Projet spécifique :
insérez la description de l'image ici
Parmi elles, TextFragment et BlankFragment sont des classes qui facilitent la réalisation uniquement de texte ou de parties vides de Fragment, ce qui est très pratique.
Le code spécifique peut être déplacé vers github pour regarder

Prenez le login comme exemple pour écrire ViewController :

Maintenant que le texte a été beaucoup analysé, il est préférable d'écrire directement un exemple pour voir quel est l'effet.En prenant la connexion comme exemple, tant que le compte est admin et que le mot de passe est 123456, la connexion est réussie.

analyser:

ActivityModel est le ViewModel, Item est la couche View et la couche Handler n'a pas encore été écrite.

insérez la description de l'image ici

  • Couche ViewController :

LoginDemoViewController.swift

import UIKit

class LoginDemoViewController: UIViewController{
    
    
    
    var naviBar: SGNavigationBar!
    var activity: SGActivity!
    var activityModel: LoginActivityModel!

    override func viewDidLoad() {
    
    
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        
        initView()
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
    
    
        super.viewWillAppear(animated)
        self.activity.activityWillAppear()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
    
    
        super.viewWillDisappear(animated)
        self.activity.activityWillDisappear()
    }
    
    override func viewDidAppear(_ animated: Bool) {
    
    
        super.viewDidAppear(animated)
        self.activity.activityDidAppear()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
    
    
        super.viewDidDisappear(animated)
        self.activity.activityDidDisappear()
    }
    
    override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
    
    
        super.willRotate(to: toInterfaceOrientation, duration: duration)
        activity.activityRotate(rawValue: toInterfaceOrientation.rawValue)
    }
    
    private func initView(){
    
    
        self.view.backgroundColor = .white
        naviBar = SGNavigationBar(title: "Login", leftText: "")
        naviBar.isEnableDividerLine = true
        naviBar.isBlur = false
        naviBar.setOnLeftClickListener {
    
    
            self.dismiss(animated: true)
        }
        self.view.addSubview(naviBar)
        
        activity = SGActivity(frame: CGRect(x: 0,
                                            y: naviBar.frame.maxY,
                                            width: self.view.frame.width,
                                            height: kSCREEN_HEIGHT - naviBar.frame.maxY))
        activityModel = LoginActivityModel()
        activityModel.context = self
        activity.activityDelegate = activityModel
        self.view.addSubview(activity)
        activity.activityDidLoad()
    }
    
}
  • Couche d'objet :
import Foundation
import UIKit

class TextFieldItem: SGItem{
    
    
    
    public lazy var accountTextFiled:  UITextField = self.createAccountTextField()
    public lazy var passwordTextField: UITextField = self.createPasswordTextField()
    private var textFiledBundle: TextFieldBundle?
    
    override init(frame: CGRect) {
    
    
        super.init(frame: frame)
        
        initView()
    }
    
    required init?(coder: NSCoder) {
    
    
        fatalError("init(coder:) has not been implemented")
    }
    
    override func bindBundle(_ bundle: Any?) {
    
    
        self.textFiledBundle = bundle as? TextFieldBundle
        
    }
    
    override func bindBundleLandscape(_ bundle: Any?) {
    
    
        
    }
    
    override func itemWillRotate(rawValue: Int) {
    
    
        switch rawValue{
    
    
        case 1:
            accountTextFiled.frame = CGRect(x: 16,
                                            y: 5,
                                            width: kSCREEN_WIDTH - 16 * 2,
                                            height: 30)
            passwordTextField.frame = CGRect(x: 16,
                                             y: accountTextFiled.frame.maxY + 10,
                                             width: kSCREEN_WIDTH - 16 * 2,
                                             height: 30)
        case 3:
            accountTextFiled.frame = CGRect(x: 16,
                                            y: 3,
                                            width: kSCREEN_HEIGHT - 16 * 2,
                                            height: 30)
            passwordTextField.frame = CGRect(x: 16,
                                             y: accountTextFiled.frame.maxY + 3,
                                             width: kSCREEN_HEIGHT - 16 * 2,
                                             height: 30)
        case 4:
            accountTextFiled.frame = CGRect(x: 16,
                                            y: 3,
                                            width: kSCREEN_HEIGHT - 16 * 2,
                                            height: 30)
            passwordTextField.frame = CGRect(x: 16,
                                             y: accountTextFiled.frame.maxY + 3,
                                             width: kSCREEN_HEIGHT - 16 * 2,
                                             height: 30)
        default:
            break
        }
    }
    
}

extension TextFieldItem{
    
    
    
    private func initView(){
    
    
        self.addSubview(accountTextFiled)
        self.addSubview(passwordTextField)
    }
    
    private func createAccountTextField() -> UITextField{
    
    
        let textFiled = UITextField()
        textFiled.borderStyle = .roundedRect
        textFiled.frame = CGRect(x: 16, y: 5, width: kSCREEN_WIDTH - 16 * 2, height: 30)
        textFiled.placeholder = "Input account please."
        return textFiled
    }
    
    private func createPasswordTextField() -> UITextField{
    
    
        let textFiled = UITextField()
        textFiled.borderStyle = .roundedRect
        textFiled.frame = CGRect(x: 16, y: accountTextFiled.frame.maxY + 10, width: kSCREEN_WIDTH - 16 * 2, height: 30)
        textFiled.placeholder = "Input password please."
        return textFiled
    }
    
}

ButtonItem.swift

import UIKit

class ButtonItem: SGItem{
    
    
    
    private lazy var registerButton:  UIButton = self.createRegisterButton()
    private lazy var loginButton : UIButton = self.createLoginButton()
    private var buttonBundle: ButtonBundle?
    
    override init(frame: CGRect) {
    
    
        super.init(frame: frame)
        
        initView()
    }
    
    required init?(coder: NSCoder) {
    
    
        fatalError("init(coder:) has not been implemented")
    }
    
    override func bindBundle(_ bundle: Any?) {
    
    
        self.buttonBundle = bundle as? ButtonBundle
        
    }
    
    override func bindBundleLandscape(_ bundle: Any?) {
    
    
        
    }
    
    override func itemWillRotate(rawValue: Int) {
    
    
        switch rawValue{
    
    
        case 1:
            registerButton.frame = CGRect(x: 50, y: 5, width: 60, height: 26)
            loginButton.frame = CGRect(x: kSCREEN_WIDTH - 50 - 60, y: 5, width: 60, height: 26)
        case 3:
            registerButton.frame = CGRect(x: 50, y: 5, width: 60, height: 26)
            loginButton.frame = CGRect(x: kSCREEN_HEIGHT - 50 - 60, y: 5, width: 60, height: 26)
        case 4:
            registerButton.frame = CGRect(x: 50, y: 5, width: 60, height: 26)
            loginButton.frame = CGRect(x: kSCREEN_HEIGHT - 50 - 60, y: 5, width: 60, height: 26)
        default:
            break
        }
    }
    
}

extension ButtonItem{
    
    
    
    private func initView(){
    
    
        self.addSubview(registerButton)
        self.addSubview(loginButton)
    }
    
    private func createRegisterButton() -> UIButton{
    
    
        let button = UIButton(type: .system)
        button.frame = CGRect(x: 50, y: 5, width: 60, height: 26)
        button.setTitle("Register", for: .normal)
        button.setOnClickListener {
    
     v in
            if self.buttonBundle?.registerClosure != nil{
    
    
                self.buttonBundle?.registerClosure!()
            }
        }
        return button
    }
    
    private func createLoginButton() -> UIButton{
    
    
        let button = UIButton(type: .system)
        button.frame = CGRect(x: kSCREEN_WIDTH - 50 - 60, y: 5, width: 60, height: 26)
        button.setTitle("Login", for: .normal)
        button.setOnClickListener {
    
     v in
            if self.buttonBundle?.loginClosure != nil{
    
    
                self.buttonBundle?.loginClosure!()
            }
        }
        return button
    }
    
}

  • Fragment层
    TextFieldFragment.swift
import UIKit

class TextFieldFragment: SGFragment, SGFragmentDelegate{
    
    
    
    lazy var textFieldBundle: TextFieldBundle = {
    
    
        return TextFieldBundle()
    }()
    
    lazy var textFieldItem: TextFieldItem = {
    
    
        let item = TextFieldItem()
        item.bundle = textFieldBundle
        item.size = CGSize(width: kSCREEN_WIDTH, height: 130)
        return item
    }()
    
    override init() {
    
    
        super.init()
        
        self.items.append(textFieldItem)
        
        self.delegate = self
    }
    
    func numberOfItemForFragment(_ fragment: SGFragment) -> Int {
    
    
        return items.count
    }
    
    func itemAtIndex(_ index: Int, fragment: SGFragment) -> SGItem {
    
    
        return items[index]
    }
    
}

extension TextFieldFragment{
    
    
   
    public func getAcccountText() -> String?{
    
    
        return textFieldItem.accountTextFiled.text ?? ""
    }
    
    public func getPasswordText() -> String?{
    
    
        return textFieldItem.passwordTextField.text ?? ""
    }
    
}

ButtonFragment.swift

import UIKit

class ButtonFragment: SGFragment, SGFragmentDelegate{
    
    
    
    private lazy var buttonBundle: ButtonBundle = {
    
    
        let bundle = ButtonBundle()
        bundle.registerClosure = {
    
     [weak self] in
            if self?.registerClosure != nil{
    
    
                self?.registerClosure!()
            }
        }
        bundle.loginClosure = {
    
     [weak self] in
            if self?.loginClosure != nil{
    
    
                self?.loginClosure!()
            }
        }
        return bundle
    }()
    
    private lazy var buttonItem: ButtonItem = {
    
    
        let item = ButtonItem()
        item.bundle = buttonBundle
        item.size = CGSize(width: kSCREEN_WIDTH, height: 100)
        return item
    }()
    
    public var loginClosure: (() -> Void)?
    public var registerClosure: (() -> Void)?
    
    override init() {
    
    
        super.init()
        
        self.items.append(buttonItem)
        
        self.delegate = self
    }
    
    func numberOfItemForFragment(_ fragment: SGFragment) -> Int {
    
    
        return items.count
    }
    
    func itemAtIndex(_ index: Int, fragment: SGFragment) -> SGItem {
    
    
        return items[index]
    }
    
}

  • Bundle层 :
    ButtonBundle.swift
import UIKit

class ButtonBundle: NSObject{
    
    
    
    public var loginClosure: (() -> Void)?
    public var registerClosure: (() -> Void)?
    
}
  • ActivityModel层:
    LoginActivityModel.swift
import UIKit
import Foundation

class LoginActivityModel: NSObject, SGActivityDelegate{
    
    
    
    public weak var context: UIViewController?
    
    private let ACCOUNT: String = "admin"
    private let PASSWORD: String = "123456"
    
    private var fragments: Array<SGFragment>!
    
    private lazy var textFieldFragment: TextFieldFragment = {
    
    
        let fragment = TextFieldFragment()
        return fragment
    }()
    
    private lazy var buttonFragment: ButtonFragment = {
    
    
        let fragment = ButtonFragment()
        fragment.loginClosure = {
    
     [weak self] in
            self?.loginEvent()
        }
        fragment.registerClosure = {
    
     [weak self] in
            self?.registerEvent()
        }
        return fragment
    }()
    
    var clickAction: (() -> Void)?
    
    override init() {
    
    
        super.init()
        
        initData()
    }
    
    private func initData(){
    
    
        fragments = Array<SGFragment>()
        
        let notice = SGTextFragment(text: "Start A Journey")
        let blank1 = SGBlankFragment(height: 80)
        let blank2 = SGBlankFragment(height: 10)
        
        fragments.append(notice)
        fragments.append(blank1)
        fragments.append(textFieldFragment)
        fragments.append(blank2)
        fragments.append(buttonFragment)
        
    }
    
}

// MARK: - Event
extension LoginActivityModel{
    
    
    
    private func loginEvent(){
    
    
        if textFieldFragment.getAcccountText() == ACCOUNT && textFieldFragment.getPasswordText() == PASSWORD {
    
    
            context?.toast("Login Succeed.", location: .center)
            Log.debug("Login succeed.")
        }
    }
    
    private func registerEvent(){
    
    
        if textFieldFragment.getAcccountText() != ACCOUNT {
    
    
            Log.debug("Register Done.")
        }
    }
}

extension LoginActivityModel{
    
    
    
    func numberOfSGFragmentForSGActivity(_ activity: SGActivity) -> Int {
    
    
        return fragments.count
    }

    func fragmentAtIndex(_ activity: SGActivity, index: Int) -> SGFragment {
    
    
        return fragments[index]
    }
    
    func topFragmentForSGActivity(_ activity: SGActivity) -> SGFragment? {
    
    
        return SGTextFragment(text: "Swiped to the top")
    }
    
}

Résultats de connexion

État par défaut de l'écran vertical :
insérez la description de l'image ici

Atterri avec succès :
insérez la description de l'image ici

État de l'écran horizontal :
insérez la description de l'image ici
on peut voir que le couplage de la couche ViewController et de la couche ActivityModel a été beaucoup amélioré, le flux de données est clair et simple, la transmission des événements est normale et les écrans horizontaux et verticaux sont également adaptés normalement .

Je suppose que tu aimes

Origine blog.csdn.net/kicinio/article/details/126689051
conseillé
Classement