目前写 iOS 程序差不多整整两年了,期间断断续续写过 3 个 iOS 项目。之前写 android 的时候使用了 MVP 架构,但是写 iOS 的时候一直没有找到好的 MVP 实践,所以之前项目的所有代码都是堆在 ViewController 中。虽然使用了较多的注释和#MARK 使得代码不那么凌乱,但是动辄几百甚至上千行的 ViewController 还是让人很抓狂。
直到昨天我开始写下一个 iOS 项目的时候,开始之前又谷歌了一次“iOS MVP”,这次终于找到几篇文章,让我找到了一个简单易懂的 MVP 实践。:
Don't Put View Code Into Your View Controller
以登陆页面为例。要实现一个最简单的登陆页面如下:
第一步,View
在之前的项目中,页面的 UI 代码都是写在 ViewController 中, 在本次实践中将 UI 代码抽离出来, 写在 LoginView.swift 中(只写了输入框和按钮):
import UIKit
protocol 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 Foundation
struct 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 UIKit
class 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。