GithubAPIでリポジトリをカスタムセルで、一覧表示させてみた

# 初めに
SwiftでGithubAPIを使ってリポジトリ一覧アプリを作ってみたいと思います。
初心者にもわかりやすく、AutoLayoutの設定、デザインパターン、コードの可読性もしっかり守っているので、APIの入門記事としてはぴったりかなと。
まず完成形はこちら！

![Videotogif (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/13c010c8-f194-b874-d852-200f65cbec98.gif)

## UIの設計

このように配置していきます。
![スクリーンショット 2021-09-07 12.07.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/1f241993-628f-979e-acf6-823ff7b72bb8.png)
![スクリーンショット 2021-09-07 12.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/155b4cdb-e0eb-f507-6b2c-1b2652e9621a.png)

DefaultであるMain.storyboardの名前をSearchViewController.swiftに変更してください。
もしstoryboardの変更でエラーが出たら、[こちら](https://ja.stackoverflow.com/questions/68823/%E8%B5%B7%E5%8B%95%E3%81%99%E3%82%8Bstoryboard%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%97%E3%82%88%E3%81%86%E3%81%A8%E3%81%99%E3%82%8B%E3%81%A8-thread-1-exception-could-not-find-a-storyboard-name)を参照してください。

制約をつけていきます。
![スクリーンショット 2021-09-07 12.07.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/d7e0cf17-c653-6e2f-efdd-59d30d846a17.png)

SearchViewController,RepositoryCellを作り、IBOutlet,IBAction接続します。

```swift:SearchViewController.swift
import UIKit

class SearchViewController: UITableViewController {
    
    @IBOutlet private weak var searchBar: UISearchBar!{
        didSet {
            searchBar.placeholder = "リポジトリを検索できるよ！"
            searchBar.delegate = self
        }
    }
}

```

```swift:RepositoryCell.swift
import UIKit

class RepositoryCell: UITableViewCell {

    @IBOutlet private weak var ownerImageView: UIImageView!{
        didSet {
            ownerImageView.layer.cornerRadius = 10
            ownerImageView.clipsToBounds = true
        }
    }
    
    @IBOutlet private weak var repositoryNameLabel: UILabel!{
        didSet{
            repositoryNameLabel.textColor = UIColor.link
        }
    }
    
    @IBOutlet private weak var starImageView: UIImageView!{
        didSet {
            starImageView.setImage(systemName: "star", tintColor: UIColor.systemGray)
        }
    }
    
    @IBOutlet private weak var ownerNameLabel: UILabel!
    @IBOutlet private weak var repositoryDescriptionLabel: UILabel!
    @IBOutlet private weak var starCountLabel: UILabel!
    @IBOutlet private weak var languageLabel: UILabel!
    
    static let cellIdentifier = String(describing: RepositoryCell.self)
}
```

## 全体設計
UIができた後に、今回のアプリの設計を行なっていく。

![スクリーンショット 2021-09-07 14.37.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/b936569c-cfb7-ba3b-1b3e-f78151795bfe.png)

![スクリーンショット 2021-09-07 14.16.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/b0b6094e-d7d0-7be0-cfcb-5829bced5626.png)

## APIの取得
まず、APIの取得からやっていきたいと思います。
[GitHubAPI](https://docs.github.com/en/rest/reference)を使います。

今回は,`https://api.github.com/search/repositories?q=\(text)`を使っていきます。

こちらでこのように(`https://api.github.com/search/repositories?q=Swift`)APIを叩くと、JSONデータを変換してくれます。


![スクリーンショット 2021-09-07 14.22.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/a938e0fe-22f8-f9f2-4507-055954620e4f.png)

これらのデータをうまく使い今回はアプリを作成していきます。

## GitHubAPI
今回のAPIにおいてのロジックを管理するGitHubAPIを書いていきます。

```swift:GitHubAPI.swift
import Foundation

class GitHubAPI {
    private static var task: URLSessionTask?
    
    enum FetchRepositoryError: Error {
        case wrong
        case network
        case parse
    }
    
    static func fetchRepository(text: String, completionHandler: @escaping (Result<[Repository], FetchRepositoryError>) -> Void) {
        if !text.isEmpty {
            
            let urlString = "https://api.github.com/search/repositories?q=\(text)".addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
            guard let url = URL(string: urlString) else {
                completionHandler(.failure(FetchRepositoryError.wrong))
                return
            }
            
            let task = URLSession.shared.dataTask(with: url) { (data, res, err) in
                if err != nil {
                    completionHandler(.failure(FetchRepositoryError.network))
                    return
                }
                
                guard let safeData = data else {return}
                
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                do {
                    let decodedData = try decoder.decode(Repositories.self, from: safeData)
                    completionHandler(.success(decodedData.items))

                } catch  {
                    completionHandler(.failure(FetchRepositoryError.parse))
                }
            }
            task.resume()
        }
    }
    
    static func taskCancel() {
        task?.cancel()
    }
}
```
## Repository
レスポンスしたデータをデコードするためRepositoryを作ります。

```swift:Repository.swift
import Foundation
import UIKit

struct Repositories: Codable {
    let items: [Repository]
}

struct Repository: Codable {
    let name: String
    let fullName: String
    let language: String?
    let stargazersCount: Int
    let watchersCount: Int
    let forksCount: Int
    let openIssuesCount: Int
    let description: String?
    
    let owner: Owner

    var avatarImageUrl: URL? {
        return URL(string: owner.avatarUrl)
    }
}

struct Owner: Codable {
    let avatarUrl: String
    let login: String
}
```

## SearchViewController
取得したデータをViewに反映させる、またTableView,SearchBarの操作のためにSearchViewControllerを作っていきます。
その前にロード処理のために便利な[JGProgressHUD](https://github.com/JonasGessner/JGProgressHUD)というライブラリを使いたいと思います。
JGProgressHUDの詳しい説明、導入の仕方などは[これら](http://cocoadocs.org/docsets/JGProgressHUD/1.4/)の記事を見るとわかると思います。

```swift:SearchViewController.swift
import UIKit
import JGProgressHUD

class SearchViewController: UITableViewController {
    
    @IBOutlet private weak var searchBar: UISearchBar!{
        didSet {
            searchBar.placeholder = "リポジトリを検索できるよ！"
            searchBar.delegate = self
        }
    }
    
    private var repositories: [Repository] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let nib = UINib(nibName: RepositoryCell.cellIdentifier, bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: RepositoryCell.cellIdentifier)
        
        tableView.rowHeight = UITableView.automaticDimension
    }
    
    private func showAlert(title: String, message: String = "") -> UIAlertController {
        let alert: UIAlertController = UIAlertController(title: title, message : message, preferredStyle: UIAlertController.Style.alert)
        let defaultAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.default)
        alert.addAction(defaultAction)
        return alert
    }
    
    private func wrongError() -> UIAlertController {
        return showAlert(title: "不正なワードの入力", message: "検索ワードの確認を行ってください")
    }
    
    private func networkError() -> UIAlertController {
        return showAlert(title: "インターネットの非接続", message: "接続状況の確認を行ってください")
    }
    
    private func parseError() -> UIAlertController {
        return showAlert(title: "データの解析に失敗しました")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return repositories.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: RepositoryCell.cellIdentifier, for: indexPath) as! RepositoryCell
        
        let repository = repositories[indexPath.row]
        cell.configure(repository: repository)
        return cell
    }
}

//MARK: - UISearchBarDelegate
extension SearchViewController:UISearchBarDelegate{
    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
        return true
    }
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        GitHubAPI.taskCancel()
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard !(searchBar.text?.isEmpty ?? true) else { return }
        searchBar.resignFirstResponder()
        
        let progressHUD = JGProgressHUD()
        progressHUD.show(in: self.view)
        
        if let word = searchBar.text{
            GitHubAPI.fetchRepository(text: word) { result in
                DispatchQueue.main.async {
                    progressHUD.dismiss()
                }
                
                switch result {
                case .success(let items):
                    self.repositories = items
                    DispatchQueue.main.async {
                        self.tableView.reloadData()
                    }
                case .failure(let error):
                    DispatchQueue.main.async {
                        switch error {
                        case .wrong :
                            let alert = self.wrongError()
                            self.present(alert, animated: true, completion: nil)
                            return
                        case .network:
                            let alert = self.networkError()
                            self.present(alert, animated: true, completion: nil)
                            return
                        case .parse:
                            let alert = self.parseError()
                            self.present(alert, animated: true, completion: nil)
                            return
                        }
                    }
                }
            }
        }
        return
    }
}
```

## RepositoryCell
tableViewCellの配置を行うRepositoryCellを作っていきます。
その前に画像のキャッシュのために便利な[SDWebImage](https://github.com/SDWebImage/SDWebImage)というライブラリを使いたいと思います。
SDWebImageの詳しい説明、導入の仕方などは[これら](https://qiita.com/hcrane/items/422811dfc18ae919f8a4)の記事を見るとわかると思います。

```swift:RepositoryCell.swift
import UIKit
import SDWebImage

class RepositoryCell: UITableViewCell {

    @IBOutlet private weak var ownerImageView: UIImageView!{
        didSet {
            ownerImageView.layer.cornerRadius = 10
            ownerImageView.clipsToBounds = true
        }
    }
    
    @IBOutlet private weak var repositoryNameLabel: UILabel!{
        didSet{
            repositoryNameLabel.textColor = UIColor.link
        }
    }
    
    @IBOutlet private weak var starImageView: UIImageView!{
        didSet {
            starImageView.setImage(systemName: "star", tintColor: UIColor.systemGray)
        }
    }
    
    @IBOutlet private weak var ownerNameLabel: UILabel!
    @IBOutlet private weak var repositoryDescriptionLabel: UILabel!
    @IBOutlet private weak var starCountLabel: UILabel!
    @IBOutlet private weak var languageLabel: UILabel!
    
    static let cellIdentifier = String(describing: RepositoryCell.self)

    func configure(repository: Repository) {
        ownerNameLabel.text = repository.owner.login
        repositoryNameLabel.text = repository.fullName

        if let url = repository.avatarImageUrl {
            ownerImageView.sd_setImage(with: url, completed: nil)
        } else {
            ownerImageView.image = nil
        }
        
        repositoryDescriptionLabel.text = repository.description ?? ""
        starCountLabel.text = "\(repository.stargazersCount)"
        languageLabel.text = repository.language
        accessoryType = .disclosureIndicator
    }
}
```
##　SFSymbols
UIImageViewの拡張クラスを作っていきます。

```swift:SFSymbols.swift
import UIKit

extension UIImageView {
    func setImage(systemName: String, tintColor: UIColor) {
        self.image = UIImage(systemName: systemName)
        self.tintColor = tintColor
    }
}
```

# 終わりに
以上でこのようなアプリができました。

![Videotogif (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/13c010c8-f194-b874-d852-200f65cbec98.gif)