美文网首页iOS开发之常用技术点
在 iOS 上实现基于协议的 MVP

在 iOS 上实现基于协议的 MVP

作者: 大青虫Insect | 来源:发表于2019-01-17 21:51 被阅读68次

    概述

    如果你做过 Android 开发,那你一定知道,MVP 是 Google 官方推荐的 Android 开发架构。和 iOS 一样,Android 也存在着如果代码不够规范导致 C 层(Activity)过于臃肿的问题。

    对于 MVP ,Android 无论是 Java 或是 Kotlin 都是基于 Interface 来实现的。如果你学习过 Java 或者 Kotlin 就会发现,Interface 和 iOS 里的 Protocol 还是十分相似的,只是 Obj-C 的 Protocol 相比其他几个在功能上稍微弱势一点。

    本篇将教大家如何在 iOS 中使用 Protocol 实现类似 Android 的 MVP 架构。本篇灵感来自以下 Blog
    浅谈 MVP in Android
    Android MVP 十分钟入门!

    Login Demo

    这里我们以登录为例,说一下我们的需求。

    1. 用户输入账号密码,点击登录时判断用户名和密码有没有输入,没有内容时提示用户输入。
    2. 本地模拟网络请求,随机返回用户登录成功或是失败。登录成功时,返回首页并显示用户名。登录失败时,显示 Error 的信息。


      运行效果

    试想一下在传统的 MVC 中我们会如何处理,C 持有两个 UITextField

     @IBOutlet private weak var accountTF: UITextField!
     @IBOutlet private weak var pwdTF: UITextField!
    

    并添加按钮点击事件

    // MARK: - 点击登录
     @IBAction func loginBtnDidClick() {
            
     }
    

    在点击事件中,判断两个 UITextField 的输入内容是否合法。不合法时显示 Toast,合法时发起网络请求,在成功和失败的回调中分别对 UI 做出处理。

    这种方式有什么不好呢?为什么我们的 C 层写着写着就变得异常臃肿?其实很大原因是因为 C 层的职责不够明确, C 层既负责了逻辑的处理,也负责了 UI 的变化。

    MVP

    接下来我们用 MVP 的思想优化这个 Demo。MVP 所做的事情很简单,就是将业务逻辑和视图逻辑抽象到 Protocol 中。

    • Model: 在 iOS 的 MVC 中 Model 通常都是指数据模型,目的是方便我们进行数据的操作。但在 MVP 中 Model 除了提供数据模型外,还负责处理具体的业务逻辑,比如我们这里的登录请求。需要注意的是,在 Model 里不应当持有 View。
    • View: 只负责响应 UI 变化,不负责处理业务逻辑。
    • Presenter: 负责完成 View 于 Model 间的交互。

    完工以后,我们的目录是这样的,接下来开始一步步编写思路。


    目录

    定义 Model,View,Presenter 的 Protocol

    Model - Protocol

    首先登录返回的用户信息类肯定是必不可少的

    struct User: Codable {
        
        /// 姓名
        let name: String
        /// 年龄
        let age: Int
    }
    

    其次还需要一个 业务方法 Login

    protocol LoginModelProtocol: class {
        
        /// 登录逻辑处理
        func login(account: String, pwd: String)
    }
    

    View - Protocol

    protocol LoginViewProtocol: class {
        
        /// 账号
        func account() -> String
        /// 密码
        func password() -> String
        /// 输入不合法
        func showToast(_ text: String)
        /// 请求正在进行中
        func showLoading()
        /// 网络请求返回, 登录成功
        func loginSuccess(_ response: User)
        /// 网络请求返回, 登录失败
        func loginFailure(_ error: String)
    }
    

    对于View的接口,去观察功能上的操作,然后考虑:

    • 该操作需要什么?(account, password)
    • 该操作的结果,对应的反馈?(showToast, loginSuccess, loginFailure)
    • 该操作过程中对应的友好的交互?(showLoading)

    Presenter - Protocol

    Presenter Protocol 作为连接 Model 和 View 的中间桥梁,需要将二者连接起来,因此他需要完成以下工作:

    • 响应登录按钮点击事件
    • 响应不合法事件,显示提示
    • 登录请求中
    • 网络请求成功回调
    • 网络请求失败回调

    因此,Presenter 就可以这么定义:

    protocol LoginPresenterProtocol: class {
        
        /// 登录
        func login()
        /// 显示提示
        func showToast(_ text: String)
        /// 登录请求中
        func loading()
        /// 网络请求返回, 登录成功
        func loginSuccess(_ response: User)
        /// 网络请求返回, 登录失败
        func loginFailure(_ error: String)
    }
    

    Model,View,Presenter 的具体实现

    Model

    还记得我们刚刚所说的,在 MVP 中,Model 的工作就是完成具体的业务和逻辑操作。比如说网络请求,持久化数据增删改查等。同时Model中又不会包含任何View。

    class LoginModel {
        
        weak var present: LoginPresenter?
        
        init(present: LoginPresenter?) {
            self.present = present
        }
    }
    
    // MARK: - LoginModelProtocol
    extension LoginModel: LoginModelProtocol {
        
        func login(account: String?, pwd: String?) {
            
            guard let account = account, let pwd = pwd else {
                
                present?.showToast("账号密码不合法") 
                return
            }
            if account.count == 0 || pwd.count == 0 {
                
                present?.showToast("账号密码不合法")
                return
            }
            
            present?.loading()
            Net.login(account: account, pwd: pwd, success: { [weak self] in
                self?.present?.loginSuccess($0)
            }) { [weak self] in
                self?.present?.loginFailure($0)
            }
        }
    }
    

    Presenter

    class LoginPresenter {
        
        var model: LoginModelProtocol?
        weak var view: LoginViewProtocol?
        
        init(view: LoginViewProtocol?) {
            
            self.view = view
            model = LoginModel(present: self)
        }
    }
    
    // MARK: - LoginPresenterProtocol
    extension LoginPresenter: LoginPresenterProtocol {
        
        func login() {
            model?.login(account: view?.account(), pwd: view?.password())
        }
        
        func loading() {
            view?.showLoading()
        }
        
        func loginSuccess(_ response: User) {
            view?.loginSuccess(response)
        }
        
        func loginFailure(_ error: String) {
            view?.loginFailure(error)
        }
        
        func showToast(_ text: String) {
            view?.showToast(text)
        }
    }
    

    可以看到,我们在 LoginPresenter 的构造方法中,同时实例化了Model 和 View,这样 Presenter 中就同时包含了两者。在 Presenter 的具体实现中,业务相关的操作由 Model 去完成(例如 Login),视图相关的操作由 View 去完成(例如获取用户输入内容等)。Presenter 只作为一个桥梁,巧妙的将 View 和 Model 的具体实现连接了起来。

    View

    最后再看一下 View 的具体实现,也就是 Controller 的实现:

    class LoginViewController: UIViewController {
    
        private var present: LoginPresenter?
        
        // MARK: - IBOutlet
        @IBOutlet private weak var accountTF: UITextField!
        @IBOutlet private weak var pwdTF: UITextField!
        
        // MARK: - LifeCycle
        override func viewDidLoad() {
            super.viewDidLoad()
            present = LoginPresenter(view: self)
        }
        
        deinit {
            
            present?.detachView()
            print("销毁----------")
        }
        
        // MARK: - 点击登录
        @IBAction func loginBtnDidClick() {
            present?.login()
        }
    }
    
    // MARK: - LoginViewProtocol
    extension LoginViewController: LoginViewProtocol {
        
        func account() -> String {
            return accountTF.text ?? ""
        }
        
        func password() -> String {
            return pwdTF.text ?? ""
        }
        
        func showLoading() {
            Toast.loading()
        }
        
        func showToast(_ text: String) {
            Toast.show(info: text)
        }
        
        func loginSuccess(_ response: User) {
            
            Toast.show(info: ""
            \(response.name) \n
            登录成功
            "")
            dismiss(animated: true, completion: nil)
        }
        
        func loginFailure(_ error: String) {
            Toast.show(info: error)
        }
    }
    

    至此,我们就通过 MVP 实现了之前所设想的业务逻辑和 UI 变换分离的 C 层。

    • Button 的 点击负责发起登录任务,但又不负责具体实现,而是由Presenter 转接给 Model 去实现
    • Controller 什么时候显示 Toast,什么时候跳转界面直接由 Presenter 告诉他,他只做一个 View 该做的事情
    • Controller 里没有任何逻辑处理,所有的逻辑处理都在 Model 中完成了

    最后

    看到这里其实你会发现,虽然我们的业务逻辑变得清晰了。但不可避免的我们增加了更多的类和代码,这点其实非常类似于 MVVM,相比原来简单的实现,我们需要写更多的胶水代码。

    但是随着项目规模的增大,代码逻辑清晰所带来的影响是非常深远的。维护低耦合,高内聚,优雅,健壮的代码不管对自己或是别人来说都是一种享受。

    相关文章

      网友评论

        本文标题:在 iOS 上实现基于协议的 MVP

        本文链接:https://www.haomeiwen.com/subject/dizidqtx.html