美文网首页iOS-Rx
RxSwift官方实例十一(github搜索)

RxSwift官方实例十一(github搜索)

作者: 酒茶白开水 | 来源:发表于2021-01-05 15:52 被阅读0次

    代码下载

    搭建UI

    新建一个控制器,搭建如下UI:


    模型层处理

    定义如下数据模型用于操作github搜索的数据:

    enum GitHubServiceError: Error {
        case offline
        case githubLimitReached
        case networkError
    }
    struct Repository: CustomDebugStringConvertible {
        var name: String
        var url: URL
    
        init(name: String, url: URL) {
            self.name = name
            self.url = url
        }
        
        var debugDescription: String {
            return "\(name) | \(url)"
        }
    }
    class Unique: NSObject {
    }
    
    enum GitHubCommand {
        case changeSearch(text: String)
        case loadMoreItems
        case gitHubResponseReceived(SearchRepositoriesResponse)
    }
    
    struct Version<Value>: Hashable {
    
        private let _unique: Unique
        let value: Value
    
        init(_ value: Value) {
            self._unique = Unique()
            self.value = value
        }
    
        func hash(into hasher: inout Hasher) {
            hasher.combine(self._unique)
        }
    
        static func == (lhs: Version<Value>, rhs: Version<Value>) -> Bool {
            return lhs._unique === rhs._unique
        }
    }
    
    struct GitHubSearchRepositoriesState {
        static let initial = GitHubSearchRepositoriesState(searchText: "")
        
        var searchText: String
        var shouldLoadNextPage: Bool
        var repositories: Version<[Repository]>
        var nextURL: URL?
        var failure: GitHubServiceError?
    
        init(searchText: String) {
            self.searchText = searchText
            shouldLoadNextPage = true
            repositories = Version([])
            nextURL = URL(string: "https://api.github.com/search/repositories?q=\(searchText.URLEscaped)")
            failure = nil
        }
        
        static func reduce(state: GitHubSearchRepositoriesState, command: GitHubCommand) -> Self {
            var result = state
            switch command {
            case let .changeSearch(text):
                result = GitHubSearchRepositoriesState(searchText: text)
                result.failure = state.failure
            case let .gitHubResponseReceived(response):
                switch response {
                case let .success((repositories, url)):
                    result.repositories = Version(state.repositories.value + repositories)
                    result.shouldLoadNextPage = false
                    result.nextURL = url
                    result.failure = nil
                case let .failure(error):
                    result.failure = error
                }
            case .loadMoreItems:
                if state.failure == nil {
                    result.shouldLoadNextPage = true
                }
            }
            return result
        }
    }
    

    GitHubServiceError枚举表示GitHub搜索的网络错误。

    Repository结构用于表示GitHub搜索的仓库信息,name和url属性存储仓库名称和地址。

    GitHubCommand结构用于表示用户操作事件。

    Version结构用于包装其他数据模型,使其遵守Hashable协议并实现==比较函数。

    GitHubSearchRepositoriesState结构表示程序当前状态,属性searchText(搜索的文本)、shouldLoadNextPage(是否加载下一页)、repositories(搜索到的结果仓库)、nextURL(下次获取数据的url地址)、failure(网络请求参数)。其reduce函数是根据GitHubSearchRepositoriesState、GitHubCommand参数返回GitHubSearchRepositoriesState,说白了就是根据操作处理状态。

    定义一个数据类型SearchRepositoriesResponse用于表示网络请求结果:

    typealias SearchRepositoriesResponse = Result<(repositories: [Repository], nextURL: URL?), GitHubServiceError>
    

    定义GitHubSearchRepositoriesAPI类获取、解析、包装网络数据,具体实现可以查看源代码,使用该类的如下函数来搜索github仓库数据:

        func loadSearchURL(searchURL: URL) -> Observable<SearchRepositoriesResponse>
    

    ViewModel

    定义GithubSearchViewModel结构,属性loading表示网络加载的序列,属性sections表示列表数据的序列:

    struct GithubSearchViewModel {
        let loading: Driver<Bool>
        let sections: Driver<[SectionModel<String, Repository>]>
    
        init(search: RxCocoa.ControlProperty<String?>, loadMore: Observable<(Bool)>) {
            let activity = ActivityIndicator()
            loading = activity.loading
            let searchText = search.orEmpty.changed
                .asDriver()
                .throttle(.milliseconds(300))
                .distinctUntilChanged()
                .map(GitHubCommand.changeSearch)
    
            let loadNextPage = loadMore
                .withLatestFrom(loading, resultSelector: { $0 && (!$1) })
                .filter({ $0 })
                .map({ _ in GitHubCommand.loadMoreItems })
                .asDriver(onErrorDriveWith: Driver.empty())
            
            let inputFeedback: (Driver<GitHubSearchRepositoriesState>) -> Driver<GitHubCommand> = { state in
                let performSearch = state.flatMapLatest { (state) -> Driver<GitHubCommand> in
                    if (!state.shouldLoadNextPage) || state.searchText.isEmpty || state.nextURL == nil {
                        return Driver.empty()
                    }
                    
                    return GitHubSearchRepositoriesAPI.sharedAPI.loadSearchURL(searchURL: state.nextURL!)
                        .trackActivity(activity)
                        .asDriver(onErrorJustReturn: Result.failure(GitHubServiceError.networkError))
                        .map(GitHubCommand.gitHubResponseReceived)
                }
                return Driver.merge(searchText, loadNextPage, performSearch)
            }
            
            sections = Driver<GitHubSearchRepositoriesState>.deferred {
                let subject = ReplaySubject<GitHubSearchRepositoriesState>.create(bufferSize: 1)
                let commands = inputFeedback(subject.asDriver(onErrorDriveWith: Driver.empty()))
                return commands.scan(GitHubSearchRepositoriesState.initial, accumulator: GitHubSearchRepositoriesState.reduce(state:command:))
                    .do { (s) in
                        subject.onNext(s)
                    } onSubscribed: {
                        subject.onNext(GitHubSearchRepositoriesState.initial)
                    }.startWith(GitHubSearchRepositoriesState.initial)
            }.map { [SectionModel(model: "Repositories", items: $0.repositories.value)] }
        }
    }
    

    初始化方法分析:

    • 初始化函数接收两个事件参数分别是search(表示搜索事件)、loadMore(表示加载更多的事件)
    • 创建ActivityIndicator对象记录网络状态
    • 使用一些操作使搜索事件的序列去空、防抖等并转化成元素为GitHubCommand的序列
    • 加载更多的事件序列使用withLatestFrom操作符来根据当前网络加载状态来判断是否真正需要加载下一页,使用一些操作符去除不需要的元素、转化成元素为GitHubCommand的序列、转化为Driver类型的序列
    • 定义一个inputFeedback闭包将元素为GitHubSearchRepositoriesState的Driver序列转化为元素为GitHubCommand的Driver序列,在闭包内部根据GitHubSearchRepositoriesState的属性创建一个执行网络的请求的performSearch序列,然后合并搜索、加载下一页、执行网络请求操作序列返回。
    • 使用deferred操作符构建表示列表数据的Driver序列,构建一个ReplaySubject并将其转化为Driver作为参数执行inputFeedback闭包得到一个GitHubCommand序列,然后使用scan操作符扫描GitHubCommand序列用一个初始GitHubSearchRepositoriesState累计结果、使用GitHubSearchRepositoriesState的reduce函数处理每个元素,然后使用do操作符在序列被订阅和发出元素时让之前的ReplaySubject发出元素,这样做就能让每个GitHubCommand操作先执行GitHubSearchRepositoriesState的reduce函数然后根据结果判断是否执行网络请求(这个步骤需要注意),最后使用map操作符转化为目标类型序列。

    数据绑定

    先定义如下扩展,方便获取UIScrollView滚动到底部的序列:

    extension UIScrollView {
        func isNearBottomEdge(edgeOffset: CGFloat = 20.0) -> Bool {
            self.contentOffset.y + self.bounds.size.height + edgeOffset > self.contentSize.height
        }
    }
    extension Reactive where Base: UIScrollView {
        func nearBottom(edgeOffset: CGFloat = 20.0) -> Observable<Bool> {
            contentOffset.map { _ in base.isNearBottomEdge(edgeOffset: edgeOffset) }
        }
    }
    

    回到控制器中,构建如下TableViewSectionedDataSource辅助绑定UITableView:

    let dataSource = TableViewSectionedDataSource<SectionModel<String, Repository>>(cellForRow: { (ds, tv, ip) -> UITableViewCell in
            let cell = CommonCell.cellFor(tableView: tv)
            let repository = ds[ip]
            cell.textLabel?.text = repository.name
            cell.detailTextLabel?.text = repository.url.absoluteString
            
            return cell
        }, titleForHeader: { (ds, tv, i) -> String? in
            let section = ds[i]
            
            return section.items.count > 0 ? "\(section.items.count)个仓库" : "没有发现\(section.model)仓库"
        })
    

    在viewDidLoad函数中构建viewModel,绑定数据:

            let viewModel = GithubSearchViewModel(search: searchBar.rx.text, loadMore: tableView.rx.nearBottom())
    
            // 数据绑定
            viewModel.sections
                .drive(tableView.rx.items(dataSource: dataSource))
                .disposed(by: bag)
    
            // 选中数据
            tableView.rx.modelSelected(Repository.self)
                .subscribe(onNext: { (repository) in
                    if UIApplication.shared.canOpenURL(repository.url) {
                        if #available(iOS 10.0, *) {
                            UIApplication.shared.open(repository.url, completionHandler: nil)
                        } else {
                            UIApplication.shared.openURL(repository.url)
                        }
                    }
                }).disposed(by: bag)
            
            // 选中行
            tableView.rx.itemSelected
                .subscribe(onNext: { [weak self] in self!.tableView.deselectRow(at: $0, animated: true) })
                .disposed(by: bag)
            
            // 网络请求中
            viewModel.loading.drive(onNext: { [weak self] in
                    UIApplication.shared.isNetworkActivityIndicatorVisible = $0
                    $0 && self!.tableView.isNearBottomEdge(edgeOffset: 20.0) ? self!.startAnimating() : self!.stopAnimating()
                }).disposed(by: bag)
            
            // 滑动tableView
            self.tableView.rx.contentOffset.distinctUntilChanged()
                .subscribe({ [weak self] _ in
                    if self!.searchBar.isFirstResponder {
                        self!.searchBar.resignFirstResponder()
                    }
                }).disposed(by: bag)
    

    相关文章

      网友评论

        本文标题:RxSwift官方实例十一(github搜索)

        本文链接:https://www.haomeiwen.com/subject/bshwoktx.html