처음 reactorkit을 적용했던 Counter는 비교적 간단했는데, Github repository를 search 하는 이번 예제는 구현하는 데 꽤나 복잡했다.
복잡한 만큼 2개의 게시글로 나눌 생각이다.
이번 게시글은 Reactor부분.
코드를 하나하나 보며 이번 기회에 Reactorkit의 흐름을 좀 더 이해해보자.
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 let .updateQuery( query) :
return Observable.concat([
Observable.just(Mutation.setQuery(query)),
self.search(query: query, page : 1)
.take(until: 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)
.take(until: self.action.filter(Action.isUpdateQueryAction))
.map { Mutation.appendRepos($0, nextPage: $1) },
Observable.just(Mutation.setLoadingNextPage(false))
])
}
}
updateQuery
loadNextPage
func reduce(state: State, mutation: Mutation) -> State {
switch mutation {
case let .setQuery(query):
var newState = state
newState.query = query
return newState
case let .setRepos(repos, nextPage):
var newState = state
newState.repos = repos
newState.nextPage = nextPage
return newState
case let .appendRepos(repos, nextPage):
var newState = state
newState.repos.append(contentsOf: repos)
newState.nextPage = nextPage
return newState
case let .setLoadingNextPage(isLoadingNextPage):
var newState = state
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 .just(emptyResult)}
return URLSession.shared.rx.json(url:url)
.map{ json -> ([String],Int?) in
guard let dict = json as? [String: Any] else { return emptyResult }
guard let items = dict["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. Wait for 60 seconds and try again.")
}
})
.catchAndReturn(emptyResult)
}
url 메소드 : query와 page를 받아 url로 만들어줄 메소드
search 메소드 : url을 받아, json 형태로 변형 후 repos 배열과 nextpage를 return
Observable 형태로 최종 return
에러 처리
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 var repos : [Repository.RepoName] = []
private var nextPage : Int?
private func search(query: String?, page: Int) -> Observable<(repos: [Repository.RepoName], nextPage: Int?)> {
let emptyResult: ([Repository.RepoName], Int?) = ([], nil)
guard let url = self.url(for: query, page: page) else {return .just(emptyResult)}
AF.request(url,method: .get)
.responseDecodable(of: Repository.self){[weak self] response in
guard case .success(let data) = response.result else {return}
self?.repos = data.items
self?.nextPage = data.items.isEmpty ? nil : page + 1
}
.resume()
return .just((repos,nextPage))
}
extension GithubReactor.Action {
static func isUpdateQueryAction(_ action: GithubReactor.Action) -> Bool {
if case .updateQuery = action {
return true
} else {
return false
}
}
}
이렇게 Reactor부분에 대해서 간단하게 설명을 해보았습니다.
다음 게시글은 ViewController 부분 설명으로 돌아오겠습니다!