2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

初めに

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

Videotogif (4).gif

UIの設計

このように配置していきます。
スクリーンショット 2021-09-07 12.07.08.png
スクリーンショット 2021-09-07 12.06.24.png

DefaultであるMain.storyboardの名前をSearchViewController.swiftに変更してください。
もしstoryboardの変更でエラーが出たら、こちらを参照してください。

制約をつけていきます。
スクリーンショット 2021-09-07 12.07.08.png

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

SearchViewController.swift
import UIKit

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

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

スクリーンショット 2021-09-07 14.16.54.png

APIの取得

まず、APIの取得からやっていきたいと思います。
GitHubAPIを使います。

今回は,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

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

GitHubAPI

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

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を作ります。

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というライブラリを使いたいと思います。
JGProgressHUDの詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。

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というライブラリを使いたいと思います。
SDWebImageの詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。

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の拡張クラスを作っていきます。

SFSymbols.swift
import UIKit

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

終わりに

以上でこのようなアプリができました。

Videotogif (4).gif

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?