ABOUT ME

https://github.com/chongin12 chongin12@naver.com

Today
Yesterday
Total
  • [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는 갈 길이 멀다.

    반응형

    댓글

Designed by Tistory.