From 9487203c00013078da8d65968bdbd1b682a78e35 Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 04:09:39 +0900 Subject: [PATCH 1/7] =?UTF-8?q?Repository=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift b/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift index fd54ddd..35088e9 100644 --- a/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift +++ b/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift @@ -18,7 +18,7 @@ struct Repository: Decodable { let watchersCount: Int let forksCount: Int let openIssuesCount: Int - let description: String + let description: String? let owner: Owner From dc506aef1d41287eef6e98c7de0b17dff9a211a3 Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 06:12:06 +0900 Subject: [PATCH 2/7] =?UTF-8?q?Repository=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=82=92Equatable=E3=81=AB=E6=BA=96=E6=8B=A0=E3=81=95=E3=81=9B?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Common/Models/Repository.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift b/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift index 35088e9..7fdf149 100644 --- a/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift +++ b/iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift @@ -36,8 +36,15 @@ struct Repository: Decodable { } } +extension Repository: Equatable { + static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } +} + extension Repository { var languageColor: UIColor? { return language.flatMap(Language.init(rawValue:))?.color } } + From e0ddf0bda25eb19bc527f02a23848ce7beeb63ad Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 06:12:37 +0900 Subject: [PATCH 3/7] =?UTF-8?q?paging=E3=81=AB=E5=BF=85=E8=A6=81=E3=81=AAp?= =?UTF-8?q?arameter=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOSEngineerCodeCheck/Sources/API/GitHubAPI.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/iOSEngineerCodeCheck/Sources/API/GitHubAPI.swift b/iOSEngineerCodeCheck/Sources/API/GitHubAPI.swift index d8a57e2..4fae1e4 100644 --- a/iOSEngineerCodeCheck/Sources/API/GitHubAPI.swift +++ b/iOSEngineerCodeCheck/Sources/API/GitHubAPI.swift @@ -40,10 +40,16 @@ struct GitHubAPI { let path: String = "/search/repositories" var parameters: Any? { - return ["q": query] + return [ + "q": query, + "page": page, + "per_page": perPage + ] } let query: String + let page: Int + let perPage: Int = 20 func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { return try (object as? Response) ?? From 3d48920341697d07e745fa3aa2c73c64f95319c2 Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 06:32:00 +0900 Subject: [PATCH 4/7] =?UTF-8?q?paging=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Views/Search/SearchModel.swift | 40 ++++++++++++++++--- .../Views/Search/SearchViewController.swift | 5 +++ .../Views/Search/SearchViewPresenter.swift | 20 +++++++--- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift b/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift index 695883c..1270840 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift +++ b/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift @@ -10,20 +10,48 @@ import APIKit protocol SearchModelProtocol { var delegate: SearchModelDelegate? { get set } + var repositories: [Repository] { get } var sessionTask: SessionTask? { get } - func fetchItems(text: String) + func fetchAdditionalRepositories() + func fetchInitialRepositories(query: String) func cancel() } class SearchModel: SearchModelProtocol { weak var delegate: SearchModelDelegate? - private(set) var sessionTask: SessionTask? + internal var sessionTask: SessionTask? + private var currentPage: Int = 1 + private var currentQuery: String = "" - func fetchItems(text: String) { - self.sessionTask = GitHubAPI.call(request: GitHubAPI.SearchRepositories(query: text)) { [weak self] (result) in + private(set) var repositories: [Repository] = [] { + didSet { + if oldValue != repositories { + self.delegate?.didChange(repositories: repositories) + } + } + } + + func fetchAdditionalRepositories() { + fetchRepositories(query: self.currentQuery, page: currentPage + 1) { [weak self] (response) in + self?.repositories += response.repositories + } + } + + func fetchInitialRepositories(query: String) { + self.currentQuery = query + self.currentPage = 1 + fetchRepositories(query: query, page: self.currentPage) { [weak self] response in + self?.repositories = response.repositories + } + } + + private func fetchRepositories(query: String, page: Int, completion: @escaping (SearchResponse) -> Void) { + let request = GitHubAPI.SearchRepositories(query: query, page: page) + self.sessionTask = GitHubAPI.call(request: request) { [weak self] (result) in switch result { - case let .success(response): - self?.delegate?.didChange(repositories: response.repositories) + case let.success(response): + self?.currentPage += 1 + completion(response) case let .failure(error): self?.delegate?.didReceive(error: error) } diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift index 7867645..e19b124 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift +++ b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift @@ -57,6 +57,11 @@ class SearchViewController: UITableViewController, StoryboardInstantiatable, Inj let detailVC = DetailViewController.instantiate(with: item) navigationController?.pushViewController(detailVC, animated: true) } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + let maxScrollDistance = max(0, scrollView.contentSize.height - scrollView.bounds.size.height) + presenter.setIsReachedBottom(maxScrollDistance <= scrollView.contentOffset.y) + } } extension SearchViewController: SearchView { diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift index 3cf4f66..08bd159 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift +++ b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift @@ -13,32 +13,42 @@ protocol SearchPresenter { var repositories: [Repository] { get } func searchButtonTapped(with text: String) func textDidChange() + func setIsReachedBottom(_ isReachedBottom: Bool) } class SearchViewPresenter: SearchPresenter { weak var view: SearchView? - private(set) var repositories: [Repository] = [] + var repositories: [Repository] { + return model.repositories + } private var model: SearchModelProtocol + private var isReachedBottom: Bool = false init(model: SearchModelProtocol) { self.model = model self.model.delegate = self } - func searchButtonTapped(with text: String) { - guard text.count != 0 else { return } - model.fetchItems(text: text) + func searchButtonTapped(with query: String) { + guard query.count != 0 else { return } + model.fetchInitialRepositories(query: query) } func textDidChange() { model.cancel() } + + func setIsReachedBottom(_ isReachedBottom: Bool) { + if !self.isReachedBottom && isReachedBottom { + model.fetchAdditionalRepositories() + } + self.isReachedBottom = isReachedBottom + } } extension SearchViewPresenter: SearchModelDelegate { func didChange(repositories: [Repository]) { - self.repositories = repositories DispatchQueue.main.async { self.view?.updateTableView() } From d32b9a37a39993bcee48fe14ea08dea5f90541b2 Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 08:47:23 +0900 Subject: [PATCH 5/7] =?UTF-8?q?SearchViewPresenter=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Search/SearchViewPresenterTest.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/iOSEngineerCodeCheckTests/Views/Search/SearchViewPresenterTest.swift b/iOSEngineerCodeCheckTests/Views/Search/SearchViewPresenterTest.swift index 03ca639..26ebda3 100644 --- a/iOSEngineerCodeCheckTests/Views/Search/SearchViewPresenterTest.swift +++ b/iOSEngineerCodeCheckTests/Views/Search/SearchViewPresenterTest.swift @@ -29,20 +29,20 @@ class SearchViewPresenterSpy: SearchView { } class SearchModelStub: SearchModelProtocol { - + var repositories: [Repository] = [] var delegate: SearchModelDelegate? var sessionTask: SessionTask? - private var fetchRepositoriesResponse: [String: Result<[Repository], Error>] = [:] + private var fetchInitialRepositoriesResponse: [String: Result<[Repository], Error>] = [:] - func addFetchRepositoriesResponse(text: String, result: Result<[Repository], Error>) { - self.fetchRepositoriesResponse[text] = result + func addFetchInitialRepositoriesResponse(query: String, result: Result<[Repository], Error>) { + self.fetchInitialRepositoriesResponse[query] = result } - func fetchItems(text: String) { - guard let response = fetchRepositoriesResponse[text] else { - fatalError("fetchItemsResponse not found when text is \(text)") + func fetchInitialRepositories(query: String) { + guard let response = fetchInitialRepositoriesResponse[query] else { + fatalError("fetchItemsResponse not found when text is \(query)") } switch response { @@ -54,6 +54,7 @@ class SearchModelStub: SearchModelProtocol { } func cancel() {} + func fetchAdditionalRepositories() {} } extension SearchModelStub { @@ -75,7 +76,7 @@ class SearchViewPresenterTest: XCTestCase { let response: Result<[Repository], Error> = .success([.init(id: 0, name: "swift", fullName: "swift/apple", language: "c", stargazersCount: 12345, watchersCount: 12345, forksCount: 12345, openIssuesCount: 12345, description: "oss", owner: .init(avatarUrl: "https:/hogehoge", login: "apple"))]) - stub.addFetchRepositoriesResponse(text: textToSearch, result: response) + stub.addFetchInitialRepositoriesResponse(query: textToSearch, result: response) let exp = XCTestExpectation(description: "searchButtonTappedが呼ばれた後に実行されるupdateTableViewを待つ") spy._updateTableView = { exp.fulfill() @@ -95,7 +96,7 @@ class SearchViewPresenterTest: XCTestCase { let textToSearch = "swift" let response: Result<[Repository], Error> = .failure(SearchModelStub.APIErrpr.unknownError) - stub.addFetchRepositoriesResponse(text: textToSearch, result: response) + stub.addFetchInitialRepositoriesResponse(query: textToSearch, result: response) let exp = XCTestExpectation(description: "searchButtonTappedが呼ばれた後に実行されるshowAlertを待つ") spy._showAlert = { text in exp.fulfill() From 9e9fcfd3881371562551848723f193c1485bcc6c Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 09:08:29 +0900 Subject: [PATCH 6/7] =?UTF-8?q?SearchViewController=E3=81=AEcell=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Search/Cell/RepositoryCell.xib | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/Cell/RepositoryCell.xib b/iOSEngineerCodeCheck/Sources/Views/Search/Cell/RepositoryCell.xib index be607c8..849d69e 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/Cell/RepositoryCell.xib +++ b/iOSEngineerCodeCheck/Sources/Views/Search/Cell/RepositoryCell.xib @@ -11,11 +11,11 @@ - - + + - + @@ -26,38 +26,38 @@ - + - + @@ -65,19 +65,21 @@ + + @@ -85,7 +87,6 @@ - @@ -105,7 +106,7 @@ - + From 024eb2fb5f97d81a02c6813e021519f80f75a0e8 Mon Sep 17 00:00:00 2001 From: Shinya Todaka Date: Sun, 7 Feb 2021 09:38:19 +0900 Subject: [PATCH 7/7] =?UTF-8?q?error=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=82=92=E3=82=A2=E3=83=A9=E3=83=BC=E3=83=88=E3=81=A7?= =?UTF-8?q?=E4=BC=9D=E3=81=88=E3=82=8B=E5=87=A6=E7=90=86=E3=82=92=E6=9B=B8?= =?UTF-8?q?=E3=81=84=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Views/Search/SearchModel.swift | 2 +- .../Sources/Views/Search/SearchViewController.swift | 8 ++++++-- .../Sources/Views/Search/SearchViewPresenter.swift | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift b/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift index 1270840..d540510 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift +++ b/iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift @@ -65,5 +65,5 @@ class SearchModel: SearchModelProtocol { protocol SearchModelDelegate: class { func didChange(repositories: [Repository]) - func didReceive(error: Error) + func didReceive(error: SessionTaskError) } diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift index e19b124..e908e65 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift +++ b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewController.swift @@ -70,8 +70,12 @@ extension SearchViewController: SearchView { } func showAlert(with errorMessage: String) { - //TODO: show alert - print(errorMessage) + let alert = UIAlertController(title: errorMessage, + message: errorMessage, + preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default){ _ in } + alert.addAction(okAction) + present(alert, animated: false, completion: nil) } } diff --git a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift index 08bd159..2e01724 100644 --- a/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift +++ b/iOSEngineerCodeCheck/Sources/Views/Search/SearchViewPresenter.swift @@ -7,6 +7,7 @@ // import Foundation +import APIKit protocol SearchPresenter { var view: SearchView? { get set } @@ -54,7 +55,7 @@ extension SearchViewPresenter: SearchModelDelegate { } } - func didReceive(error: Error) { + func didReceive(error: SessionTaskError) { DispatchQueue.main.async { //TODO: change error message self.view?.showAlert(with: error.localizedDescription)