这篇文章先上我要说的项目的整体框架图吧,有兴趣的可以看下去。
那么正片现在就要开始了。。。。
我们一个一个介绍,先看看什么是MVVM模式。
当然在说这个之前,我们不得不先说说什么是MVC模式,当然这个模式是随着编码的积累慢慢形成的。
初入IOS的小白,可能什么代码都会放在C(Controller)中,例如:一个视图中UILabel、UIButton、UITableView等视图对象,数据对象,各种逻辑判断的flag,各种if,else满天飞,这样就会发现C(Controller)中的代码越来越多,之后别人阅读和维护这样的项目,心里这种十万个“垃圾”,“写的像一坨屎”翻腾。
于是我们慢慢的形成一种模式,MVC。
MVC模式
说明:从图中可以看出,C(Controller)是连接M(Model)和V(View)的重要中转站,
当然C也是作为项目中地位最为重要的一部分,负责的事务:
1.添加,显示,更新视图对象
2.处理view对象中的响应事件
3.网络请求,给M(Model)传入字典数据得到自定义的数据对象
4.将得到的数据对象(Model)传入view的对象中更新视图
5.视图之间的交互,跳转
为了能让你更具体了解什么是MVC,我们就拿简书界面来说吧。
主要写一下思路,至于代码的部分,只是说明这个流程
class YourController: UIViewController {
//View 对象(也可以是自定义视图对象)
private lazy var tableView : UITableView = {
() -> UITableView in
let _tableView = tableViewConfig(.zero, self, self, .plain)
_tableView.rowHeight = RS_SCALE_LENGTH(value: 64.0)
_tableView.backgroundColor = .clear
registerCell(_tableView, RSScenesCell.self)
return _tableView
}()
//网络请求返回字典数据
func dataRequest() {
Alamofire.request(URL, method: .post, parameters: mutParam, encoding: URLEncoding.default).responseJSON { response in
if response.result.error == nil {
// 请求成功
let text: NSString = NSString(data: response.data!, encoding: String.Encoding.utf8.rawValue)! as NSString
print("Data\(text as AnyObject)")
//向Model中传入字典得到自定义数据对象
//刷新tableView
} else {
print("error\((response.result.error)! as AnyObject)")
}
}
}
}
extension YourController : UITableViewDelegate, UITableViewDataSource {
//delegate or dataSource
}
//Model 部分
class SubHomeScrollDataModel: NSObject {
var isViewShow: Bool? //数据对象属性
class func simpleModel(dic: [String: Any]) -> SubHomeScrollDataModel {
let model = SubHomeScrollDataModel()
model.isViewShow = dic[""]
return model
}
}
这样一个很简单的MVC设计模式就出现了,当然这样的好处:
1.视图对象的封装
2.控制器代码相应的减少
当然也有它的缺点:
1.网络层的代码并没有抽离出来
2.逻辑业务层的处理
当然如果你是个老司机,看的资料很多的话,认为MVC是另一种设计模式,把这里的C看作Cell,可以推荐你看看这篇文章。当然那个的理解是基于我这个理解之上的,个人对他写的MVC的理解是,一个视图可能会分多个模块,而每个模块视图有自己的MVC模式。
简书地址
基于上面的设计缺点,我个人的理解,就是抽离出来网络层放到ViewModel中,这样就形成了我这个项目的MVVM的模式。
Alamofire
这个算是Swift项目中网络层必须用到的框架了,具体的源码我这儿就不介绍了(毕竟我也不是很清楚啊)
不过它的用法还是很简单的。
static func doPostRequest(_ param: [String: Any], URL: String, completeBlock: @escaping (_ response: DataResponse<Any>) -> Void) {
let header = ["TOKEN": RSUserManager.shared().accessToken]
Alamofire.request(URL, method: .post, parameters: param, encoding: URLEncoding.default, headers: header).responseJSON { response in
if response.result.error == nil {
/**
* 请求成功
*/
let text: NSString = NSString(data: response.data!, encoding: String.Encoding.utf8.rawValue)! as NSString
print("Data\(text as AnyObject)")
} else {
/**
请求出错了
- print: 错误信息
*/
print("error\((response.result.error)! as AnyObject)")
}
completeBlock(response)
}
}
//注: 这里是底层的与Alamofire接口相连接的BaseRequestManager
既然与Alamofire连接好了,接着我们再封装一层网络的管理者,例如首页获取数据的网络管理者,这个我们需要关心的是:
1.传入的字典
2.传出的自定义数据Model
那这个怎么玩呢? 我这里是请来了ReactiveSwift,ObjectMapper这两位大哥镇场子。
class StoreRequestManager: NSObject {
class func storeDataRequest(page: Int, segType: RSStoreSegmentType) -> SignalProducer<RSStoreDataModel, NoError> {
return SignalProducer<RSStoreDataModel, NoError>.init { (observer, _) in
var params = [String : String]()
params["page"] = "1"
params["type"] = "\(segType.hashValue)"
BaseRequestManager.doPostRequest(params,
URL:RSRequestUrl.STORE_MAIN_URL) { (response) -> Void in
if response.result.error == nil {
let responseModel = Mapper<RSStoreDataModel>().map(JSONObject: response.result.value)
observer.send(value: responseModel!)
}else {
//网络出错
let responseModel = RSStoreDataModel()
responseModel.netErrorResultModel()
observer.send(value: responseModel)
}
}
}
}
}
//注:RSStoreDataModel传出的数据模型,这个对象是实现了Mappable协议的,
//具体的可以看下面对ObjectMapper的介绍,通过ReactiveSwift中的观察者机制传出。
ObjectMapper
说得通俗一点,它的功能就是做数据对象的映射,把服务器的字典数据转成自定义对象。
。。。。。什么?陈独秀同学你说用字典的键值对获取的方法也可以生成自定义对象,那么老司机我就用代码来说说吧。
字典的格式
/* {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5:[
{
key1: value1,
key2: value2,
key3: value3,
key4: value4,
},
{
key1: value1,
key2: value2,
key3: value3,
key4: value4,
},
]
msg: "成功"
code:1
}*/
class ProductNormModel: BaseResultModel { //商品规格数据模型
var colorModels = [ProductColorModel]() //重点
var goodId: NSNumber = 0
var normId: NSNumber = 0
var normName: String = ""
func mapping(map: Map) {
super.mapping(map: map)
colorModels <- map["colors"] //数组数据的直接映射
goodId <- map["goodId"]
normId <- map["id"]
normName <- map["name"]
}
}
class ProductColorModel: Mappable { //商品颜色数据模型
var count: NSNumber = 0
var colorId : NSNumber = 0
var name: String = ""
var normId: NSNumber = 0
var price: NSNumber = 0
init(){}
required init?(map: Map){}
func mapping(map: Map) {
count <- map["count"]
colorId <- map["id"]
name <- map["name"]
normId <- map["normId"]
price <- map["price"]
}
}
//当然如果用常规的键值对获取的方法,也可以办到,但那样就感觉很繁琐了。
//数组的映射直接是火箭式封装
用这个框架的时候,我这边经过了一层数据结构的封装,怎么说呢,就是上面的BaseResultModel,这个是做什么用的呢,容我慢慢道来。
原因:每个API接口都会有msg,code/status 这些固定字段,但如果我们去解析每个接口时都去定义对象中相应的属性(对应msg,code),这样就感觉就是做重复的工作,所以我把这个工作放在底层的BaseResultModel这个对象中。
import ObjectMapper
class BaseResultModel: Mappable {
var type: RequestResultType?
var msg : String = ""
var obj = Dictionary<String, Any>()
init(){}
required init?(map: Map){}
func mapping(map: Map) {
var stauts : NSNumber?
stauts <- map["res"]
if stauts == 1 {
type = .RequestSuccessful
}else if stauts == 301 {
type = .RequestTokenIsInvalid
}else {
type = .RequestFail
}
msg <- map["msg"]
obj <- map["obj"]
}
func netErrorResultModel() {
type = RequestResultType.RequestNetError
msg = "网络请求超时"
}
}
// 这样我只需要继承这个对象即可,是不是很机智😎
ReactiveSwift
好了,接下来上场的是ReactiveSwift这位大哥了,那么先说说这位大哥的背景吧。
ReactiveSwift是一种函数式反应型编程,那什么是函数式反应型编程呢?有什么优点呢?
我这儿就不给理论性的解读了,那个资料臣妾也是看得一头雾水,那么还是看代码吧。
btn.reactive.controlEvents(.touchUpInside).observeValues { (selected) in
}
利用函数式编程(Functional Programming)技术去处理这些变化事件,
如这里面的函数controlEvents(.touchUpInside),然后通过给个小兵观察这个按钮是否发生改变,
从而给出变化后的按钮对象。
优点嘛
swift不是以简洁著称,这样就符合咋们高大帅的气质嘛。
介绍完了大哥,就来具体的说说ReactiveSwift用法,用法嘛,其实很简单,抓住几个重点就可以了。
就我个人的理解,ReactiveSwift其实处理ios中的逻辑,代理,通知方面的一个整合,另外就是一个回调观察者的机制。
例如我们ios中对象的数据或是事件的回调,通常采用的方式是代理或是Block,如果是非关联对象之间数据事件的联系则是用通知,然后系统控件(UIButton,UITextField)中的响应事件或是代理方法,这些零零散散的东西,现在直接用这个框架就可以很快捷的解决问题了。
还是看代码吧!
//按钮的点击事件
btn.reactive.controlEvents(.touchUpInside).observeValues { (selected) in
}
补充:至于是继承于UIControl的控件都可以运用此方法进行不同控件的响应。
public struct UIControlEvents : OptionSet {
public init(rawValue: UInt)
public static var touchDown: UIControlEvents { get } // on all touch downs
public static var touchDownRepeat: UIControlEvents { get } // on multiple touchdowns (tap count > 1)
public static var touchDragInside: UIControlEvents { get }
public static var touchDragOutside: UIControlEvents { get }
public static var touchDragEnter: UIControlEvents { get }
public static var touchDragExit: UIControlEvents { get }
public static var touchUpInside: UIControlEvents { get }
public static var touchUpOutside: UIControlEvents { get }
public static var touchCancel: UIControlEvents { get }
public static var valueChanged: UIControlEvents { get } // sliders, etc.
@available(iOS 9.0, *)
public static var primaryActionTriggered: UIControlEvents { get } // semantic action: for buttons, etc.
public static var editingDidBegin: UIControlEvents { get } // UITextField
public static var editingChanged: UIControlEvents { get }
public static var editingDidEnd: UIControlEvents { get }
public static var editingDidEndOnExit: UIControlEvents { get } // 'return key' ending editing
public static var allTouchEvents: UIControlEvents { get } // for touch events
public static var allEditingEvents: UIControlEvents { get } // for UITextField
public static var applicationReserved: UIControlEvents { get } // range available for application use
public static var systemReserved: UIControlEvents { get } // range reserved for internal framework use
public static var allEvents: UIControlEvents { get }
}
当然这是ios系统中UIControlEvents的枚举类,例如UISwitch,UITextField,UISlider,然后他们不同响应的方式,都可以用上面的代码去做处理。
热信号(就是代替Block和Delegate的)
A和B对象(B对象是在A对象中生成的),要把B对象中的数据x传回A中,肿么办呢?
//B文件中
//记得引入框架
import ReactiveCocoa
import ReactiveSwift
import Result
let (signalAction, observerTap) = Signal<Any, NoError>.pipe()
func viewBtnDidTouch(touchBtn: UIButton) {
touchBtn.reactive.controlEvents(.touchUpInside).observeValues { (selected) in
self?.observerTap.send(value:"x")
}
}
//A文件中
rightNaviView.signalAction.observeValues({ [weak self] (value) in
//获得value = “x”
})
这里面的[weak self]是必须要加上的啊,不然就会发生内存泄漏,类似oc的__weak typeof(self) weakSelf = self,
那样的话,你会发现你退出了一个控制器后,该控制器并没有销毁。想当初我是一行一行代码去检查为什么pop控制器,它就是不执行下面那个方法呢,原来是这儿的坑啊。
deinit {
print("\(type(of: self)) deinit")
}
通知
NotificationCenter.default.reactive.notifications(forName: Notification.Name(rawValue: "UIKeyboardWillShowNotification"),
object: nil).observeValues { (notification) in
print("键盘弹起")
}
当然这是系统中的通知,自定义通知我目前还不知道呢。
说完热信号,咋们说说冷信号。二狗你可能会问,什么是热信号什么又是冷信号呢,那我就解释一波吧。
热信号嘛,就是界面上与用户交互操作的事件的响应,例如UIButton,UItextfield,键盘的弹出通知了
,UISwitch的切换了,是不,这些反应在界面上的就可以称之为“热信号”了;
而冷信号,就是程序中通过代码去观察事件动作的,如你定义一个获取网络的API方法,然后通过Action去返回一个SignalProducer的东东,之后再用action的start的方法监听返回的数据,这样的没有与用户交互的数据相应事件称之为“冷信号”,是不是很有道理呢,那么就让我们看看具体什么是冷信号吧。
小二,上代码!!!
class StoreViewModel: NSObject {
var action : Action<(), RSStoreDataModel, NoError>! //获取界面数据的事件
func getStoreDataRequest(page:Int, type: RSStoreSegmentType, completion completed: ((_ model: RSStoreDataModel) -> Swift.Void)? = nil) {
action = Action<(), RSStoreDataModel, NoError> { (_) -> SignalProducer<RSStoreDataModel, NoError> in
StoreRequestManager.storeDataRequest(page: page, segType: type)
}
action.apply(()).start { (event) in
if completed != nil {
completed!(event.value!)
}
}
}
}
class StoreRequestManager: NSObject {
class func storeDataRequest(page: Int, segType: RSStoreSegmentType) -> SignalProducer<RSStoreDataModel, NoError> {
return SignalProducer<RSStoreDataModel, NoError>.init { (observer, _) in
var params = [String : Any]()
params["pageNo"] = page
params["pageSize"] = 16
params["isPresale"] = "\(segType.hashValue)"
BaseRequestManager.doGetRequest(params,
URL:RSRequestUrl.STORE_MAIN_URL) { (response) -> Void in
if response.result.error == nil {
let responseModel = Mapper<RSStoreDataModel>().map(JSONObject: response.result.value)
observer.send(value: responseModel!)
}else {
//网络出错
let responseModel = RSStoreDataModel()
responseModel.netErrorResultModel()
observer.send(value: responseModel)
}
}
}
}
}
冷信号一般都是返回SignalProducer的对象的,具体的就看你想怎么操作了。
当然这个冷信号的核心就是简洁了,比如说在你需要的地方要请求API,那么只需要:
func dataRequest() {
MBProgressHUD.showAdded(to: self.view, animated: true)
RSStoreViewModel().getProductDetailRequest(productId: productId) { (resultModel) in
MBProgressHUD.hide(for: self.view, animated: true)
if resultModel.type == .RequestSuccessful {
}else {
RSHelper.showViewDidResAbnormal(resultModel: resultModel)
}
}
}
是不是很帅呢!
最后如果你还是欲求不满的话,给个车牌号你吧。
再说说我们经常会用到的本地存储功能,这个一般的APP都会需要的。
数据的本地保存是不,下次没有网络的时候可以显示之前保留下来的数据,提升用户的体验;再或是把程序关掉后,再次打开APP,你之前的用户信息因为保留下来了,那么你的状态就不是未登录的状态了。
先说说我们常规的数据存储,最基础的是用SQLite,CoreData,可能会用SQL语句,苹果亲儿子的数据保存的coredata形式,当然二狗会说你太low了,还用这些不上台面的东东,看我操作一波,于是拿出了FMDB,对SQLite的进一步封装,直接指明需要存储的数据和文件路径即可,剩下的由底层的小弟去完成就可以了,嗯,确实很优秀。
但我们存储数据的时候通常以对象的形式去操作,这样你存储一个对象信息的时候,你得遵循SQL的语句规则,例如
//插入数据
NSString *name = [NSString stringWithFormat:@"王子涵%@",@(mark_student)];
int age = mark_student;
NSString *sex = @"男";
mark_student ++;
//1.executeUpdate:不确定的参数用?来占位(后面参数必须是oc对象,;代表语句结束)
BOOL result = [_db executeUpdate:@"INSERT INTO t_student (name, age, sex) VALUES (?,?,?)",name,@(age),sex];
//2.executeUpdateWithForamat:不确定的参数用%@,%d等来占位 (参数为原始数据类型,执行语句不区分大小写)
// BOOL result = [_db executeUpdateWithFormat:@"insert into t_student (name,age, sex) values (%@,%i,%@)",name,age,sex];
//3.参数是数组的使用方式
// BOOL result = [_db executeUpdate:@"INSERT INTO t_student(name,age,sex) VALUES (?,?,?);" withArgumentsInArray:@[name,@(age),sex]];
if (result) {
NSLog(@"插入成功");
} else {
NSLog(@"插入失败");
}
给人一种拘束的感觉。
那么让我给大家隆重的介绍即将登场的。
Realm
先给出他的官方地址吧
别的不多说,二狗你只需要好好看代码就可以了,能用到的不多,注意几点就可以。
1.配置Realm数据库,当然这个是必须的,不然Realm怎么知道数据放在哪里呢,当我们数据对象变化升级的时候,怎么做数据升级呢?
class func configRealm() {
/// 如果要存储的数据模型属性发生变化,需要配置当前版本号比之前大
let dbVersion : UInt64 = 2
let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] as String
let dbPath = docPath.appending("/defaultDB.realm")
let config = Realm.Configuration(fileURL: URL.init(string: dbPath), inMemoryIdentifier: nil, syncConfiguration: nil, encryptionKey: nil, readOnly: false, schemaVersion: dbVersion, migrationBlock: { (migration, oldSchemaVersion) in
}, deleteRealmIfMigrationNeeded: false, shouldCompactOnLaunch: nil, objectTypes: nil)
Realm.Configuration.defaultConfiguration = config
Realm.asyncOpen { (realm, error) in
if let _ = realm {
print("Realm 服务器配置成功!")
}else if let error = error {
print("Realm 数据库配置失败:\(error.localizedDescription)")
}
}
}
至于数据库的升级,目前我也没弄明白(主要还没遇到需要升级的情况)
2.Realm对象与NSObject对象的相互映射。这个就是数据存与取之间数据的对接了,当然不同情况下我们需要做不同的处理。
比如:在APP中用户的基本信息(userName,userId,userToken,vip,userImgUrl等),这些在APP全局中都可能会用到的数据,最好在每次启动的时候从数据库拿,并且这个时候我们用这些数据去初始化一个单例对象,这样之后在APP中访问数据的时候,所采用的姿势也是比较优雅的;再就是之前我说的缓存数据的获取,这个肯定是哪个界面需要(通过判断网络的状态),然后根据自己的需求去数据库中取(当然得有数据才行哦),具体的可以看看我的这篇文章。
数据的存储
下面就用代码具体说说这个框架的用法吧,我这边采用的数据结构是 :
需要存储的对象(A),Realm数据库的对象(B),单例对象(C)
C(UserManager) -> A(userModel) -> B(realmModel)
class UserManager {
var userModel = UserModel()
var bluetoothDicArr = Array<RSBluetoothModel>()
class func shared() -> RSUserManager {
return sharedManager
}
func sign(dic: []<String, Any>) {
userModel = UserModel().userInfo(dic) //更新单例对象中属性数据
UserRealmModel.addRealmModel(userModel) //数据写入
}
}
class UserModel {
var userId = 0
var nickName = ""
var userImgUrl = ""
var phoneNum = ""
var accessToken = ""
func userInfo(_ userDic: Dictionary<String, Any>) {
userId = userDic["userId"] as! Int
nickName = userDic["nickname"] as! String
userImgUrl = (userDic["headimgurl"] as! String)
phoneNum = userDic["phone"] as! String
accessToken = userDic["accessToken"] as! String
}
import RealmSwift
import Realm
class UserRealmModel: Object {
@objc dynamic var userId = 0
@objc dynamic var nickName = ""
@objc dynamic var userImgUrl = ""
@objc dynamic var sex = ""
@objc dynamic var accessToken = ""
class func addRealmModel(_ userModel: UserModel) {
let realm = try! Realm()
let realmModel: UserRealmModel = realmModelWithUserModel(userModel)
try! realm.write {
realm.deleteAll()
realm.add(realmModel)
}
}
class func realmModelWithUserModel(_ userModel: UserModel) -> UserRealmModel {
let model = UserRealmModel()
model.userId = Int(userModel.userId)
model.nickName = userModel.nickName
model.userImgUrl = userModel.userImgUrl
model.sex = userModel.sex
model.accessToken = userModel.accessToken
return model
}
}
当然这是比较简单的数据对接了,相当于一对一的,但是如果是一个对象中包含多个对象呢,就是一对多咯。
我这个中的是包含了多个蓝牙设备的信息
class UserManager {
var bluetoothDicArr = Array<BluetoothModel>()
func addBluetoothModel(uuid: String, nameStr name: String, deviceCode: String) {
bluetoothModel = BluetoothModel.bluetoothModel(uuid: uuid, deviceName: name, deviceCode: deviceCode)
UserRealmModel.addBluetoothInModel(bluetoothModel: bluetoothModel) //通过单例对象去添加蓝牙数组数据
}
}
class BluetoothRealmModel: Object {
@objc dynamic var deviceUUID = ""
@objc dynamic var deviceName = ""
@objc dynamic var deviceCode = "" //设备编号
class func bluetoothRealmModel(model: RSBluetoothModel) -> RSBluetoothRealmModel { // swiftModel -> realm
let realmModel = BluetoothRealmModel()
realmModel.deviceUUID = model.deviceUUID
realmModel.deviceName = model.deviceName
realmModel.deviceCode = model.deviceCode
return realmModel
}
}
class BluetoothModel: NSObject {
var deviceUUID: String = ""
var deviceName : String = ""
var deviceCode : String = ""
class func bluetoothModel(uuid: String, deviceName name: String, deviceCode code: String) -> BluetoothModel {
let model = BluetoothModel()
model.deviceUUID = uuid
model.deviceName = name
model.deviceCode = code
return model
}
class func bluetoothModel(realm: BluetoothRealmModel) -> BluetoothModel { // Realm -> swiftModel
let model = BluetoothModel()
model.deviceUUID = realm.deviceUUID
model.deviceName = realm.deviceName
model.deviceCode = realm.deviceCode
return model
}
}
class UserRealmModel: Object {
var bluetoothArr = List<BluetoothRealmModel>()
class func addBluetoothInModel(bluetoothModel: BluetoothModel) {
let realm = try! Realm()
let model = realm.objects(UserRealmModel.self).first
for obj in (model?.bluetoothArr)! { //防止重复添加
if obj.deviceUUID == bluetoothModel.deviceUUID {
return
}
}
try! realm.write {
let bluetoothModel =SBluetoothRealmModel.bluetoothRealmModel(model: bluetoothModel)
model?.bluetoothArr.append(bluetoothModel)
if (model?.bluetoothArr.count)! > 10 {
model?.bluetoothArr.removeFirst() //保证只有最近的十条蓝牙设备信息
}
}
}
}
这样基本上满足了数据结构的需要了。现在看看是不是Realm很方便快捷呢,在数据存储的方面。
那么,我们说完了我上面所有的用到的框架,这里面还有用到Kingfisher,MBProgressHUD,当然这些的用法很简单就不在这儿赘述了。
这儿我再附上Swift的目录结构及其功能吧
这篇文章就差不多接近尾声了,如果老铁看到了这儿,我要表达的意思你都懂的话,
那么我总结一下这篇文章的知识点吧。
1.尽可能的采用低耦合,高内聚的编码思想.
2.作为对象的调用,只需要关注输入和输出的数据,底层的实现只需要在相关文件中实现即可.
3.ReactiveSwift的热信号和冷信号的运用.
4.ObjectMapper的在不同数据结构中的用法(即数组和字典).
5.Realm的简单用法,对象之间的相互映射,数据在Realm中的增删改查.
6.项目模块之间的整体性.
最后给上这个项目会用到的基础框架吧
GitHub地址