下面将将进行实战项目
1.登录注册功能
输入用户名要大于6个字符,不然密码不能输入
密码必须大于6个字符,不然重复密码不能输入
重复密码输入必须和密码一样,不然注册按钮不能点击
根据输入的字符是否合法,按钮动态的改变颜色
2.UITableView和搜索SertchBar的应用
searchBar根据输入的字体展示包含该字体的cell列表
RxSwift 实现tableView列表展示
3.Moya +RxSwift 实现网络请求
4.Demo地址
下面简单看一下demo的界面 登录注册
UITableView和SearchBar
UICollectionView和Moya
项目结构和框架 结构 demo是使用的纯MVVM模式,因为RxSwift就是为MVVM而生。不懂MVVM的猿友可参考MVVM模式快速入门
项目框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pod 'RxSwift' pod 'RxCocoa' pod 'RxDataSources' pod 'Moya/RxSwift' pod 'Kingfisher' pod 'ObjectMapper' pod 'MJRefresh' pod 'SVProgressHUD'
注册界面
这里主要使用了Observable的相关知识,不了解的童鞋可参考RxSwift的使用详解01 ,了解Observable的操作
注册和登录并没有保存已注册的账号和密码, 故登录功能并不完善,后期会在完善,望知晓
下面将针对注册用户名做简单介绍:
字符串的语法法则 首先在model里处理输入字符串的语法法则和字符个数是否符合规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 } class 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是一个返回是否成功的枚举值,可传入字符串变量
1 2 3 4 enum Result { case success(message: String) case failure(message: String) }
判断该用户名 根据输入的用户名判断该用户名是否可用
1 2 3 4 5 6 7 8 9 10 11 12 13 var usernameObserable: Observable<Result> var passwordObserable: Observable<Result> var repeatPassObserable: Observable<Result> var registerBtnObserable: Observable<Bool> init(){ usernameObserable = username.asObservable().map({ (username) -> Result in return InputValidator.validateUserName(username) }) }
该返回参数Result,控制器将根据该Result是否成功来改变输入框是否是可编辑状态
初始化方法中,我们对传入的序列进行处理和转换成相对应的Result序列
controller逻辑 根据用户名输入改变各控件状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 usernameTextField.rx.text .orEmpty .bindTo(registerVM.username) .addDisposableTo(bag) registerVM.usernameObserable .bindTo(usernameHintLabel.rx.validationResult) .addDisposableTo(bag) registerVM.usernameObserable .bindTo(passwordTextField.rx.enableResult) .addDisposableTo(bag)
检测输入用户名是否符合规范
根据账号监听提示字体的状态
根据账号监听密码输入框的状态
根据账号监听注册按钮的状态
UITableView和SearchBar
该UITableView展示界面并未涉及网络请求
数据来源plist文件
图片为本地图片,可下载demo,在demo中查找图片
选用自定义UITableViewCell,故cell不做介绍
model小编这里也不多做介绍,详情可下载demo看具体代码
viewModel中逻辑 获取模型数组 读取plist文件,获取模型数组
1 2 3 4 5 6 7 8 9 10 fileprivate func getHeroData() -> [HeroModel]{ let path = Bundle.main.path(forResource: "heros.plist" , ofType: nil )! let dictArray = NSArray (contentsOfFile: path) as! [[String : Any]] return dictArray.map({ HeroModel(dict: $0 ) }).reversed() }
seachBar 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 lazy var heroVariable: Variable<[HeroModel]> = { return Variable(self .getHeroData()) }() var searchText: Observable<String> init(searchText: Observable<String>) { self .searchText = searchText self .searchText.subscribe(onNext: { (str: String) in let heros = self .getHeroData().filter({ (hero: HeroModel) -> Bool in if str.isEmpty { return true } return hero.name.contains(str) }) self .heroVariable.value = heros }).addDisposableTo(bag) }
其中heroVariable是一个数组模型的包装箱,在controller内调用使用前需要asObservable或者asDriver解包装;详细用法可参考:RxSwift的使用详解01
searchText搜索框输入的关键字,根据该关键字从数组中过滤出所有包含该关键字的model
对heroVariable重新赋值,发出事件
RxTableViewController searchBar搜索框 searchBar搜索框,输入字符后间隔0.5秒开始搜索
1 2 3 4 5 var searchText: Observable<String> { return searchBar.rx.text.orEmpty.throttle(0.5 , scheduler: MainScheduler.instance) }
UITableView的设置 1 2 3 4 5 6 7 8 9 10 11 heroVM.heroVariable.asDriver().drive(rxTableView.rx.items(cellIdentifier: kCellID, cellType: RxTableViewCell.self)) { (_, hero, cell) in cell.heroModel = hero }.addDisposableTo(bag) rxTableView.rx.modelSelected(HeroModel.self).subscribe { (event: Event<HeroModel>) in print(event.element?.name ?? "" ) }.addDisposableTo(bag)
将viewModel中的heroVariable进行解包装,如果是Driver序列,我们这里不使用bingTo,而是使用的Driver,用法和bingTo一模一样。
Deriver的监听一定发生在主线程,所以很适合我们更新UI的操作
如需设置delegate的代理
1 rxTableView.rx.setDelegate(self ).addDisposableTo(bag)
然后在实现相应的代理方法即可,如:
1 2 3 4 5 extension RxTableViewController: UITableViewDelegate { func tableView(_ tableView: UITableView , heightForRowAt indexPath: IndexPath) -> CGFloat { return 100 } }
网络请求和数据处理
配合ObjectMapper 这里再介绍一下ObjectMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class AnchorModel: Mappable { var name = "" var pic51 = "" var pic74 = "" var live = 0 var push = 0 var 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 协议,这个协议包括两个方法:
1 2 3 required init?(map: Map) {} func mapping(map: Map) {}
在 mapping 方法中,用 <-
操作符来处理和映射你的 JSON数据
详细的 ObjectMapper 教程可以查看它的 Github 主页 ,我在这里只做简单的介绍。
Moya的使用
Moya 是基于Alamofire 的网络请求库,这里我使用了Moya/Swift,它在Moya的基础上添加了对RxSwift的接口支持。
Github上的官方介绍罗列了Moya的一些特点:
编译时检查正确的API端点访问.
使你定义不同端点枚举值对应相应的用途更加明晰.
提高测试地位从而使单元测试更加容易.
接下来我们来说下Moya的使用
创建一个枚举API 1 2 3 4 5 6 enum JunNetworkTool { case getNewList case getHomeList(page: Int) }
为枚举添加扩展
需遵循协议 TargetType
这个协议的Moya这个库规定的协议,可以单击进入相应的文件进行查看
这个协议内的每一个参数(除了validate
可不重写)都必须重写,否则会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 extension JunNetworkTool: TargetType { var baseURL: URL { return (URL(string: "http://qf.56.com/home/v4/moreAnchor.ios" ))! } var path: String { return "" } var method: Moya.Method { return .get } var parameterEncoding: ParameterEncoding { return URLEncoding.default } var sampleData: Data { return "getList" .data(using: .utf8)! } var task: Task { return .request } var parameters: [String: Any]? { switch self { case .getHomeList(let index): return ["index" : index] default : return ["index" : 1 ] } } var validate: Bool { return false } }
全局变量 定义一个全局变量用于整个项目的网络请求
1 let junNetworkTool = RxMoyaProvider<JunNetworkTool>()
至此,我们就可以使用这个全局变量来请求数据了
RxDataSources是以section来做为数据结构来传输,这点很重要,比如:在传统的数据源实现的方法中有一个numberOfSection,我们在很多情况下只需要一个section,所以这个方法可实现,也可以不实现,默认返回的就是1,这给我们带来的一个迷惑点:【tableView是由row来组成的】,不知道在坐的各位中有没有是这么想的呢??有的话那从今天开始就要认清楚这一点,【tableView其实是由section组成的】,所以在使用RxDataSources的过程中,即使你的setion只有一个,那你也得返回一个section的数组出去!!!
传统方式适用于简单的数据集,但不处理需要将复杂数据集与多个部分进行绑定的情况,或者在添加/修改/删除项目时需要执行动画时。而使用RxDataSources时,它很容易写
想了解更多关于RxDataSources 的用法,请参考其GitHub主页
Sections自定义
在我们自定义的Model中创建一个AnchorSection的结构体
并遵循SectionModelType协议,实现相应的协议方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct AnchorSection { var items: [Item] } extension AnchorSection: SectionModelType { typealias Item = AnchorModel init(original: AnchorSection, items: [AnchorSection.Item]) { self = original self .items = items } }
ViewModel 自定义协议BaseViewModel 我们知道MVVM思想就是将原本在ViewController的视图显示逻辑、验证逻辑、网络请求等代码存放于ViewModel中,让我们的ViewController瘦身。这些逻辑由ViewModel负责,外界不需要关心,外界只需要结果,ViewModel也只需要将结果给到外界,基于此,我们定义了一个协议
1 2 3 4 5 6 7 8 9 protocol JunViewModelType { associatedtype Input associatedtype Output func transform(input: Input) -> Output }
自定义刷新
自定义用于网络请求的刷新状态
根据枚举值的判断,改变collection的刷新状态
1 2 3 4 5 6 7 8 9 10 enum JunRefreshStatus { case none case beingHeaderRefresh case endHeaderRefresh case beingFooterRefresh case endFooterRefresh case noMoreData }
自定义用于继承的BaseViewModel
定义请求数据的页数index
定义input和output的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class BaseViewModel: NSObject { var index: Int = 1 struct JunInput { let category: JunNetworkTool init(category: JunNetworkTool) { self .category = category } } struct JunOutput { let sections: Driver<[AnchorSection]> let requestCommond = PublishSubject<Bool>() let refreshStatus = Variable<JunRefreshStatus>(.none) init(sections: Driver<[AnchorSection]>) { self .sections = sections } } }
自定义AnchorViewModel
1 2 3 4 5 class AnchorViewModel : BaseViewModel{ let anchorArr = Variable<[AnchorModel]>([]) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 extension AnchorViewModel: JunViewModelType { typealias Input = JunInput typealias Output = JunOutput func transform(input: AnchorViewModel.JunInput) -> AnchorViewModel.JunOutput { let sectionArr = anchorArr.asDriver().map { (models) -> [AnchorSection] in return [AnchorSection(items: models)] }.asDriver(onErrorJustReturn: []) let output = JunOutput(sections: sectionArr) output.requestCommond.subscribe(onNext: { (isReloadData) in self .index = isReloadData ? 1 : self .index + 1 junNetworkTool.request(JunNetworkTool.getHomeList(page: self .index)) .mapObjectArray(AnchorModel.self) .subscribe({ (event) in switch event { case let .next(modelArr): self .anchorArr.value = isReloadData ? modelArr : (self .anchorArr.value) + modelArr SVProgressHUD.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时)
RxCollectionViewController控制器中
创建数据源RxDataSources
绑定cell
初始化input和output请求
绑定section数据
设置刷新
创建数据源RxDataSources 1 2 let dataSource = RxCollectionViewSectionedReloadDataSource<AnchorSection>()
绑定cell(自定义的cell要提前注册) 1 2 3 4 5 dataSource.configureCell = { dataSource, collectionView, indexPath, item in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollecCellID, for : indexPath) as! RxCollectionViewCell cell.anchorModel = item return cell }
以上四个参数的顺序分别为:dataSource, collectionView(或者tableView), indexPath, model, 其对应类型不言而喻,不多做介绍
1 2 let vmInput = AnchorViewModel.JunInput(category: .getNewList) let vmOutput = anchorVM.transform(input: vmInput)
绑定section数据 1 2 3 4 5 vmOutput.sections .asDriver() .drive(collectionVIew.rx.items(dataSource: dataSource)) .addDisposableTo(bag)
设置刷新 在controller中初始化刷新状态 1 2 3 4 5 6 7 8 collectionVIew.mj_header = MJRefreshNormalHeader(refreshingBlock: { vmOutput.requestCommond.onNext(true ) }) collectionVIew.mj_header.beginRefreshing() collectionVIew.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { vmOutput.requestCommond.onNext(false ) })
添加刷新的序列
在JunOutput的结构体中添加刷新序列
我们在进行网络请求并得到结果之后,修改refreshStatus的value为相应的JunRefreshStatus项
MJRefre遍会根据该状态做出相应的刷新事件
默认状态为none
1 2 let refreshStatus = Variable<JunRefreshStatus>(.none)
外界订阅output的refreshStatus
外界订阅output的refreshStatus,并且根据接收到的值进行相应的操作
refreshStatus每次改变都会触发刷新事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vmOutput.refreshStatus.asObservable().subscribe(onNext: { (status) in switch 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)
output提供一个requestCommond用于控制是否请求数据
PublishSubject 的特点:即可以作为Observable,也可以作为Observer,说白了就是可以发送信号,也可以订阅信号
当你订阅PublishSubject的时候,你只能接收到订阅他之后发生的事件。subject.onNext()发出onNext事件,对应的还有onError()和onCompleted()事件
1 2 let requestCommond = PublishSubject<Bool>()
总结
为了研究RxSwift相关知识, 工作之余的时间,差不多一个月了
学习的瓶颈大部分在于网络请求和配合刷新这一模块
文中如出现self循环引用的问题,还望大神多多指正
小编目前也还在初学阶段,文中如出现小错误还望多多指正,如有更好的方法,也希望不吝分享
如果喜欢,可以收藏,也可以在Github上star一下
参考文献: