ハッシュ更新メカニズムに基づいて考えられた、モジュール化された iOS 側のデータ駆動型 MVVM アーキテクチャの一種

序文:

免責事項:この記事は私の個人的な意見であり、記載されている事実は客観的な存在であり、絶対的な批判ではありません。あなたの考えが私の考えと異なる場合は、一緒に議論するためにメッセージを残してください。何か提案があれば、修正してください。私は単なる新人なので、ご迷惑をおかけして申し訳ありません。

iOS 側のアーキテクチャ ベンチマークは、Apple が推奨する MVC、クラシックな MVVM、優れたデカップリングを備えた MVP、Uber が発表した新世代の VIPER と Ribs、そしてもちろん悪名高い CCC (すべてのコードは ViewController に配置されています) にすぎません。 。アーキテクチャの実装標準は、ソフトウェア エンジニアリングにおける 2 つの重要な問題、つまりソフトウェア開発をどのようにスピードアップするか、ソフトウェア エンジニアリングのその後のメンテナンスの問題をどのように解決するかを解決することに他なりません。CCC を例にとると、このアーキテクチャのプロジェクト後のメンテナンスは間違いなく頭痛の種であり、新しい機能の反復に影響を与える可能性さえあります。MVP の分離は優れていますが、ロジックはより複雑です。残りの人気のあるアーキテクチャは MVVM ですが、このアーキテクチャは作成が面倒であることに加えて、後の反復において明らかな利点があり、比較的明確です。ただし、私が関わっているいくつかのプロジェクトの MVVM アーキテクチャにも明らかな問題があります。つまり、ViewModel が肥大化しすぎています。ViewController は非常にクリーンですが、ViewModel 内の複雑なビジネス ロジックとグルー コードにより、2 番目のものが発生します。そしてそれを維持するのは依然として困難ですViewController

既存の MVVM アーキテクチャによって引き起こされる考え:

  • ViewModel の肥大化 ViewModel
    の本来の目的は、ViewController の肥大化の問題を解決し、ViewController の機能の一部を置き換え、View 層と Model 層を接続することです。ただし、場面を問わず乱用すると ViewModel がどんどん大きくなり、後の反復で問題になります。
  • 複雑なロジックの方向とグルー コード システム コール
    バック イベントなど、一部の特殊なイベントは ViewModel では生成できません。私が見た ViewModel は基本的に から継承されておりNSObject、このクラス自体はシステムやアプリケーションとあまり交差しません。最も明白な例は、 ViewController が 初期化 後にコールバックすることですviewDidLoad()。ネットワーク リクエストなど、一部の特別なサービスはこのコールバック メソッドで操作する必要があります。ただし、ViewModel 自体にはこのメソッドがなく、ViewController でこのコールバック メソッドにフックすることはできません呼び出すメソッドは手動で 1 つしか作成できませんがviewModelDidLoad()、誰もが独自のコーディング スタイルを持っており、時間の経過とともに、これらの特別なイベントにより ViewModel がますます複雑になります。
  • イベントストリームの送信
    イベントストリームの送信はAndroidに比べてiOSの方が豊富ですが、その分様々な書き方が発生します。最も単純なのは、View レイヤーの属性をパブリック属性として公開し、コントロールに与えることです。addTargetこれを使用することもできますdelegateが、多対 1 の状況を考慮する必要があり、束縛されそうな特性も考慮する必要があります。 MVVMへのダイレクトバインディングはiosに反映されます RACとRxSwiftです; 通知も使えますが、通知が空を飛び交って頭が見つからない恥ずかしいシーンを考慮する必要があります; 最後はブロック。
  • 複雑な記述
    この問題は常に MVVM の欠点であり、View 層、Model 層、ViewModel 層、ViewController 層に至るまで、これは扱いにくい問題です。

デザインのアイデア:

デザインの考え方は基本的に上記の考え方に従います。

通常の開発は長いページ形式の ViewController と固定スペースに表示される ViewController がほとんどですが、これについては Meituan Are You Hungry の注文と注文詳細インターフェイスを参照してください。インターフェイスのコンテンツが長すぎるため、上下にスワイプする必要があります。これにより、ローリング コンポーネントを使用してこれらのサブビジネスを管理できるかどうかというアイデアが得られます。答えは「はい」です。既製のフレームワーク (IGListKit) があります。具体的に考えると、インターフェイスを多数のUICollectionViewCellの大部分とみなして、同じ種類の業務のいくつかのセルがSectionにまとめられ、それがコンテナ(UICollectionView)で管理されていると考えると理解できるでしょうか。この考え方は多くの同社製品に反映されており、私の記憶が正しければ、Byte社の「Diligent Study Room」やVipshopもまさにこの考え方だ。

UICollectionViewを直接書き換えるだけでしょうか?答えは「はい」ですが、内部の詳細は不明なので、リストフローコンポーネントを自分で実装し、それを基に反復処理する方がよいでしょう。

Android開発と比べると、Activity(iOSのViewControllerのようなもの)、Fragment(Activityに付属するフラグメント表示クラス)、Item(UICollectionViewCellのようなもの)、Bundle(データを保存するための特殊なクラス)があり、ちょうど良いことがわかりました。これらを取得するには基準を満たしてください。
ActivityはUICollectionView、FragmentはUICollectionView内のSectionノードとみなされ、ItemはUIViewから直接継承されます(Cellの内部レイアウトは階層が多いため、ここではきれいなUIViewを直接選択しています)。

現在、最も一般的なものの 1 つは「データ駆動型」と呼ばれるものです。つまり、更新されたオブジェクト自体は直接更新されず、指定されたメソッドとパラメーターを使用して他のクラスを通じて間接的に更新されます。この場合、このパラメータは上記のバンドルとして理解できます。ここで、Bundle はデータ ストレージ クラス (典型的な Model) としてだけでなく、コントロールのフレームを保存するために設計されており、ブロック変数はイベント処理に使用されます。上記の考察で述べたイベント フローの処理を示します。ブロックを使用する利点は明らかです。Android の匿名クラス リスナーと同様に、ドッキング パーティは 1 対 1 であり、多対 1 はありませんView層から論理的な処理を素早く見つけることができる 循環参照に加えて、より多くのブロックを記述する必要がある 今のところ、これがより良い処理方法だと思います。

複雑なロジックの方向にどう対処するか? iOS のクローズドな性質により、システムの内部実装の詳細が分からず、特定のコンテキストを取得できないため、ここでは大まかな方法​​で対処します。つまり、ViewController が手動で呼び出します。アクティビティの宣言メソッドを呼び出した後、アクティビティはフラグメントの宣言メソッドを手動で呼び出します。非常にシンプルですが実際の効果は非常に優れており、viewDidLoad()viewWillAppear()viewDidAppear()などの一般的なものがキャプチャできます。シンプルで乱暴なメソッドですが、非常に効果的です。また、これらのコールバック メソッドの名前が統一されているため、見栄えが良くなります。

さらに厄介な書き込みの問題については、長い間考えてきましたが、開発速度とその後の保守性の点で最良の方法を見つけることができず、保守性を選択するしかありません。もっと良い対処法を持っている偉い人がいたら、メッセージを残して一緒に話し合ってください。

問題の処理:

  • ViewModel が肥大化しすぎる問題を解決するにはどうすればよいでしょうか?
    ここでの解決策は、初期化に必要な手順を特定の View サブ項目に入れることです。たとえば、分割された各アイテムは、データ駆動型プロトコルを通じてバンドルを変換および割り当て、ブロックを処理するなどします。これにより、ViewModel 内で View レイヤーを初期化する際の問題が軽減されます。

  • リフレッシュの問題を解決するにはどうすればよいですか? さらに、部分リフレッシュの問題をどのように解決するかということが導き出されます。
    項目は、項目を更新するためのキーとなるバンドル プロパティを設定します。NSObjectはハッシュ属性を持っているため、バンドルの一意性はハッシュで判断でき、初期アイテムのバンドルのハッシュがリフレッシュ対象のバンドルと同じであれば、同じハッシュとみなされ、一致しません。リフレッシュする必要があります。それが異なる値の場合、バンドルは更新されたとみなされます。リフレッシュが必要な場合、値がない場合、その項目は初めてリフレッシュ操作を実行したものとみなされます。データ駆動型メソッドを呼び出す必要があります。さらに、ハッシュ キューの最大長が 2 になるように、バンドルのハッシュ値を特別なキューに保存する必要があります。これが、タイトルにあるハッシュ リフレッシュ メカニズムの起源です。IGList との違いは、iglist は追加と変更のために diff アルゴリズムを実装していることです。これはセルの再利用の場合を考慮するためです。この記事では再利用の状況を考慮していないため、diff アルゴリズムは必要ありません (実際には、データ フロー表示ページdiif が使用されます)。

  • 横画面と縦画面に適応するという問題を解決するにはどうすればよいですか?
    ここでの解決策は、ViewController でこのメソッドをオーバーライドすることです。

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

これは手動でrotateメソッドを呼び出してからアイテムレイヤーに渡し、横画面と縦画面のフレームを処理するようにrotateメソッドを書き換えていることが分かります。

最終プロジェクト:

最後に、この記事では次の機能を実現します。

  • ViewControllerActivity-Fragment-Item-Bundle-Modelの階層分割。
  • データドリブンなコンテンツ
  • ハッシュリフレッシュ
  • 画面回転の適応
  • ホストのライフサイクル
  • フラグメントレベルの追加と削除。

特定のプロジェクト:
ここに画像の説明を挿入
このうち、TextFragment と BlankFragment は、Fragment のテキストまたは空白部分だけを実現しやすくするクラスで、非常に便利です。
特定のコードはgithubに移動して見ることができます

ログインを例として ViewController を作成します。

テキストがかなり分析されたので、効果を確認するために例を直接書いた方が良いでしょう。ログインを例にとると、アカウントが admin、パスワードが 123456 であれば、ログインは成功します。

分析します:

ActivityModel は ViewModel、Item は View レイヤー、Handler レイヤーはまだ書き込まれていません。

ここに画像の説明を挿入

  • 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()
    }
    
}
  • アイテムレイヤー:
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
    }
    
}

  • フラグメント层
    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]
    }
    
}

  • バンドル层:
    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")
    }
    
}

ログイン結果

縦画面のデフォルト状態:
ここに画像の説明を挿入

正常に着陸しました:
ここに画像の説明を挿入

水平画面の状態:
ここに画像の説明を挿入
ViewController 層と ActivityModel 層の両方の結合が大幅に改善され、データ フローが明確かつシンプルになり、イベント送信が正常に行われ、水平および垂直画面も正常に適応されていることがわかります。 。

おすすめ

転載: blog.csdn.net/kicinio/article/details/126689051