当前位置: > > > Swift - RxSwift的使用详解71(RxFeedback架构3:GitHub资源搜索样例)

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

    GitHub 资源搜索与展示这个功能我之前也做过,当时用的是 MVVM 架构(点击查看)。下面我使用 RxFeedback 来对其进行重构。

1,效果图

(1)当我们在表格上方的搜索框中输入文字时,会实时地去请求 GitHub 接口查询相匹配的资源库。
(2)数据返回后,会将结果数量显示在导航栏标题上,同时把匹配度最高的资源条目显示在表格中(这个是 GitHub 接口限制,由于数据太多,可能不会一次性全部返回)。
(3)在搜索过程中,页面中央会显示一个旋转的活动指示器,数据返回后指示器自动消失。
(4)如果搜索请求失败,会弹出相关的错误信息。
            

(5)点击某个单元格,会弹出显示该资源的详细信息(全名和描述)
(6)删除搜索框的文字后,表格内容同步清空,导航栏标题变成显示“hangge.com
        

2,准备工作

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

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

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

(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)同时我们把网络请求和数据转换相关代码提取出来,作为一个专门的 Service:GitHubNetworkService.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))
            })
    }
}

(5)主视图控制器(ViewController.swift)里的代码就是本文的重点了。与 MVVM 架构不同的是,我们不再需要定义 ViewModel,而是通过状态、事件、反馈循环来实现整个页面的业务逻辑分离。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback

class ViewController: UIViewController {
    
    //显示资源列表的tableView
    var tableView:UITableView!
    
    //搜索栏
    var searchBar:UISearchBar!
    
    let disposeBag = DisposeBag()
    
    //状态
    struct State {
        var search: String? { //搜索的文字
            didSet {
                if search == nil || search!.isEmpty {
                    self.title = "hangge.com"
                    self.loading = false
                    self.results = []
                    return
                }
                self.loading = true
            }
        }
        
        var title: String? //导航栏标题
        var loading: Bool //当前正在加载
        var results: [GitHubRepository] //搜索结果
        var lastError: GitHubServiceError? //错误信息
        
        //返回初始化状态
        static var empty: State {
            return State(search: nil, title: "hangge.com", loading: false,
                         results: [], lastError: nil)
        }
    }
    
    //事件
    enum Event {
        case searchChanged(String)
        case response(SearchRepositoriesResponse)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /**** 数据请求服务 ***/
        let networkService = GitHubNetworkService()
        
        //创建表视图
        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 loadingHUD = MBProgressHUD.showAdded(to: self.view, animated: false)

        Driver.system(
            initialState: State.empty,
            reduce: { (state: State, event: Event) -> State in
                switch event {
                case .searchChanged(let search):
                    var result = state
                    result.search = search
                    result.lastError = nil
                    return result
                case .response(.success(let repositories)):
                    var result = state
                    result.results = repositories.items
                    result.title = "共有 \(repositories.totalCount!) 个结果"
                    result.loading = false
                    result.lastError = nil
                    return result
                case .response(.failure(let error)):
                    var result = state
                    result.loading = false
                    result.lastError = error
                    return result
                }
            },
            feedback:
                bind(self) { me, state in
                    let subscriptions = [
                        //搜索结果绑定
                        state.map { $0.results }
                            .drive(me.tableView.rx.items)(me.configureCell),
                        //导航栏标题绑定
                        state.map { $0.title }
                            .drive(me.navigationItem.rx.title),
                        //加载指示器状态绑定
                        state.map { !$0.loading }
                            .drive(loadingHUD.rx.isHidden),
                        //消息指示器状态绑定
                        state.map { $0.lastError }.drive(onNext: { error in
                            if let error = error{
                                print(error.displayMessage)
                                let hud = MBProgressHUD.showAdded(to: self.view,
                                                                  animated: true)
                                hud.mode = .text ////纯文本模式
                                hud.label.text = error.displayMessage
                                hud.removeFromSuperViewOnHide = true //隐藏时从父视图中移除
                                hud.hide(animated: true, afterDelay: 2)  //2秒钟后自动隐藏
                            }
                        })
                    ]
                    
                    let events: [Signal<Event>] = [
                        me.searchBar.rx.text.orEmpty
                            .changed
                            .throttle(1, scheduler: MainScheduler.instance)//间隔超1秒才发
                            .asSignal(onErrorRecover: { _ in .empty() })
                            .map(Event.searchChanged)
                    ]
                    
                    return Bindings(subscriptions: subscriptions, events: events)
                },
                react(query: { $0.search }, effects: { search in
                    if search.isEmpty {
                        return Signal.empty()
                    }
                    return networkService.searchRepositories(query: search)
                        .asSignal(onErrorJustReturn: .failure(.offline))
                        .map(Event.response)
                })
            )
            .drive()
            .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 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()
    }
}

附:代码的拆分优化

    上面样例中我们把 RxFeedBack 架构用到的 stateeventfeedback 等等都定义在 ViewController 中,这样功能一旦多些,ViewController 里的代码就会变得十分冗长,难以阅读。下面我对代码进行改成,拆分成各个独立的文件。

1,GitHubRepositories+State.swift

StateEvent 以及相关的 reduce 都放在这里面。
import Foundation

//状态
struct GitHubRepositoriesState {
    var search: String? { //搜索的文字
        didSet {
            if search == nil || search!.isEmpty {
                self.title = "hangge.com"
                self.loading = false
                self.results = []
                return
            }
            self.loading = true
        }
    }
    
    var title: String? //导航栏标题
    var loading: Bool //当前正在加载
    var results: [GitHubRepository] //搜索结果
    var lastError: GitHubServiceError? //错误信息
}

//事件
enum GitHubRepositoriesEvent {
    case searchChanged(String) //搜索文字改变
    case response(SearchRepositoriesResponse) //结果响应
}

extension GitHubRepositoriesState {
    //返回初始化状态
    static var empty: GitHubRepositoriesState {
        return GitHubRepositoriesState(search: nil, title: "hangge.com",
                                loading: false, results: [], lastError: nil)
    }
    
    static func reduce(state: GitHubRepositoriesState, event: GitHubRepositoriesEvent)
        -> GitHubRepositoriesState {
            switch event {
            case .searchChanged(let search):
                var result = state
                result.search = search
                result.lastError = nil
                return result
            case .response(.success(let repositories)):
                var result = state
                result.results = repositories.items
                result.title = "共有 \(repositories.totalCount!) 个结果"
                result.loading = false
                result.lastError = nil
                return result
            case .response(.failure(let error)):
                var result = state
                result.loading = false
                result.lastError = error
                return result
            }
    }
}

2,GitHubRepositories+Feedback.swift

这里面放置与 UI 无关的反馈。
import Foundation
import RxSwift
import RxCocoa
import RxFeedback

struct GitHubRepositoriesFeedback {
    //验证用户名
    static func searchRepositories(networkService: GitHubNetworkService)
        -> (Driver<GitHubRepositoriesState>) -> Signal<GitHubRepositoriesEvent> {
            let query:(GitHubRepositoriesState) -> String?
                = { $0.search }
            
            let effects:(String) -> Signal<GitHubRepositoriesEvent>
                = { return networkService.searchRepositories(query: $0)
                    .asSignal(onErrorJustReturn: .failure(.offline))
                    .map(GitHubRepositoriesEvent.response) }
            
            return react(query: query, effects: effects)
    }
}

3,ViewController.swift

可以发现代码经过前面的提取之后,主视图控制器的代码变得简洁许多。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback

class ViewController: UIViewController {
    
    //显示资源列表的tableView
    var tableView:UITableView!
    
    //搜索栏
    var searchBar:UISearchBar!
    
    let disposeBag = DisposeBag()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /**** 数据请求服务 ***/
        let networkService = GitHubNetworkService()
        
        //创建表视图
        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 loadingHUD = MBProgressHUD.showAdded(to: self.view, animated: false)
        
        //UI绑定
        let bindUI: (Driver<GitHubRepositoriesState>) -> Signal<GitHubRepositoriesEvent> =
            bind(self) { me, state in
            let subscriptions = [
                //搜索结果绑定
                state.map { $0.results }
                    .drive(me.tableView.rx.items)(me.configureCell),
                //导航栏标题绑定
                state.map { $0.title }
                    .drive(me.navigationItem.rx.title),
                //加载指示器状态绑定
                state.map { !$0.loading }
                    .drive(loadingHUD.rx.isHidden),
                //消息指示器状态绑定
                state.map { $0.lastError }.drive(onNext: { error in
                    if let error = error{
                        print(error.displayMessage)
                        let hud = MBProgressHUD.showAdded(to: self.view,
                                                          animated: true)
                        hud.mode = .text ////纯文本模式
                        hud.label.text = error.displayMessage
                        hud.removeFromSuperViewOnHide = true //隐藏时从父视图中移除
                        hud.hide(animated: true, afterDelay: 2)  //2秒钟后自动隐藏
                    }
                })
            ]
            
            let events: [Signal<GitHubRepositoriesEvent>] = [
                me.searchBar.rx.text.orEmpty
                    .changed
                    .throttle(1, scheduler: MainScheduler.instance)//间隔超1秒才发
                    .asSignal(onErrorRecover: { _ in .empty() })
                    .map(GitHubRepositoriesEvent.searchChanged)
            ]
            
            return Bindings(subscriptions: subscriptions, events: events)
        }

        Driver.system(
            initialState: GitHubRepositoriesState.empty,
            reduce: GitHubRepositoriesState.reduce,
            feedback:
                //UI反馈
                bindUI,
                //非UI的自动反馈(资源搜索)
                GitHubRepositoriesFeedback.searchRepositories(networkService:
                    networkService)
            )
            .drive()
            .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 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