在 iOS 上实现基于协议的 MVP

概述

如果你做过 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,相比原来简单的实现,我们需要写更多的胶水代码。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容