RxSwift+Moya之项目实战
RxSwift+Moya之项目实战
RxSwift相关基本介绍和用法可参考:
RxSwift的使用详解01
RxSwift的使用详解02
一. 下面将将进行实战项目
- 1.登录注册功能
- 输入用户名要大于6个字符,不然密码不能输入
- 密码必须大于6个字符,不然重复密码不能输入
- 重复密码输入必须和密码一样,不然注册按钮不能点击
- 根据输入的字符是否合法,按钮动态的改变颜色
- 2.UITableView和搜索SertchBar的应用
- searchBar根据输入的字体展示包含该字体的cell列表
- RxSwift实现tableView列表展示
- 3.Moya+RxSwift实现网络请求
- 应用RxSwift在UICollectionView中的应用
- 用Moya进行网络请求
- ObjectMapper进行json到model的数据解析
- 整个Demo的架构使用MVVM
二. Demo地址
下面简单看一下demo的界面
1. 登录注册
2. UITableView和SearchBar
3. UICollectionView和Moya
三. 项目结构和框架
1. 结构
demo是使用的纯MVVM模式,因为RxSwift就是为MVVM而生。不懂MVVM的猿友可参考MVVM模式快速入门
2. 项目框架
// Swift三方库// Rxpod 'RxSwift' //RxSwift的必备库pod 'RxCocoa' //对 UIKit Foundation 进行 Rx 化pod 'RxDataSources' // 帮助我们优雅的使用tableView的数据源方法// 网络请求pod 'Moya/RxSwift' // 为RxSwift专用提供,对Alamofire进行封装的一个网络请求库// 图片处理pod 'Kingfisher' //图片处理库// 数据解析pod 'ObjectMapper' //json转模型// OC库// MJRefreshpod 'MJRefresh' //MJ上拉下拉刷新pod 'SVProgressHUD' //HUD
四. 注册界面
- 这里主要使用了Observable的相关知识,不了解的童鞋可参考RxSwift的使用详解01,了解Observable的操作
- 注册和登录并没有保存已注册的账号和密码, 故登录功能并不完善,后期会在完善,望知晓
- 下面将针对注册用户名做简单介绍:
1. 首先在model里处理输入字符串的语法法则和字符个数是否符合规范
extension InputValidator {//判断字符串是否符合语法法则class func isValidEmail(_ email: String) -> Bool {let regular = try? NSRegularExpression(pattern: "^\\S+@\\S+\\.\\S+$", options: [])if let re = regular {let range = NSRange(location: 0, length: email.lengthOfBytes(using: .utf8))let result = re.matches(in: email, options: [], range: range)return result.count > 0}return false}//判断密码字符个数>8class func isValidPassword(_ password: String) -> Bool {return password.characters.count >= 8}//判断用户名class func validateUserName(_ username: String) -> Result {//判断字符个数是否正确if username.characters.count < 6 {return Result.failure(message: "输入的字符个数不能少于6个字符")}//账号可用return Result.success(message: "账号可用")}
}
其中Result是一个返回是否成功的枚举值,可传入字符串变量
enum Result {case success(message: String)case failure(message: String)
}
2. 根据输入的用户名判断该用户名是否可用
var usernameObserable: Observable<Result>var passwordObserable: Observable<Result>var repeatPassObserable: Observable<Result>var registerBtnObserable: Observable<Bool>init(){//检测账号usernameObserable = username.asObservable().map({ (username) -> Result inreturn InputValidator.validateUserName(username)})}
- 该返回参数Result,控制器将根据该Result是否成功来改变输入框是否是可编辑状态
- 初始化方法中,我们对传入的序列进行处理和转换成相对应的Result序列
3. controller逻辑,根据用户名输入改变各控件状态
//1. 账号判断逻辑//1-1. 检测账号usernameTextField.rx.text.orEmpty // 将String? 类型转为String型.bindTo(registerVM.username).addDisposableTo(bag)//1-2. 根据账号监听提示字体的状态registerVM.usernameObserable.bindTo(usernameHintLabel.rx.validationResult).addDisposableTo(bag)//1-3. 根据账号监听密码输入框的状态registerVM.usernameObserable.bindTo(passwordTextField.rx.enableResult).addDisposableTo(bag)
- 检测输入用户名是否符合规范
- 根据账号监听提示字体的状态
- 根据账号监听密码输入框的状态
- 根据账号监听注册按钮的状态
五. UITableView和SearchBar
- 该UITableView展示界面并未涉及网络请求
- 数据来源plist文件
- 图片为本地图片,可下载demo,在demo中查找图片
- 选用自定义UITableViewCell,故cell不做介绍
- model小编这里也不多做介绍,详情可下载demo看具体代码
1. viewModel中的代码逻辑
1-1. 读取plist文件,获取模型数组
fileprivate func getHeroData() -> [HeroModel]{// 1.获取路径let path = Bundle.main.path(forResource: "heros.plist", ofType: nil)!// 2.读取文件内容let dictArray = NSArray(contentsOfFile: path) as! [[String : Any]]// 3.遍历所有的字典并且转成模型对象return dictArray.map({ HeroModel(dict: $0) }).reversed()
}
1-2. seachBar
lazy var heroVariable: Variable<[HeroModel]> = {return Variable(self.getHeroData())}()var searchText: Observable<String>init(searchText: Observable<String>) {self.searchText = searchTextself.searchText.subscribe(onNext: { (str: String) inlet heros = self.getHeroData().filter({ (hero: HeroModel) -> Bool in//过滤if str.isEmpty { return true }//model是否包含搜索字符串return hero.name.contains(str)})self.heroVariable.value = heros}).addDisposableTo(bag)}
- 其中heroVariable是一个数组模型的包装箱,在controller内调用使用前需要asObservable或者asDriver解包装;详细用法可参考:RxSwift的使用详解01
- searchText搜索框输入的关键字,根据该关键字从数组中过滤出所有包含该关键字的model
- 对heroVariable重新赋值,发出事件
1-3. RxTableViewController.swift主要代码
1-3-1. searchBar搜索框,输入字符后间隔0.5秒开始搜索
var searchText: Observable<String> {//输入后间隔0.5秒搜索,在主线程运行return searchBar.rx.text.orEmpty.throttle(0.5, scheduler: MainScheduler.instance)
}
1-3-2. UITableView的设置
//2.给tableView绑定数据//注意: 三个参数:row, model, cell三个顺序不可以搞错, 不需要的可省略 heroVM.heroVariable.asDriver().drive(rxTableView.rx.items(cellIdentifier: kCellID, cellType: RxTableViewCell.self)) { (_, hero, cell) incell.heroModel = hero}.addDisposableTo(bag)// 3.监听UITableView的点击rxTableView.rx.modelSelected(HeroModel.self).subscribe { (event: Event<HeroModel>) inprint(event.element?.name ?? "")}.addDisposableTo(bag)
- 将viewModel中的heroVariable进行解包装,如果是Driver序列,我们这里不使用bingTo,而是使用的Driver,用法和bingTo一模一样。
- Deriver的监听一定发生在主线程,所以很适合我们更新UI的操作
- 如需设置delegate的代理
rxTableView.rx.setDelegate(self).addDisposableTo(bag)
然后在实现相应的代理方法即可,如:
extension RxTableViewController: UITableViewDelegate{func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {return 100}
}
六. UICollectionView+Moya+ObjectMapper网络请求和数据处理
- 与上述UITableView不同的是,这部分将以RxDataSources处理数据源
- model数组以sections组集合处理
- 结合Moya进行网络请求
- 使用ObjectMapper进行json数据转模型
1. 配合ObjectMapper
这里再介绍一下ObjectMapper
class AnchorModel: Mappable {var name = "" //名字var pic51 = "" //头像var pic74 = "" //大图var live = 0var push = 0var focus = 0 //关注量required init?(map: Map) {}func mapping(map: Map) {name <- map["name"]pic51 <- map["pic51"]pic74 <- map["pic74"]live <- map["live"]push <- map["push"]focus <- map["focus"]}
}
- 使用 ObjectMapper ,需要让自己的 Model 类使用 Mappable 协议,这个协议包括两个方法:
required init?(map: Map) {}func mapping(map: Map) {}
- 在 mapping 方法中,用
<-
操作符来处理和映射你的 JSON数据 - 详细的 ObjectMapper 教程可以查看它的 Github 主页,我在这里只做简单的介绍。
2. Moya的使用
- Moya是基于Alamofire的网络请求库,这里我使用了Moya/Swift,它在Moya的基础上添加了对RxSwift的接口支持。
- Github上的官方介绍罗列了Moya的一些特点:
- 编译时检查正确的API端点访问.
- 使你定义不同端点枚举值对应相应的用途更加明晰.
- 提高测试地位从而使单元测试更加容易.
- 接下来我们来说下Moya的使用
2-1. 创建一个枚举API
//请求枚举类型
enum JunNetworkTool {case getNewListcase getHomeList(page: Int)
}
2-2. 为枚举添加扩展
- 需遵循协议 TargetType
- 这个协议的Moya这个库规定的协议,可以单击进入相应的文件进行查看
- 这个协议内的每一个参数(除了
validate
可不重写)都必须重写,否则会报错
//请求参数
extension JunNetworkTool: TargetType {//统一基本的urlvar baseURL: URL {return (URL(string: "http://qf.56.com/home/v4/moreAnchor.ios"))!}//path字段会追加至baseURL后面var path: String {return ""}//请求的方式var method: Moya.Method {return .get}//参数编码方式(这里使用URL的默认方式)var parameterEncoding: ParameterEncoding {return URLEncoding.default}//用于单元测试var sampleData: Data {return "getList".data(using: .utf8)!}//将要被执行的任务(请求:request 下载:upload 上传:download)var task: Task {return .request}//请求参数(会在请求时进行编码)var parameters: [String: Any]? {switch self {case .getHomeList(let index):return ["index": index]default:return ["index": 1]}}//是否执行Alamofire验证,默认值为falsevar validate: Bool {return false}
}
2-3. 定义一个全局变量用于整个项目的网络请求
let junNetworkTool = RxMoyaProvider<JunNetworkTool>()
至此,我们就可以使用这个全局变量来请求数据了
3. RxDataSources
- RxDataSources是以section来做为数据结构来传输,这点很重要,比如:在传统的数据源实现的方法中有一个numberOfSection,我们在很多情况下只需要一个section,所以这个方法可实现,也可以不实现,默认返回的就是1,这给我们带来的一个迷惑点:【tableView是由row来组成的】,不知道在坐的各位中有没有是这么想的呢??有的话那从今天开始就要认清楚这一点,【tableView其实是由section组成的】,所以在使用RxDataSources的过程中,即使你的setion只有一个,那你也得返回一个section的数组出去!!!
- 传统方式适用于简单的数据集,但不处理需要将复杂数据集与多个部分进行绑定的情况,或者在添加/修改/删除项目时需要执行动画时。而使用RxDataSources时,它很容易写
- 想了解更多关于RxDataSources的用法,请参考其GitHub主页
3-1. Sections自定义
- 在我们自定义的Model中创建一个AnchorSection的结构体
- 并遵循SectionModelType协议,实现相应的协议方法
//MARK: SectionModel
struct AnchorSection {// items就是rowsvar items: [Item]// 你也可以这里加你需要的东西,比如 headerView 的 title
}extension AnchorSection: SectionModelType {// 重定义 Item 的类型为typealias Item = AnchorModelinit(original: AnchorSection, items: [AnchorSection.Item]) {self = originalself.items = items}
}
4. ViewModel
4-1. 自定义协议BaseViewModel
我们知道MVVM思想就是将原本在ViewController的视图显示逻辑、验证逻辑、网络请求等代码存放于ViewModel中,让我们的ViewController瘦身。这些逻辑由ViewModel负责,外界不需要关心,外界只需要结果,ViewModel也只需要将结果给到外界,基于此,我们定义了一个协议
protocol JunViewModelType {//associatedtype: 关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定associatedtype Inputassociatedtype Output//我们通过 transform 方法将input携带的数据进行处理,生成了一个Outputfunc transform(input: Input) -> Output
}
4-2. 自定义用于网络请求的刷新状态
- 根据枚举值的判断,改变collection的刷新状态
//刷新的状态
enum JunRefreshStatus {case nonecase beingHeaderRefreshcase endHeaderRefreshcase beingFooterRefreshcase endFooterRefreshcase noMoreData
}
4-3. 自定义用于继承的BaseViewModel
- 定义请求数据的页数index
- 定义input和output的结构体
class BaseViewModel: NSObject {// 记录当前的索引值var index: Int = 1struct JunInput {// 网络请求类型let category: JunNetworkToolinit(category: JunNetworkTool) {self.category = category}}struct JunOutput {// tableView的sections数据let sections: Driver<[AnchorSection]>// 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)let requestCommond = PublishSubject<Bool>()// 告诉外界的tableView当前的刷新状态let refreshStatus = Variable<JunRefreshStatus>(.none)//初始化时,section的数据init(sections: Driver<[AnchorSection]>) {self.sections = sections}}
}
4-4. 自定义AnchorViewModel
- 1) 继承BaseViewModel
class AnchorViewModel : BaseViewModel{// 存放着解析完成的模型数组let anchorArr = Variable<[AnchorModel]>([])}
- 2) 遵循JunViewModelType协议
extension AnchorViewModel: JunViewModelType {typealias Input = JunInputtypealias Output = JunOutputfunc transform(input: AnchorViewModel.JunInput) -> AnchorViewModel.JunOutput {let sectionArr = anchorArr.asDriver().map { (models) -> [AnchorSection] in// 当models的值被改变时会调用return [AnchorSection(items: models)]}.asDriver(onErrorJustReturn: [])let output = JunOutput(sections: sectionArr)output.requestCommond.subscribe(onNext: { (isReloadData) inself.index = isReloadData ? 1 : self.index + 1//开始请求数据junNetworkTool.request(JunNetworkTool.getHomeList(page: self.index)).mapObjectArray(AnchorModel.self).subscribe({ (event) inswitch event {case let .next(modelArr):self.anchorArr.value = isReloadData ? modelArr : (self.anchorArr.value) + modelArrSVProgressHUD.showSuccess(withStatus: "加载成功")case let .error(error):SVProgressHUD.showError(withStatus: error.localizedDescription)case .completed:output.refreshStatus.value = isReloadData ? .endHeaderRefresh : .endFooterRefresh}}).addDisposableTo(bag)}).addDisposableTo(bag)return output}
}
- sectionArr是将model数组按照section分别存储
- 当请求回来的anchorArr数据改变的时候, sectionArr随之会发生改变
- isReloadData用于区分是下拉刷新(true时), 还是上拉加载更多(false时)
5. RxCollectionViewController控制器中
- 创建数据源RxDataSources
- 绑定cell
- 初始化input和output请求
- 绑定section数据
- 设置刷新
5-1. 创建数据源RxDataSources
// 创建一个数据源属性,类型为自定义的Section类型
let dataSource = RxCollectionViewSectionedReloadDataSource<AnchorSection>()
5-2. 绑定cell(自定义的cell要提前注册)
dataSource.configureCell = { dataSource, collectionView, indexPath, item inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollecCellID, for: indexPath) as! RxCollectionViewCellcell.anchorModel = itemreturn cell
}
- 以上四个参数的顺序分别为:dataSource, collectionView(或者tableView), indexPath, model, 其对应类型不言而喻,不多做介绍
5-3. 初始化input和output请求
let vmInput = AnchorViewModel.JunInput(category: .getNewList)
let vmOutput = anchorVM.transform(input: vmInput)
5-4. 绑定section数据
//4-1. 通过dataSource和section的model数组绑定数据(demo的用法, 推荐)
vmOutput.sections.asDriver().drive(collectionVIew.rx.items(dataSource: dataSource)).addDisposableTo(bag)
5-5. 设置刷新
5-5-0. 在controller中初始化刷新状态
collectionVIew.mj_header = MJRefreshNormalHeader(refreshingBlock: {vmOutput.requestCommond.onNext(true)
})
collectionVIew.mj_header.beginRefreshing()collectionVIew.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {vmOutput.requestCommond.onNext(false)
})
5-5-1. 添加刷新的序列
- 在JunOutput的结构体中添加刷新序列
- 我们在进行网络请求并得到结果之后,修改refreshStatus的value为相应的JunRefreshStatus项
- MJRefre遍会根据该状态做出相应的刷新事件
- 默认状态为none
// 告诉外界的tableView当前的刷新状态
let refreshStatus = Variable<JunRefreshStatus>(.none)
5-5-2. 外界订阅output的refreshStatus
- 外界订阅output的refreshStatus,并且根据接收到的值进行相应的操作
- refreshStatus每次改变都会触发刷新事件
//5. 设置刷新状态
vmOutput.refreshStatus.asObservable().subscribe(onNext: { (status) inswitch status {case .beingHeaderRefresh:self.collectionVIew.mj_header.beginRefreshing()case .endHeaderRefresh:self.collectionVIew.mj_header.endRefreshing()case .beingFooterRefresh:self.collectionVIew.mj_footer.beginRefreshing()case .endFooterRefresh:self.collectionVIew.mj_footer.endRefreshing()case .noMoreData: self.collectionVIew.mj_footer.endRefreshingWithNoMoreData()default:break}
}).addDisposableTo(bag)
5-5-3. output提供一个requestCommond用于控制是否请求数据
- PublishSubject 的特点:即可以作为Observable,也可以作为Observer,说白了就是可以发送信号,也可以订阅信号
- 当你订阅PublishSubject的时候,你只能接收到订阅他之后发生的事件。subject.onNext()发出onNext事件,对应的还有onError()和onCompleted()事件
// 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)
let requestCommond = PublishSubject<Bool>()
七. 总结
- 为了研究RxSwift相关知识, 工作之余的时间,差不多一个月了
- 学习的瓶颈大部分在于网络请求和配合刷新这一模块
- 文中如出现self循环引用的问题,还望大神多多指正
- 小编目前也还在初学阶段,文中如出现小错误还望多多指正,如有更好的方法,也希望不吝分享
- 如果喜欢,可以收藏,也可以在Github上star一下
最后再一次附上Demo地址
参考文献:
- Moya
- ObjectMapper
- RxDataSources
- Kingfisher
- RxSwift的使用详解01
- RxSwift的使用详解02
- moya + RxSwift 进行网络请求
- 扒一扒swift中的unowned和weak下
- iOS - RxSwift -项目实战记录
RxSwift+Moya之项目实战相关推荐
- 计算机视觉一些项目实战技术(续)
计算机视觉一些项目实战技术(续) PROTO-OBJECT BASED SALIENCY 在本项目中,提出一种新的方法来完成显著目标侦测的任务.与以往基于聚光灯注意理论的显著目标检测器相比,遵循基于对 ...
- 计算机视觉一些项目实战技术
计算机视觉一些项目实战技术 SELECTIVE SEARCH FOR OBJECT LOCALISATION 需要多种策略来查找上述图像中的所有对象.勺子在桌子上的沙拉碗里.因此,图像本质上是层次性的 ...
- 【WEB API项目实战干货系列】- API登录与身份验证(三)
上一篇: [WEB API项目实战干货系列]- 接口文档与在线测试(二) 这篇我们主要来介绍我们如何在API项目中完成API的登录及身份认证. 所以这篇会分为两部分, 登录API, API身份验证. ...
- 【Rsync项目实战一】备份全网服务器数据
目录 [Rsync项目实战]备份全网服务器数据 [企业案例] 1.1 环境部署 1.2 开始部署backup服务器:Rsync服务端过程: 1.3 开始部署nfs01服务器:Rsync客户端过程: [ ...
- 数据量大了一定要分表,分库分表 Sharding-JDBC 入门与项目实战
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:juejin.im/post/684490418236581 ...
- 手把手教你洞悉 PyTorch 模型训练过程,彻底掌握 PyTorch 项目实战!(文末重金招聘导师)...
(文末重金招募导师) 在CVPR 2020会议接收中,PyTorch 使用了405次,TensorFlow 使用了102次,PyTorch使用数是TensorFlow的近4倍. 自2019年开始,越来 ...
- 9大项目实战!tensorflow2.0框架实战(免费资料+干货合集)
(翻至底部还有超多免费资料+干货合集) 随着PyTorch的不断发展,你是否开始抛弃TF转而向PT发起进攻了呢? 即便现在Pytorch发展迅速,但TensorFlow就像一个定时炸弹,你不知道什么时 ...
- 4个可以写进简历的京东 NLP 项目实战
01 京东AI项目实战课程安排 覆盖了从经典的机器学习.文本处理技术.序列模型.深度学习.预训练模型.知识图谱.图神经网络所有必要的技术. 项目一.京东健康智能分诊项目 第一周:文本处理与特征工程 | ...
- PWA项目实战分享(听书APP)
PWA项目实战分享 - BookPlayer 每天听本书App 因为自己有个需求,特别的痒,昼夜难免.第二天就开始起手做这个项目,利用业余时间,大概持续了10天时间(因为边学边做),从设计到数据(包括 ...
- App项目实战之路(二):API篇
原创文章,转载请注明:转载自Keegan小钢 并标明原文链接:http://keeganlee.me/post/practice/20160812 微信订阅号:keeganlee_me 写于2016- ...
最新文章
- 优秀logo设计解析_优秀Logo设计!汽车类标志表现手法
- 初识github之注册和基本概念
- matlab inpainting,MATLAB-Python-inpainting-codes-master
- crm系统是什么很棒ec实力_搭建CRM系统要明确几个步骤?什么样的CRM是真正有用的系统?...
- 如何写标题摘要关键字
- ★LeetCode(538)——把二叉搜索树转换为累加树(JavaScript)
- Spring中使用集成MongoDB Client启动时报错:rc: 48
- mongodb objectid java_我可以确定字符串是否是MongoDB ObjectID吗?
- es if语法 script_Elasticsear7.x DSL语法之文档管理
- vue 直传视频到阿里云OSS
- 打通最后100米:苏宁小店如何成为家门口的“共享冰箱”
- 企业进行客户关系管理的重要性是什么
- VO,DTO,BO,POJO,PO的概念介绍
- 数据库的设计关键点总结
- 联想r720安装固态_联想拯救者r720笔记本NVME接口M.2固态硬盘怎么安装win7系统
- postgresql 连接超时 timeout expired
- AB实验结果分析01-保证实验分析结果的准确性
- 数字图像处理(五)几何变换之图像平移、镜像、绕中心点旋转、缩放等
- 联想拯救者2021款R系列声音卡顿、间断呲呲问题修复指南
- 为什么spring cloud服务启动之后回到命令行会自动挂掉