参考链接:
以下内容在上述文章基础上进行, 请事先查阅.
其实现在iOS开发还是在Objective-C的大环境下, 让我在项目中使用MVVM这种架构, 其实我是拒绝的, 因为个人觉得OC的代码设计是不适合使用MVVM框架的, 具体原因就是 “丑”, 一个字代表了所有的观点. 但作为技术研讨, MVVM的思想还是有向大家普及的必要的.
MVVM
MVVM是指 Model, View, ViewModel, 这里的ViewModel代替了原先Controller的部分工作, MVVM的概念最初是从前端开始的, 以后面会说到的Vue举例, 每一个Vue文件就是一个ViewModel, 实现了动态绑定的功能, 通俗的说就是当数据发生改变, 视图立即改变, 相反当用户操作视图也同时操作了数据. ViewModel从名字上来看就是View和Model的中间层.
接下来我们来看一下移动端怎么实现MVVM架构:
.
├── Controller.swift
├── Model.swift
├── View.swift
└── ViewModel.swift
我们先在项目结构下添加ViewModel.swift, 将之前在Controller中的网络请求代码移植到VM中:
ViewModel.swift
class ViewModel {
lazy var models: [Model] = [Model]()
}
extension ViewModel {
func dynamicBinding(finishedCallback : @escaping () -> ()) {
Http.requestData(.get, URLString: "http://localhost:3001/api/J1/getJ1List") { (response) in
guard let result = response as? [String : Any] else { return }
guard let data:[String : Any] = result["data"] as? [String : Any] else { return }
guard let models:[[String : Any]] = data["models"] as? [[String : Any]] else { return }
self.models.removeAll()
for dict in models {
self.models.append(Model(dict: dict))
}
finishedCallback()
}
}
}
小贴士:Swift中的Class可以不用继承直接定义, 降低开销.
我们对应修改其他代码如下:
View.swift
class View: UIView {
var viewModel: ViewModel? { //update
didSet {
tableView.reloadData()
}
}
fileprivate lazy var tableView: UITableView = { [weak self] in
var tableView = UITableView(frame: self!.bounds, style: .plain)
tableView.dataSource = self
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(tableView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension View: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.models.count ?? 0 //update
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "identifier"
let model: Model? = viewModel?.models[indexPath.row] //update
let cell = tableView.dequeueReusableCell(withIdentifier: identifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: identifier)
cell.textLabel?.text = model?.text
cell.detailTextLabel?.text = model?.detailText
return cell
}
}
Controller.swift
class Controller: UIViewController {
fileprivate lazy var viewModel: ViewModel = ViewModel() //update
fileprivate lazy var baseView: View = { [weak self] in
return View(frame: self!.view.bounds)
}()
override func loadView() {
super.loadView()
title = "J1"
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
adapterView()
}
}
extension Controller {
fileprivate func setupView() {
view.addSubview(baseView)
}
fileprivate func adapterView() { //update
viewModel.dynamicBinding {
self.baseView.viewModel = self.viewModel
}
}
}
我们通过将网络请求封装到ViewModel中实现了代码分层, 设计也更为简洁. 但是我们现在的用户界面好Low啊, 是不是应该做点什么? 对了, 我们给界面里添加点图片吧!
当然图片我们也是通过后端获取, 我们在后端目录中添加image.js文件来实现图片服务器.
.
└── public
└── images
└── image.js
image.js
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
console.log(req.url);
let path = '..' + decodeURI(req.url);
fs.readFile(path, 'binary', (err, file) => {
if (err) {
// console.log(err);
return;
} else {
res.writeHead(200, {
'Content-Type': 'image/png'
});
res.write(file, 'binary');
res.end();
return;
}
})
}).listen(3002);
console.log('port = 3002');
这里使用的是原生的Node.js搭建的图片服务器, 监听3002端口.
这里我们来说说服务器是如何实现想客户端传送数据的, 我们现在分为API服务器: 3001和图片服务器: 3002. API服务器传输的类型是Content-Type : application/json这种格式的, 而图片服务器返回的是二进制的格式Content-Type’: ‘image/png. 原理就是将文本或数据写在Body上.
我们cd 到目录中 通过node命令 $ node image.js 来执行js脚本文件. 看到port = 3002打印在终端上说明, 服务启动成功.
有图片服务器, 没有图片怎么行? 我们在images中添加图片文件. 添加完后在浏览器中输入: http://localhost:3002/images/J1/关于健一@2x.png, 就能够访问到服务器的图片了.
接下来我们将图片的URL地址通过API的形式返回给移动端调用.
.
└── app
└── controllers
└── J1.js
J1.js
exports.getJ1List = async(ctx, next) => {
ctx.body = {
models: [{
text: '我的账户',
detailText: "欢迎进入=>我的账户",
imageUrl: "http://localhost:3002/images/J1/我的账户@2x.png"
}, {
text: '我的优惠券',
detailText: "欢迎进入=>我的优惠券",
imageUrl: "http://localhost:3002/images/J1/我的优惠券@2x.png"
}, {
text: '收货地址',
detailText: "欢迎进入=>收货地址",
imageUrl: "http://localhost:3002/images/J1/收货地址@2x.png"
}, {
text: '在线客服',
detailText: "欢迎进入=>在线客服",
imageUrl: "http://localhost:3002/images/J1/在线客服@2x.png"
}, {
text: '用药提醒',
detailText: "欢迎进入=>用药提醒",
imageUrl: "http://localhost:3002/images/J1/用药提醒@2x.png"
}, {
text: '药查查',
detailText: "欢迎进入=>药查查",
imageUrl: "http://localhost:3002/images/J1/药查查@2x.png"
}, {
text: '疾病百科',
detailText: "欢迎进入=>疾病百科",
imageUrl: "http://localhost:3002/images/J1/疾病百科@2x.png"
}, {
text: '药品百科',
detailText: "欢迎进入=>药品百科",
imageUrl: "http://localhost:3002/images/J1/药品百科@2x.png"
}, {
text: '健一咨询',
detailText: "欢迎进入=>健一咨询",
imageUrl: "http://localhost:3002/images/J1/健一咨询@2x.png"
}, {
text: '帮助中心',
detailText: "欢迎进入=>帮助中心",
imageUrl: "http://localhost:3002/images/J1/帮助中心@2x.png"
}, {
text: '点赞/吐槽',
detailText: "欢迎进入=>点赞/吐槽",
imageUrl: "http://localhost:3002/images/J1/点赞:吐槽@2x.png"
}, {
text: '关于健一',
detailText: "欢迎进入=>关于健一",
imageUrl: "http://localhost:3002/images/J1/关于健一@2x.png"
}]
}
}
在浏览器访问接口 http://localhost:3001/api/J1/getJ1List
{
"code": 0,
"message": "success",
"data": {
"models": [
{
"text": "我的账户",
"detailText": "欢迎进入=>我的账户",
"imageUrl": "http://localhost:3002/images/J1/我的账户@2x.png"
},
{
"text": "我的优惠券",
"detailText": "欢迎进入=>我的优惠券",
"imageUrl": "http://localhost:3002/images/J1/我的优惠券@2x.png"
},
{
"text": "收货地址",
"detailText": "欢迎进入=>收货地址",
"imageUrl": "http://localhost:3002/images/J1/收货地址@2x.png"
},
{
"text": "在线客服",
"detailText": "欢迎进入=>在线客服",
"imageUrl": "http://localhost:3002/images/J1/在线客服@2x.png"
},
{
"text": "用药提醒",
"detailText": "欢迎进入=>用药提醒",
"imageUrl": "http://localhost:3002/images/J1/用药提醒@2x.png"
},
{
"text": "药查查",
"detailText": "欢迎进入=>药查查",
"imageUrl": "http://localhost:3002/images/J1/药查查@2x.png"
},
{
"text": "疾病百科",
"detailText": "欢迎进入=>疾病百科",
"imageUrl": "http://localhost:3002/images/J1/疾病百科@2x.png"
},
{
"text": "药品百科",
"detailText": "欢迎进入=>药品百科",
"imageUrl": "http://localhost:3002/images/J1/药品百科@2x.png"
},
{
"text": "健一咨询",
"detailText": "欢迎进入=>健一咨询",
"imageUrl": "http://localhost:3002/images/J1/健一咨询@2x.png"
},
{
"text": "帮助中心",
"detailText": "欢迎进入=>帮助中心",
"imageUrl": "http://localhost:3002/images/J1/帮助中心@2x.png"
},
{
"text": "点赞/吐槽",
"detailText": "欢迎进入=>点赞/吐槽",
"imageUrl": "http://localhost:3002/images/J1/点赞:吐槽@2x.png"
},
{
"text": "关于健一",
"detailText": "欢迎进入=>关于健一",
"imageUrl": "http://localhost:3002/images/J1/关于健一@2x.png"
}
]
}
}
数据返回没有问题了, 接下来我们来看看如何从移动端访问服务器图片, 这里使用喵神的Kingfisher来请求图片, 之前已经通过Pods导入到项目中了.
使用第三方框架的时候我们都需要在外面再封装一层, 我们创建Image.swift
Image.swift
import Kingfisher
extension UIImageView {
func loadUrl(imageUrl: String?, placeholder: String = "placeholder") {
self.kf.setImage(with: URL(string: imageUrl?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""), placeholder: UIImage(named: placeholder), options: nil, progressBlock: nil, completionHandler: nil)
}
}
接下来在Model和View中添加imageUrl字段并修改如下:
Model.swift
class Model: NSObject {
var text : String = ""
var detailText : String = ""
var imageUrl : String = "" //update
init(dict : [String : Any]) {
super.init()
setValuesForKeys(dict)
}
override func setValue(_ value: Any?, forUndefinedKey key: String) {}
}
View.swift
class View: UIView {
var viewModel: ViewModel? {
didSet {
tableView.reloadData()
}
}
fileprivate lazy var tableView: UITableView = { [weak self] in
var tableView = UITableView(frame: self!.bounds, style: .plain)
tableView.dataSource = self
tableView.delegate = self //update
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(tableView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension View: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.models.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "identifier"
let model: Model? = viewModel?.models[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: identifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: identifier)
cell.textLabel?.text = model?.text
cell.detailTextLabel?.text = model?.detailText
cell.imageView?.loadUrl(imageUrl: model?.imageUrl) //update
return cell
}
}
extension View: UITableViewDelegate { //update
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
以上就是个人理解的MVVM, 也说出你对于MVVM的想法, 我们一起探讨!
About: