目前写 iOS 程序差不多整整两年了,期间断断续续写过 3 个 iOS 项目。之前写 android 的时候使用了 MVP 架构,但是写 iOS 的时候一直没有找到好的 MVP 实践,所以之前项目的所有代码都是堆在 ViewController 中。虽然使用了较多的注释和#MARK 使得代码不那么凌乱,但是动辄几百甚至上千行的 ViewController 还是让人很抓狂。
直到昨天我开始写下一个 iOS 项目的时候,开始之前又谷歌了一次“iOS MVP”,这次终于找到几篇文章,让我找到了一个简单易懂的 MVP 实践。:
以登陆页面为例。要实现一个最简单的登陆页面如下:
第一步,View
在之前的项目中,页面的 UI 代码都是写在 ViewController 中, 在本次实践中将 UI 代码抽离出来, 写在 LoginView.swift 中(只写了输入框和按钮):
import UIKitprotocol LoginViewDelegate: NSObjectProtocol { func loginWith(username: String, password: String)}class LoginView: UIView { var delegate: LoginViewDelegate? let usernameTf = UITextField() let passwordTf = UITextField() let loginBtn = UIButton() override init(frame: CGRect) { super.init(frame: frame) initView() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } convenience init() { self.init(frame: CGRect.zero) initView() } func initView() { self.backgroundColor = UIColor.white // 用户名输入框 self.addSubview(usernameTf) usernameTf.borderStyle = .roundedRect usernameTf.snp.makeConstraints { (make) in make.centerX.equalToSuperview() make.top.equalToSuperview().offset(270) make.left.equalToSuperview().offset(40) make.right.equalToSuperview().offset(-40) } // 密码输入框 self.addSubview(passwordTf) passwordTf.borderStyle = .roundedRect passwordTf.snp.makeConstraints { (make) in make.centerX.equalToSuperview() make.top.equalTo(usernameTf.snp.bottom).offset(16) make.left.equalToSuperview().offset(40) make.right.equalToSuperview().offset(-40) } //登录按钮 Views.resetButton(loginBtn, title: "登录", color: UIColor.green, font: UIFont.systemFont(ofSize: 22)) self.addSubview(loginBtn) loginBtn.addTarget(self, action: #selector(clickLoginButton), for: .touchUpInside) loginBtn.snp.makeConstraints { (make) in make.centerX.equalToSuperview() make.top.equalTo(passwordTf.snp.bottom).offset(20) } } @objc func clickLoginButton() { delegate?.loginWith(username: usernameTf.text ?? "", password: passwordTf.text ?? "") }}复制代码
如此只要在 ViewController 中实现 LoginViewDelegate 协议就可以捕获到登录按钮点击事件。
第二步,Presenter
presenter 有以下特点:
- 处理用户交互的逻辑
- 与 Model 层进行通信,将数据转换为 UI 友好的格式,并更新视图
- 不依赖 UIKit
在这个例子中,presenter 只有一个执行登录请求的方法:
import Foundationstruct LoginSelfData { let username: String let password: String}protocol LoginSelf: NSObjectProtocol { func startLoading() func finishLoading() func loginSucceed() func loginFail(mes: String) func noNetwork()}class LoginPresenter { var loginSelf: LoginSelf? /// 登录请求,点击登录按钮时调用 /// /// - Parameters: /// - username: 用户名 /// - password: 密码 func loginWith(username: String, password: String) { let parameter = ["username": username, "password": password] ... // 执行登录请求 loginSelf?.startLoading() ... // 接收到返回数据 loginSelf?.finishLoading() ... // 登录成功 loginSelf?.loginSucceed() ... // 登录失败 loginSelf?.loginFail(mes: "msg") }}复制代码
如此只要在 ViewController 中实现 LoginSelf 协议,就可以捕获到登录请求完成后的回调方法,这时就可以根据不同的回调方法更新视图。
第三步,ViewController
ViewController 中的代码很简单, 只要分别初始化 View 和 Presenter 并实现各自的协议就好。
import UIKitclass LoginViewController: UIViewController { let loginPresenter = LoginPresenter() var loginView: LoginView? override func viewDidLoad() { super.viewDidLoad() loginPresenter.loginSelf = self loginView = LoginView.init(frame: self.view.bounds) loginView?.delegate = self self.view = loginView }}// MARK: - 更新视图extension LoginViewController: LoginSelf { func startLoading() { } func finishLoading() { } func loginSucceed() { } func loginFail(mes: String) { } func noNetwork() { }}// MARK: - 响应用户交互操作extension LoginViewController: LoginViewDelegate { /// 登录 /// /// - Parameters: /// - username: 用户名 /// - password: 密码 func loginWith(username: String, password: String) { loginPresenter.loginWith(username: username, password: password) }}复制代码
小结
如此 一个相对简单的 iOS MVP 实践就完成了,采取这样的写法之后顿时感觉代码一下子很清晰,可能总的代码量会多一点,但是单个文件肯定会比之前的胖 ViewController 少,不仅方便同事之间交流,也方便自己查找代码、 修改 bug。