返回 导航

Swift

hangge.com

Swift - RxSwift的使用详解53(MVVM架构演示3:使用Driver样例)

作者:hangge | 2018-04-30 08:10
    本文演示的样例效果同前文是一样的,都是做一个 GitHub 资源搜索功能。只不过前面 ViewModel 里的输入输出使用是普通的 Observable 序列,这次我们改用 Driver 这个特征序列。

四、一个使用 Driver 的 MVVM 样例

1,效果图

(1)当我们在表格上方的搜索框中输入文字时,会实时地去请求 GitHub 接口查询所有匹配的资源库。
(2)数据返回后会将查询结果数量显示在导航栏标题上,同时把最匹配的资源条目显示显示在表格中(这个是 GitHub 接口限制,由于数据太多,可能不会一次全部都返回)。
(3)点击某个单元格,会弹出显示该资源的详细信息(全名和描述)
(4)删除搜索框的文字后,表格内容同步清空,导航栏标题变成显示“hangge.com
            

2,准备工作

(1)首先我们在项目中配置好 RxSwiftAlamofireMoyaResult 这几个库,具体步骤可以参考我之前写的这篇文章:

(2)为了方便将结果映射成自定义对象,我们还需要引入 ObjectMapperMoya-ObjectMapper 这两个第三方库。具体步骤可以参考我之前写的这篇文章:

3,样例代码

(1)我们先创建一个 GitHubAPI.swift 文件作为网络请求层,里面的内容如下(这个同前文一样):
  • 首先定义一个 provider,即请求发起对象。往后我们如果要发起网络请求就使用这个 provider
  • 接着声明一个 enum 来对请求进行明确分类,这里我们只有一个枚举值表示查询资源。
  • 最后让这个 enum 实现 TargetType 协议,在这里面定义我们各个请求的 url、参数、header 等信息。
import Foundation
import Moya
import RxMoya

//初始化GitHub请求的provider
let GitHubProvider = MoyaProvider<GitHubAPI>()

/** 下面定义GitHub请求的endpoints(供provider使用)**/
//请求分类
public enum GitHubAPI {
    case repositories(String)  //查询资源库
}

//请求配置
extension GitHubAPI: TargetType {
    //服务器地址
    public var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }
    
    //各个请求的具体路径
    public var path: String {
        switch self {
        case .repositories:
            return "/search/repositories"
        }
    }
    
    //请求类型
    public var method: Moya.Method {
        return .get
    }
    
    //请求任务事件(这里附带上参数)
    public var task: Task {
        print("发起请求。")
        switch self {
        case .repositories(let query):
            var params: [String: Any] = [:]
            params["q"] = query
            params["sort"] = "stars"
            params["order"] = "desc"
            return .requestParameters(parameters: params,
                                      encoding: URLEncoding.default)
        default:
            return .requestPlain
        }
    }
    
    //是否执行Alamofire验证
    public var validate: Bool {
        return false
    }
    
    //这个就是做单元测试模拟的数据,只会在单元测试文件中有作用
    public var sampleData: Data {
        return "{}".data(using: String.Encoding.utf8)!
    }
    
    //请求头
    public var headers: [String: String]? {
        return nil
    }
}

(2)接着定义好相关模型:GitHubModel.swift(这个还是同前文一样)
import Foundation
import ObjectMapper

//包含查询返回的所有库模型
struct GitHubRepositories: Mappable {
    var totalCount: Int!
    var incompleteResults: Bool!
    var items: [GitHubRepository]! //本次查询返回的所有仓库集合
    
    init() {
        print("init()")
        totalCount = 0
        incompleteResults = false
        items = []
    }
    
    init?(map: Map) { }
    
    // Mappable
    mutating func mapping(map: Map) {
        totalCount <- map["total_count"]
        incompleteResults <- map["incomplete_results"]
        items <- map["items"]
    }
}

//单个仓库模型
struct GitHubRepository: Mappable {
    var id: Int!
    var name: String!
    var fullName:String!
    var htmlUrl:String!
    var description:String!
    
    init?(map: Map) { }
    
    // Mappable
    mutating func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
        fullName <- map["full_name"]
        htmlUrl <- map["html_url"]
        description <- map["description"]
    }
}

(3)下面就是本文的重头戏了。我们创建一个 ViewModel,它的作用就是将用户各种输入行为,转换成输出状态。和前文不同的是,本样例中不管输入还是输出都是 Driver 类型。
关于 Driver 的优点可以参考我之前写的这篇文章:Swift - RxSwift的使用详解18(特征序列2:Driver)
import Foundation
import RxSwift
import RxCocoa

class ViewModel {
    /**** 输入部分 ***/
    //查询行为
    fileprivate let searchAction:Driver<String>
    
    /**** 输出部分 ***/
    //所有的查询结果
    let searchResult: Driver<GitHubRepositories>
    
    //查询结果里的资源列表
    let repositories: Driver<[GitHubRepository]>
    
    //清空结果动作
    let cleanResult: Driver<Void>
    
    //导航栏标题
    let navigationTitle: Driver<String>
    
    //ViewModel初始化(根据输入实现对应的输出)
    init(searchAction:Driver<String>) {
        self.searchAction = searchAction
        
        //生成查询结果序列
        self.searchResult = searchAction
            .filter { !$0.isEmpty } //如果输入为空则不发送请求了
            .flatMapLatest{  //也可考虑改用flatMapFirst
                GitHubProvider.rx.request(.repositories($0))
                    .filterSuccessfulStatusCodes()
                    .mapObject(GitHubRepositories.self)
                    .asDriver(onErrorDriveWith: Driver.empty())
        }
        
        //生成清空结果动作序列
        self.cleanResult = searchAction.filter{ $0.isEmpty }.map{ _ in Void() }
        
        //生成查询结果里的资源列表序列(如果查询到结果则返回结果,如果是清空数据则返回空数组)
        self.repositories = Driver.merge(
            searchResult.map{ $0.items },
            cleanResult.map{[]}
        )
        
        //生成导航栏标题序列(如果查询到结果则返回数量,如果是清空数据则返回默认标题)
        self.navigationTitle = Driver.merge(
            searchResult.map{ "共有 \($0.totalCount!) 个结果" },
            cleanResult.map{ "hangge.com" }
        )
    }
}

(4)最后我们视图控制器(ViewController)只需要调用 ViewModel 进行数据绑定就可以了。可以看到由于网络请求、数据处理等逻辑已经被剥离到 ViewModel 中,VC 这边的负担大大减轻了。
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    
    //显示资源列表的tableView
    var tableView:UITableView!
    
    //搜索栏
    var searchBar:UISearchBar!
    
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //创建表视图
        self.tableView = UITableView(frame:self.view.frame, style:.plain)
        //创建一个重用的单元格
        self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        self.view.addSubview(self.tableView!)
        
        //创建表头的搜索栏
        self.searchBar = UISearchBar(frame: CGRect(x: 0, y: 0,
                                                   width: self.view.bounds.size.width, height: 56))
        self.tableView.tableHeaderView =  self.searchBar
        
        //查询条件输入
        let searchAction = searchBar.rx.text.orEmpty.asDriver()
            .throttle(0.5) //只有间隔超过0.5k秒才发送
            .distinctUntilChanged()
        
        //初始化ViewModel
        let viewModel = ViewModel(searchAction: searchAction)
        
        //绑定导航栏标题数据
        viewModel.navigationTitle.drive(self.navigationItem.rx.title).disposed(by: disposeBag)
        
        //将数据绑定到表格
        viewModel.repositories.drive(tableView.rx.items) { (tableView, row, element) in
            let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = element.name
            cell.detailTextLabel?.text = element.htmlUrl
            return cell
            }.disposed(by: disposeBag)
        
        //单元格点击
        tableView.rx.modelSelected(GitHubRepository.self)
            .subscribe(onNext: {[weak self] item in
                //显示资源信息(完整名称和描述信息)
                self?.showAlert(title: item.fullName, message: item.description)
            }).disposed(by: disposeBag)
    }
    
    //显示消息
    func showAlert(title:String, message:String){
        let alertController = UIAlertController(title: title,
                                                message: message, preferredStyle: .alert)
        let cancelAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)
        self.present(alertController, animated: true, completion: nil)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

功能改进:将网络请求服务提取出来

(1)从上面的样例可以发现,我们在 ViewModel 中是直接调用 Moya Provider 进行数据请求,并进行模型转换。
(2)我们也可以把网络请求和数据转换相关代码提取出来,作为一个专门的 Service。比如 GitHubNetworkService,内容如下:
import RxSwift
import RxCocoa
import ObjectMapper

class GitHubNetworkService {
    
    //搜索资源数据
    func searchRepositories(query:String) -> Driver<GitHubRepositories> {
        return GitHubProvider.rx.request(.repositories(query))
            .filterSuccessfulStatusCodes()
            .mapObject(GitHubRepositories.self)
            .asDriver(onErrorDriveWith: Driver.empty())
    }
}

(3)ViewModel 这边不再直接调用 provider,而是通过这个 Service 就获取需要的数据。可以看到代码简洁许多:
import Foundation
import RxSwift
import RxCocoa

class ViewModel {
    /**** 数据请求服务 ***/
    let networkService = GitHubNetworkService()
    
    /**** 输入部分 ***/
    //查询行为
    fileprivate let searchAction:Driver<String>
    
    /**** 输出部分 ***/
    //所有的查询结果
    let searchResult: Driver<GitHubRepositories>
    
    //查询结果里的资源列表
    let repositories: Driver<[GitHubRepository]>
    
    //清空结果动作
    let cleanResult: Driver<Void>
    
    //导航栏标题
    let navigationTitle: Driver<String>
    
    //ViewModel初始化(根据输入实现对应的输出)
    init(searchAction:Driver<String>) {
        self.searchAction = searchAction
        
        //生成查询结果序列
        self.searchResult = searchAction
            .filter { !$0.isEmpty } //如果输入为空则不发送请求了
            .flatMapLatest(networkService.searchRepositories) //也可考虑改用flatMapFirst
        
        //生成清空结果动作序列
        self.cleanResult = searchAction.filter{ $0.isEmpty }.map{ _ in Void() }
        
        //生成查询结果里的资源列表序列(如果查询到结果则返回结果,如果是清空数据则返回空数组)
        self.repositories = Driver.merge(
            searchResult.map{ $0.items },
            cleanResult.map{[]}
        )
        
        //生成导航栏标题序列(如果查询到结果则返回数量,如果是清空数据则返回默认标题)
        self.navigationTitle = Driver.merge(
            searchResult.map{ "共有 \($0.totalCount!) 个结果" },
            cleanResult.map{ "hangge.com" }
        )
    }
}
评论

全部评论(0)

回到顶部