当前位置: > > > 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 等信息。
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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 属性的相互映射关系。这里代码与之前那篇文章一样,没有变化。)
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
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 中定义了结果类型枚举,以及失败类型枚举。
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
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,方便使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,而是通过状态、事件、反馈循环来实现整个页面的业务逻辑分离。
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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 都放在这里面。
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
50
51
52
53
54
55
56
57
58
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 无关的反馈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

可以发现代码经过前面的提取之后,主视图控制器的代码变得简洁许多。
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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