初めに
SwiftでGithubAPIを使ってリポジトリ一覧アプリを作ってみたいと思います。
初心者にもわかりやすく、AutoLayoutの設定、デザインパターン、コードの可読性もしっかり守っているので、APIの入門記事としてはぴったりかなと。
まず完成形はこちら!
UIの設計
DefaultであるMain.storyboardの名前をSearchViewController.swiftに変更してください。
もしstoryboardの変更でエラーが出たら、こちらを参照してください。
SearchViewController,RepositoryCellを作り、IBOutlet,IBAction接続します。
import UIKit
class SearchViewController: UITableViewController {
@IBOutlet private weak var searchBar: UISearchBar!{
didSet {
searchBar.placeholder = "リポジトリを検索できるよ!"
searchBar.delegate = self
}
}
}
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ができた後に、今回のアプリの設計を行なっていく。
APIの取得
まず、APIの取得からやっていきたいと思います。
GitHubAPIを使います。
今回は,https://api.github.com/search/repositories?q=\(text)
を使っていきます。
こちらでこのように(https://api.github.com/search/repositories?q=Swift
)APIを叩くと、JSONデータを変換してくれます。
これらのデータをうまく使い今回はアプリを作成していきます。
GitHubAPI
今回のAPIにおいてのロジックを管理するGitHubAPIを書いていきます。
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を作ります。
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の詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。
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の詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。
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の拡張クラスを作っていきます。
import UIKit
extension UIImageView {
func setImage(systemName: String, tintColor: UIColor) {
self.image = UIImage(systemName: systemName)
self.tintColor = tintColor
}
}
終わりに
以上でこのようなアプリができました。