-
[ReactorKit#4] Example : GithubSearch 코드작성 (스토리보드 없이)iOS Dev/ReactorKit 2020. 6. 7. 16:22반응형
새 프로젝트 GithubSearch-Prac을 만들고 Source Control Navigator에서 Remotes를 우클릭해 Create Remote해서 깃허브에 연결해둔다.
저번 Counter과 마찬가지로 Storyboard 없이 작업할 것이기 때문에 #2번 포스트에서도 한 작업을 똑같이 해준다. 스토리보드 두개 지우고, 프로젝트 정보에서도 Main interface 부분 비우고, info.plist에서도 storyboard name 행을 지웠다.
커밋 추가(message : storyboard 삭제)
필요한 pod파일들도 설치해준다.
GithubSearchViewReactor를 만들어주고, 코드 작성을 시작한다.
// // GithubSearchViewReactor.swift // GithubSearch-Prac // // Created by 정종인 on 2020/06/07. // Copyright © 2020 swmaestro10th. All rights reserved. // import Foundation import ReactorKit import RxCocoa class GithubSearchViewReactor: Reactor { enum Action { case updateQuery(String?) case loadNextPage } enum Mutation { case setQuery(String?) case setRepos([String], nextPage: Int?) case appendRepos([String], nextPage: Int?) case setLoadingNextPage(Bool) } struct State { var query: String? var repos: [String] = [] var nextPage: Int? var isLoadingNextPage: Bool = false } let initialState: State = State() func mutate(action: Action) -> Observable<Mutation> { switch action { case .updateQuery(let query): return Observable.concat([ Observable.just(Mutation.setQuery(query)), self.search(query: query, page: 1) .takeUntil(self.action.filter(Action.isUpdateQueryAction(_:))) .map { Mutation.setRepos($0, nextPage: $1) } ]) case .loadNextPage: guard !self.currentState.isLoadingNextPage else { return Observable.empty() } guard let page = self.currentState.nextPage else { return Observable.empty() } return Observable.concat([ Observable.just(Mutation.setLoadingNextPage(true)), self.search(query: self.currentState.query, page: page) .takeUntil(self.action.filter(Action.isUpdateQueryAction(_:))) .map { Mutation.appendRepos($0, nextPage: $1) }, Observable.just(Mutation.setLoadingNextPage(false)) ]) } } func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { case let .setQuery(query): newState.query = query case let .setRepos(repos, nextPage: nextPage): newState.repos = repos newState.nextPage = nextPage case let .appendRepos(repos, nextPage: nextPage): newState.repos.append(contentsOf: repos) newState.nextPage = nextPage case let .setLoadingNextPage(isLoadingNextPage): newState.isLoadingNextPage = isLoadingNextPage } return newState } private func url(for query: String?, page: Int) -> URL? { guard let query = query, !query.isEmpty else { return nil } return URL(string: "https://api.github.com/search/repositories?q=\(query)&page=\(page)") } private func search(query: String?, page: Int) -> Observable<(repos: [String], nextPage: Int?)> { let emptyResult: ([String], Int?) = ([], nil) // 실패했을 때 내보낼 결과 guard let url = self.url(for: query, page: page) else { return Observable.just(emptyResult) } return URLSession.shared.rx.json(url: url) .map { json -> ([String], Int?) in guard let dictionary = json as? [String: Any] else { return emptyResult } guard let items = dictionary["items"] as? [[String: Any]] else { return emptyResult } let repos = items.compactMap { $0["full_name"] as? String } let nextPage = repos.isEmpty ? nil : page + 1 return (repos, nextPage) } .do(onError: { error in if case let .some(.httpRequestFailed(response, _)) = error as? RxCocoaURLError, response.statusCode == 403 { print("Github API rate limit exceeded.") } }) .catchErrorJustReturn(emptyResult) } } extension GithubSearchViewReactor.Action { static func isUpdateQueryAction(_ action: GithubSearchViewReactor.Action) -> Bool { if case .updateQuery = action { return true } else { return false } } }
커밋 추가(message : reactor)
다음엔 뷰를 추가할 것이다. GithubSearchView 파일을 하나 만들어준다. GithubSearchViewController도 만들어서 연결해준다.
GithubSearchView 코드 :
// // GithubSearchView.swift // GithubSearch-Prac // // Created by 정종인 on 2020/06/07. // Copyright © 2020 swmaestro10th. All rights reserved. // import Foundation import UIKit import ReactorKit import SnapKit import SafariServices class GithubSearchView: UIView { weak var vc: GithubSearchViewController? init(controlBy viewController: GithubSearchViewController) { self.vc = viewController super.init(frame: UIScreen.main.bounds) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private let baseView: UIView = { let v = UIView() v.backgroundColor = .white return v }() private let backgroundView: UIView = { let v = UIView() v.backgroundColor = .white return v }() let tableView: UITableView = { let v = UITableView() v.verticalScrollIndicatorInsets.top = v.contentInset.top v.backgroundColor = .white return v }() let searchController: UISearchController = { let v = UISearchController(searchResultsController: nil) v.obscuresBackgroundDuringPresentation = false return v }() var disposeBag: DisposeBag = DisposeBag() func setup() { setupUI() setBind() } private func setupUI() { addSubviews() setLayout() } private func setBind() { //delegate, datasource, register tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } private func addSubviews() { self.addSubview(baseView) baseView.addSubview(backgroundView) backgroundView.addSubview(tableView) } private func setLayout() { baseView.snp.makeConstraints { $0.top.left.bottom.right.equalToSuperview() } backgroundView.snp.makeConstraints { $0.top.left.bottom.right.equalTo(safeAreaLayoutGuide) } tableView.snp.makeConstraints { $0.top.left.bottom.right.equalToSuperview() } } } extension GithubSearchView: View { func bind(reactor: GithubSearchViewReactor) { searchController.searchBar.rx.text .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .map { Reactor.Action.updateQuery($0) } .bind(to: reactor.action) .disposed(by: disposeBag) tableView.rx.contentOffset .filter { [weak self] offset in guard let `self` = self else { return false } guard self.tableView.frame.height > 0 else { return false } return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100 } .map { _ in Reactor.Action.loadNextPage } .bind(to: reactor.action) .disposed(by: disposeBag) tableView.rx.itemSelected .subscribe(onNext: { [weak self, weak reactor] indexPath in guard let `self` = self else { return } self.endEditing(true) self.tableView.deselectRow(at: indexPath, animated: false) guard let repo = reactor?.currentState.repos[indexPath.row] else { return } guard let url = URL(string: "https://github.com/\(repo)") else { return } let viewController = SFSafariViewController(url: url) self.searchController.present(viewController, animated: true, completion: nil) }) .disposed(by: disposeBag) reactor.state.map { $0.repos } .bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in cell.textLabel?.text = repo } .disposed(by: disposeBag) } }
GithubSearchViewController 코드 :
// // GithubSearchViewController.swift // GithubSearch-Prac // // Created by 정종인 on 2020/06/07. // Copyright © 2020 swmaestro10th. All rights reserved. // import Foundation import UIKit class GithubSearchViewController: UIViewController { private lazy var githubSearchView = GithubSearchView(controlBy: self) override func viewDidLoad() { super.viewDidLoad() githubSearchView.setup() navigationItem.searchController = githubSearchView.searchController githubSearchView.reactor = GithubSearchViewReactor() } override func loadView() { self.view = githubSearchView } }
중간에 시행착오가 있었지만, 주 원인은 GithubSearchViewController에서 override func loadView()을 까먹었던 것이다. 잊지 말고 꼭 추가하도록 하자!
SceneDelegate 파일도 바꾸어 주어야 한다.
// // SceneDelegate.swift // GithubSearch-Prac // // Created by 정종인 on 2020/06/07. // Copyright © 2020 swmaestro10th. All rights reserved. // import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(frame: UIScreen.main.bounds) window?.windowScene = windowScene window?.rootViewController = UINavigationController(rootViewController: GithubSearchViewController()) window?.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } }
scene 함수부분을 바꾸어주면 된다.
커밋(message : finish)
자세한 내용은 https://github.com/chongin12/GithubSearch-Prac 에서 확인할 수 있다.
느낀 점 : tableview를 쓸 때 항상 커스텀 셀 클래스를 만들고, UITableViewDelegate, UITableViewDataSource 다 추가해주고, self.tableview.delegate = self, self.tableview.datasource = self 이런 작업을 해왔었는데, 이런 작업 없이 깔끔하게 cell register한번 하고 reactorKit으로 채워주니깐 느낌이 새롭다. 하지만 여전히 reactorKit과 RxSwift는 갈 길이 멀다.
반응형'iOS Dev > ReactorKit' 카테고리의 다른 글
[ReactorKit#5] Example : RxTodo 구조 분석[미완] (2) 2020.06.21 [ReactorKit#3] Example : GithubSearch 구조 분석 (0) 2020.06.07 [ReactorKit#2] Example : Counter 코드작성 (스토리보드 없이) (0) 2020.06.07 [ReactorKit#1] Example : Counter 구조 분석 (0) 2020.06.06 [ReactorKit#0] ReactorKit 시작하기 (2) 2020.06.05