返回 导航

Swift

hangge.com

Swift - RxSwift的使用详解74(ReactorKit架构3:GitHub资源搜索样例)

作者:hangge | 2018-07-02 08:10
    GitHub 资源搜索与展示这个功能我之前也做过,当时分别用的是 MVVM 架构(点击查看)、以及 RxFeedback 架构(点击查看)。下面我使用 ReactorKit 来对其进行重构。

1,效果图

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

2,准备工作

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

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

(3)使用 ReactorKit 架构自然还要引入 ReactorKit 库,具体步骤可以参考我之前写的这篇文章:

(4)网络活动指示器以及错误提示框我使用的是 MBProgressHUD,具体步骤可以参考我之前写的这篇文章:

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(需要实现 ObjectMapper Mappable 协议,并设置好成员对象与 JSON 属性的相互映射关系。这里代码与之前那篇文章一样,没有变化。)
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)为了前台页面能根据不同的响应情况(成功或失败)进行不同的处理。我们在 Result.swift 中定义了结果类型枚举,以及失败类型枚举。
import Foundation

//响应结果枚举
enum Result<T, E: Error> {
    case success(T)  //成功(里面是返回的数据)
    case failure(E)  //失败(里面是错误原因)
}

//失败情况枚举
enum GitHubServiceError: Error {
    case offline
    case githubLimitReached
}

//失败枚举对应的错误信息
extension GitHubServiceError {
    var displayMessage: String {
        switch self {
        case .offline:
            return "网络链接失败!"
        case .githubLimitReached:
            return "请求太频繁,请稍后再试!"
        }
    }
}

(4)同时我们把网络请求和数据转换相关代码提取出来,作为一个专门的 ServiceGitHubNetworkService.swift),方便使用。
import RxSwift
import RxCocoa
import ObjectMapper

typealias SearchRepositoriesResponse = Result<(GitHubRepositories), GitHubServiceError>

class GitHubNetworkService {
    
    //搜索资源数据
    func searchRepositories(query:String) -> Observable<SearchRepositoriesResponse> {
        return GitHubProvider.rx.request(.repositories(query))
            .filterSuccessfulStatusCodes()
            .mapObject(GitHubRepositories.self)
            .map{ .success($0) } //成功返回
            .asObservable()
            .catchError({ error in
                print("发生错误:",error.localizedDescription)
                //失败返回(GitHub接口对请求频率有限制,太频繁会被拒绝:403)
                return Observable.of(.failure(.githubLimitReached))
            })
    }
}

(6)下面就是本文的重点了,在这里我们定义一个反应器(ViewReactor.swift),作用是将 Action 转换成 State
import RxSwift

final class ViewReactor: Reactor {
    
    //数据请求服务
    let networkService = GitHubNetworkService()
    
    //代表用户行为
    enum Action {
        case updateQuery(String?)  //查询条件内容改变
    }
    
    //代表状态变化
    enum Mutation {
        case setQuery(String?) //设置查询条件文字
        case setLoading(Bool) //设置搜索状态
        case setRepos(SearchRepositoriesResponse) //设置搜索结果
    }
    
    //代表页面状态
    struct State {        
        var query: String?  //搜索文字
        var loading: Bool = false //当前正在加载
        var results: [GitHubRepository] = []  //搜索结果集
        var lastError: GitHubServiceError? //错误信息
        var totalCount: Int = 0 //搜索结果数
        var title: String {
            get {
                if query == nil || query!.isEmpty {
                    return "hangge.com"
                } else {
                    return "共有 \(totalCount) 个结果"
                }
            }
        }
    }
    
    //初始页面状态
    let initialState: State
    
    init() {
        self.initialState = State()
    }
    
    //实现 Action -> Mutation 的转换
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        //搜索文字内容变化
        case let .updateQuery(query):
            //如果查询条件为空则直接清空结果(不需要查询)
            guard !query!.isEmpty else { return Observable.just(Mutation.setQuery(query)) }
            //依次执行下面2个状态变化动作
            return Observable.concat([
                //设置搜索文字
                Observable.just(Mutation.setQuery(query)),
                //设置搜索状态
                Observable.just(Mutation.setLoading(true)),
                //设置搜索结果
                networkService.searchRepositories(query: query!)
                    //如果查询条件再次变化,但上一次搜索还未完成,上一次搜索会自动取消
                    .takeUntil(self.action.filter(isUpdateQueryAction))
                    .map(Mutation.setRepos),
                //设置搜索状态
                Observable.just(Mutation.setLoading(false))
                ])
        }
    }
    
    //实现 Mutation -> State 的转换
    func reduce(state: State, mutation: Mutation) -> State {
        //从旧状态那里复制一个新状态
        var state = state
        
        //根据状态变化设置响应的状态值
        switch mutation {
        //设置查询条件
        case let .setQuery(query):
            state.query = query
            //查询条件为空则直接清空结果
            if state.query!.isEmpty {
                state.results = []
                state.totalCount = 0
            }
        //设置搜索状态
        case let .setLoading(value):
            state.loading = value
        //设置响应结果(成功)
        case .setRepos(.success(let repositories)):
            state.results = repositories.items
            state.totalCount = repositories.totalCount
            state.lastError = nil
        //设置响应结果(失败)
        case .setRepos(.failure(let error)):
            state.results = []
            state.totalCount = 0
            state.lastError = error
        }
        
        //返回新状态
        return state
    }
    
    //判断当前的 action 是否是查询条件改变
    private func isUpdateQueryAction(_ action: Action) -> Bool {
        if case .updateQuery = action {
            return true
        } else {
            return false
        }
    }
}

(7)主视图控制器(ViewController.swift)里的代码就很简单了,只需要将页面上的用户行为绑定到 reactor 里的 action 上,同时将 reactor 里的 state 绑定到页面 UI 控件上即可。
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController, StoryboardView {
    
    //显示资源列表的tableView
    var tableView:UITableView!
    
    //搜索栏
    var searchBar:UISearchBar!
    
    //活动指示器
    var loadingHUD:MBProgressHUD!
    
    var 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
        
        //创建一个加载指示器
        self.loadingHUD = MBProgressHUD.showAdded(to: self.view, animated: false)
        
        //设置reactor,会自动触发bind()方法
        self.reactor = ViewReactor()
        
        //单元格点击
        self.tableView.rx.modelSelected(GitHubRepository.self)
            .subscribe(onNext: {[weak self] item in
                //显示资源信息(完整名称和描述信息)
                self?.showAlert(title: item.fullName, message: item.description)
            }).disposed(by: disposeBag)
    }
    
    //处理绑定事件(该方法会在 self.reactor 变化时自动触发)
    func bind(reactor: ViewReactor) {
        
        //Action(实现 View -> Reactor 的绑定)
        searchBar.rx.text.orEmpty.changed  //搜索框文字改变事件
            .throttle(0.3, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .map(Reactor.Action.updateQuery)  //转换为 Action.updateQuery
            .bind(to: reactor.action)  //绑定到 reactor.action
            .disposed(by: disposeBag)
        
        // State(实现 Reactor -> View 的绑定)
        reactor.state.map { $0.results }  //得到最新搜索结果
            .bind(to: tableView.rx.items)(self.configureCell) //绑定到表格上
            .disposed(by: disposeBag)
        
        reactor.state.map { $0.title }  //得到最新标题文字
            .bind(to: navigationItem.rx.title) //绑定到导航栏上
            .disposed(by: disposeBag)
        
        reactor.state.map { !$0.loading }  //得到请求状态
            .bind(to: loadingHUD.rx.isHidden) //绑定到加载指示器上
            .disposed(by: disposeBag)
        
        //错误信息显示
        reactor.state.map{ $0.lastError }
            .subscribe(onNext: { [weak self] error in
                if let error = error, let view = self?.view {
                    print(error.displayMessage)
                    let hud = MBProgressHUD.showAdded(to: view, animated: true)
                    hud.mode = .text ////纯文本模式
                    hud.label.text = error.displayMessage
                    hud.removeFromSuperViewOnHide = true //隐藏时从父视图中移除
                    hud.hide(animated: true, afterDelay: 2)  //2秒钟后自动隐藏
                }
            })
            .disposed(by: disposeBag)
    }
    
    //单元格配置
    func configureCell(tableView: UITableView, row: Int, item: GitHubRepository)
        -> UITableViewCell {
            let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = item.name
            cell.detailTextLabel?.text = item.htmlUrl
            return cell
    }
    
    //显示消息
    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()
    }
}
评论

全部评论(0)

回到顶部