Swift开发函数响应编程&MVVMRxSwift实战

RxSwift+Moya网络请求之项目实战

2017-10-08  本文已影响716人  TitanCoder

RxSwift+Moya之项目实战

RxSwift相关基本介绍和用法可参考:

一. 下面将将进行实战项目

二. Demo地址

下面简单看一下demo的界面

1. 登录注册

登录注册

2. UITableView和SearchBar

UITableView和SearchBar

3. UICollectionView和Moya

UICollectionView和Moya

三. 项目结构和框架

1. 结构

demo是使用的纯MVVM模式,因为RxSwift就是为MVVM而生。不懂MVVM的猿友可参考MVVM模式快速入门

项目结构

2. 项目框架

// Swift三方库
    // Rx
    pod 'RxSwift'  //RxSwift的必备库
    pod 'RxCocoa'  //对 UIKit Foundation 进行 Rx 化
    pod 'RxDataSources'   // 帮助我们优雅的使用tableView的数据源方法

    // 网络请求
    pod 'Moya/RxSwift'  // 为RxSwift专用提供,对Alamofire进行封装的一个网络请求库

    // 图片处理
    pod 'Kingfisher'  //图片处理库

    // 数据解析
    pod 'ObjectMapper'  //json转模型


    
// OC库
    // MJRefresh
    pod 'MJRefresh'   //MJ上拉下拉刷新
    pod 'SVProgressHUD'  //HUD

四. 注册界面

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
    }
    
    //判断密码字符个数>8
    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是一个返回是否成功的枚举值,可传入字符串变量

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 in
            return InputValidator.validateUserName(username)
        })
    }    

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

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 = searchText
        
        self.searchText.subscribe(onNext: { (str: String) in
            let 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)
    }

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) in
        cell.heroModel = hero
    }.addDisposableTo(bag)
        
    // 3.监听UITableView的点击
    rxTableView.rx.modelSelected(HeroModel.self).subscribe { (event: Event<HeroModel>) in
        print(event.element?.name ?? "")
    }.addDisposableTo(bag)

rxTableView.rx.setDelegate(self).addDisposableTo(bag)

然后在实现相应的代理方法即可,如:

extension RxTableViewController: UITableViewDelegate{
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}

六. UICollectionView+Moya+ObjectMapper网络请求和数据处理

1. 配合ObjectMapper

这里再介绍一下ObjectMapper

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"]
    }
}

required init?(map: Map) {}
 
func mapping(map: Map) {}

2. Moya的使用

2-1. 创建一个枚举API

//请求枚举类型
enum JunNetworkTool {
    
    case getNewList
    case getHomeList(page: Int)
}

2-2. 为枚举添加扩展

//请求参数
extension JunNetworkTool: TargetType {
    
    //统一基本的url
    var 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验证,默认值为false
    var validate: Bool {
        return false
    }
}

2-3. 定义一个全局变量用于整个项目的网络请求

let junNetworkTool = RxMoyaProvider<JunNetworkTool>()

至此,我们就可以使用这个全局变量来请求数据了

3. RxDataSources

3-1. Sections自定义

//MARK: SectionModel
struct AnchorSection {
    // items就是rows
    var items: [Item]
    
    // 你也可以这里加你需要的东西,比如 headerView 的 title
}

extension AnchorSection: SectionModelType {
    // 重定义 Item 的类型为
    typealias Item = AnchorModel
    init(original: AnchorSection, items: [AnchorSection.Item]) {
        self = original
        self.items = items
    }
}

4. ViewModel

4-1. 自定义协议BaseViewModel

我们知道MVVM思想就是将原本在ViewController的视图显示逻辑、验证逻辑、网络请求等代码存放于ViewModel中,让我们的ViewController瘦身。这些逻辑由ViewModel负责,外界不需要关心,外界只需要结果,ViewModel也只需要将结果给到外界,基于此,我们定义了一个协议

protocol JunViewModelType {
    //associatedtype: 关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定
    associatedtype Input
    associatedtype Output
    
    //我们通过 transform 方法将input携带的数据进行处理,生成了一个Output
    func transform(input: Input) -> Output
}

4-2. 自定义用于网络请求的刷新状态

//刷新的状态
enum JunRefreshStatus {
    case none
    case beingHeaderRefresh
    case endHeaderRefresh
    case beingFooterRefresh
    case endFooterRefresh
    case noMoreData
}

4-3. 自定义用于继承的BaseViewModel

class BaseViewModel: NSObject {
    // 记录当前的索引值
    var index: Int = 1
    
    struct JunInput {
        // 网络请求类型
        let category: JunNetworkTool
        
        init(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

class AnchorViewModel : BaseViewModel{
    // 存放着解析完成的模型数组
    let anchorArr = Variable<[AnchorModel]>([])

}
extension AnchorViewModel: JunViewModelType {
    typealias Input = JunInput
    typealias Output = JunOutput

    func 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) 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
    }
}

5. RxCollectionViewController控制器中

5-1. 创建数据源RxDataSources

// 创建一个数据源属性,类型为自定义的Section类型
let dataSource = RxCollectionViewSectionedReloadDataSource<AnchorSection>()

5-2. 绑定cell(自定义的cell要提前注册)

dataSource.configureCell = { dataSource, collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollecCellID, for: indexPath) as! RxCollectionViewCell
    cell.anchorModel = item
    return cell
}

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. 添加刷新的序列

// 告诉外界的tableView当前的刷新状态
let refreshStatus = Variable<JunRefreshStatus>(.none)

5-5-2. 外界订阅output的refreshStatus

//5. 设置刷新状态
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)

5-5-3. output提供一个requestCommond用于控制是否请求数据

// 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)
let requestCommond = PublishSubject<Bool>()

七. 总结

最后再一次附上Demo地址

参考文献:

上一篇下一篇

猜你喜欢

热点阅读