Skip to content

Commit

Permalink
Merge pull request #6 from shinya-todaka/feature/add-pagination
Browse files Browse the repository at this point in the history
ページネーション処理を追加
  • Loading branch information
shinya-todaka authored Feb 7, 2021
2 parents ae75a70 + 024eb2f commit a5e3464
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 39 deletions.
8 changes: 7 additions & 1 deletion iOSEngineerCodeCheck/Sources/API/GitHubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) ??
Expand Down
9 changes: 8 additions & 1 deletion iOSEngineerCodeCheck/Sources/Common/Models/Repository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
}

27 changes: 14 additions & 13 deletions iOSEngineerCodeCheck/Sources/Views/Search/Cell/RepositoryCell.xib
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="125" id="KGk-i7-Jjw" customClass="RepositoryCell" customModule="iOSEngineerCodeCheck" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="252" height="130"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="132" id="KGk-i7-Jjw" customClass="RepositoryCell" customModule="iOSEngineerCodeCheck" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="249" height="133"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="252" height="130"/>
<rect key="frame" x="0.0" y="0.0" width="249" height="133"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="suM-tV-HAI">
Expand All @@ -26,66 +26,67 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bCh-CS-e2g">
<rect key="frame" x="56" y="17" width="188" height="14.5"/>
<rect key="frame" x="56" y="17" width="185" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZgP-Q8-kAH">
<rect key="frame" x="16" y="48" width="228" height="14.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
<rect key="frame" x="16" y="48" width="225" height="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4bF-X4-p0h">
<rect key="frame" x="16" y="70.5" width="228" height="19.5"/>
<rect key="frame" x="16" y="73" width="225" height="19.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="B6L-e9-q3A">
<rect key="frame" x="18" y="98" width="16" height="16"/>
<rect key="frame" x="18" y="101" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="cwM-2T-cLc"/>
<constraint firstAttribute="height" constant="16" id="eDy-Vh-Dxd"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="POI-PY-sVe">
<rect key="frame" x="42" y="99" width="31" height="14.5"/>
<rect key="frame" x="42" y="102" width="31" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="i5t-jH-jad">
<rect key="frame" x="89" y="98" width="16" height="16"/>
<rect key="frame" x="89" y="101" width="16" height="16"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="0JX-Mo-k1c"/>
<constraint firstAttribute="height" constant="16" id="ida-1Q-YVg"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wJt-K2-C9x">
<rect key="frame" x="113" y="99" width="31" height="14.5"/>
<rect key="frame" x="113" y="102" width="31" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="B6L-e9-q3A" firstAttribute="top" secondItem="4bF-X4-p0h" secondAttribute="bottom" constant="8.5" id="0eB-0V-mbe"/>
<constraint firstItem="ZgP-Q8-kAH" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="2L0-nt-HFG"/>
<constraint firstItem="bCh-CS-e2g" firstAttribute="centerY" secondItem="suM-tV-HAI" secondAttribute="centerY" id="2qd-lP-Fby"/>
<constraint firstItem="suM-tV-HAI" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="976-fd-NFs"/>
<constraint firstItem="4bF-X4-p0h" firstAttribute="top" secondItem="ZgP-Q8-kAH" secondAttribute="bottom" constant="8" id="Ay2-zs-Xx6"/>
<constraint firstItem="suM-tV-HAI" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="CZH-c7-EvE"/>
<constraint firstItem="POI-PY-sVe" firstAttribute="leading" secondItem="B6L-e9-q3A" secondAttribute="trailing" constant="8" id="FmQ-F8-emf"/>
<constraint firstItem="4bF-X4-p0h" firstAttribute="leading" secondItem="B6L-e9-q3A" secondAttribute="trailing" constant="-18" id="Kgi-PD-ISx"/>
<constraint firstItem="i5t-jH-jad" firstAttribute="leading" secondItem="POI-PY-sVe" secondAttribute="trailing" constant="16" id="NRh-VW-Kp7"/>
<constraint firstAttribute="trailing" secondItem="bCh-CS-e2g" secondAttribute="trailing" constant="8" id="PMf-WP-xdU"/>
<constraint firstItem="POI-PY-sVe" firstAttribute="centerY" secondItem="B6L-e9-q3A" secondAttribute="centerY" id="QHU-qB-pG1"/>
<constraint firstItem="wJt-K2-C9x" firstAttribute="leading" secondItem="i5t-jH-jad" secondAttribute="trailing" constant="8" id="ReH-0n-4ga"/>
<constraint firstItem="4bF-X4-p0h" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="Tqc-B2-iFy"/>
<constraint firstItem="i5t-jH-jad" firstAttribute="centerY" secondItem="POI-PY-sVe" secondAttribute="centerY" id="UmK-v2-qoj"/>
<constraint firstAttribute="bottom" secondItem="B6L-e9-q3A" secondAttribute="bottom" constant="16" id="VPO-u9-rlj"/>
<constraint firstItem="B6L-e9-q3A" firstAttribute="top" secondItem="4bF-X4-p0h" secondAttribute="bottom" constant="8" id="VPs-bw-KUP"/>
<constraint firstItem="ZgP-Q8-kAH" firstAttribute="top" secondItem="suM-tV-HAI" secondAttribute="bottom" constant="8" id="Xgl-B3-oXI"/>
<constraint firstItem="wJt-K2-C9x" firstAttribute="centerY" secondItem="i5t-jH-jad" secondAttribute="centerY" id="etd-hc-MVg"/>
<constraint firstAttribute="trailing" secondItem="4bF-X4-p0h" secondAttribute="trailing" constant="8" id="oOr-Hb-DbP"/>
Expand All @@ -105,7 +106,7 @@
<outlet property="starCountLabel" destination="POI-PY-sVe" id="Vez-fU-Mwk"/>
<outlet property="starImageView" destination="B6L-e9-q3A" id="XRR-LI-vSb"/>
</connections>
<point key="canvasLocation" x="-208.69565217391306" y="97.098214285714278"/>
<point key="canvasLocation" x="-210.86956521739131" y="98.102678571428569"/>
</tableViewCell>
</objects>
<resources>
Expand Down
42 changes: 35 additions & 7 deletions iOSEngineerCodeCheck/Sources/Views/Search/SearchModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -37,5 +65,5 @@ class SearchModel: SearchModelProtocol {

protocol SearchModelDelegate: class {
func didChange(repositories: [Repository])
func didReceive(error: Error)
func didReceive(error: SessionTaskError)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -65,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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,55 @@
//

import Foundation
import APIKit

protocol SearchPresenter {
var view: SearchView? { get set }
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()
}
}

func didReceive(error: Error) {
func didReceive(error: SessionTaskError) {
DispatchQueue.main.async {
//TODO: change error message
self.view?.showAlert(with: error.localizedDescription)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -54,6 +54,7 @@ class SearchModelStub: SearchModelProtocol {
}

func cancel() {}
func fetchAdditionalRepositories() {}
}

extension SearchModelStub {
Expand All @@ -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()
Expand All @@ -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()
Expand Down

0 comments on commit a5e3464

Please sign in to comment.