初めに
SwiftでiTunesSearchAPIを使って楽曲一覧アプリを作ってみたいと思います。
初心者にもわかりやすく、AutoLayoutの設定、デザインパターン、コードの可読性もしっかり守っているので、APIの入門記事としてはぴったりかなと。
まず完成形はこちら!
UIの設計
このように配置していきます。
制約をつけていきます。
SearchViewController,ListTableViewCellを作り、IBOutlet,IBAction接続します。
import UIKit
class SearchViewController: UIViewController{
@IBOutlet weak var searchBar: UISearchBar!{
didSet{
searchBar.delegate = self
}
}
@IBOutlet weak var tableView: UITableView!{
didSet{
tableView.delegate = self
tableView.dataSource = self
}
}
}
import UIKit
class ListTableViewCell: UITableViewCell {
@IBOutlet weak var artistImageView: UIImageView!
@IBOutlet private weak var songNameLabel: UILabel!
@IBOutlet private weak var artistNameLabel: UILabel!
}
全体設計
APIの取得
まず、APIの取得からやっていきたいと思います。
iTunesAPIを使います。
今回は,https://itunes.apple.com/search?term=\(text)&entity=song&country=jp
を使っていきます。
こちらでこのように(https://itunes.apple.com/search?term=西野カナ&entity=song&country=jp
)APIを叩くと、JSONデータを変換してくれます。
これらのデータをうまく使い今回はアプリを作成していきます。
iTunesAPI
今回のAPIにおいてのロジックを管理するiTunesAPIを書いていきます。
import Foundation
import Reachability
class iTunesAPI {
private static var task: URLSessionTask?
enum SearchRepositoryError: Error {
case wrong
case network
case parse
}
static func searchRepository(text: String, completionHandler: @escaping (Result<[Song], SearchRepositoryError>) -> Void) {
if !text.isEmpty {
let reachability = try! Reachability()
if reachability.connection == .unavailable {
completionHandler(.failure(SearchRepositoryError.network))
return
}
let urlString = "https://itunes.apple.com/search?term=\(text)&entity=song&country=jp".addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
guard let url = URL(string: urlString) else {
completionHandler(.failure(SearchRepositoryError.wrong))
return
}
let task = URLSession.shared.dataTask(with: url) { (data, res, err) in
if err != nil {
completionHandler(.failure(SearchRepositoryError.network))
return
}
guard let date = data else {return}
if let result = try? jsonStrategyDecoder.decode(Songs.self, from: date) {
completionHandler(.success(result.results))
} else {
completionHandler(.failure(SearchRepositoryError.parse))
}
}
task.resume()
}
}
static private var jsonStrategyDecoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}
static func taskCancel() {
task?.cancel()
}
}
Songs
レスポンスしたデータをデコードするためSongsを作ります。
import Foundation
struct Songs :Codable{
let results:[Song]
}
struct Song :Codable{
//アーティスト名
let artistName :String?
//楽曲名
let trackCensoredName:String?
//音源URL
let previewUrl:String?
//ジャケ写URL
let artworkUrl100:String
var artworkImageUrl100:URL?{
return URL(string: artworkUrl100)
}
}
SearchViewController
取得したデータをViewに反映させる、またTableView,SearchBarの操作のためにSearchViewControllerを作っていきます。
import UIKit
import JGProgressHUD
import AVFoundation
class SearchViewController: UIViewController{
@IBOutlet weak var searchBar: UISearchBar!{
didSet{
searchBar.delegate = self
}
}
@IBOutlet weak var tableView: UITableView!{
didSet{
tableView.delegate = self
tableView.dataSource = self
}
}
private var songs: [Song] = []
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: ListTableViewCell.cellIdentifier, bundle: nil)
tableView.register(nib, forCellReuseIdentifier: ListTableViewCell.cellIdentifier)
tableView.rowHeight = UITableView.automaticDimension
}
}
extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return songs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ListTableViewCell.cellIdentifier, for: indexPath) as! ListTableViewCell
let song = songs[indexPath.row]
cell.setup(song:song)
return cell
}
}
extension SearchViewController: UISearchBarDelegate{
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
return true
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
iTunesAPI.taskCancel()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard !(searchBar.text?.isEmpty ?? true) else { return }
searchBar.resignFirstResponder()
let word = searchBar.text!
let progressHUD = JGProgressHUD()
progressHUD.show(in: self.view)
iTunesAPI.searchRepository(text: word) { result in
DispatchQueue.main.async {
progressHUD.dismiss()
}
switch result {
case .success(let results):
self.songs = results
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure(let error):
DispatchQueue.main.async {
switch error {
case .wrong :
let alert = ErrorAlert.wrongWordError()
self.present(alert, animated: true, completion: nil)
return
case .network:
let alert = ErrorAlert.networkError()
self.present(alert, animated: true, completion: nil)
return
case .parse:
let alert = ErrorAlert.parseError()
self.present(alert, animated: true, completion: nil)
return
}
}
}
}
return
}
}
ListTableViewCell
tableViewCellの配置を行うListTableViewCellを作っていきます。
その前に画像のキャッシュのために便利なSDWebImageというライブラリを使いたいと思います。
SDWebImageの詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。
import UIKit
import SDWebImage
class ListTableViewCell: UITableViewCell {
@IBOutlet private weak var artistImageView: UIImageView!
@IBOutlet private weak var songNameLabel: UILabel!
@IBOutlet private weak var artistNameLabel: UILabel!
static let cellIdentifier = String(describing: ListTableViewCell.self)
func setup(song: Song) {
songNameLabel.text = song.trackCensoredName ?? ""
artistNameLabel.text = song.artistName ?? ""
if let url = song.artworkImageUrl100 {
artistImageView.sd_setImage(with: url, completed: nil)
} else {
artistImageView.image = nil
}
}
}
ErrorAlert
最後に、様々なエラー処理のためのクラスを作っていきたいと思います。
import UIKit
class ErrorAlert {
private static func errorAlert(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
}
static func wrongWordError() -> UIAlertController {
return errorAlert(title: "不正なワードが入力されました", message: "検索ワードを確認してください")
}
static func networkError() -> UIAlertController {
return errorAlert(title: "インターネットに接続されていません", message: "接続状況を確認してください")
}
static func parseError() -> UIAlertController {
return errorAlert(title: "データの表示に失敗しました")
}
}