iPhone
 Computer >> コンピューター >  >> スマートフォン >> iPhone

iOSアプリでUISearchControllerを使う方法!TMDB APIと連携した映画検索アプリの作り方

こんにちは!この記事では、iOSアプリにおけるUISearchControllerの使い方を、実際に映画検索アプリを作りながら解説していきます。

今回作るもの

今回は、TMDB APIを使って映画情報を取得し、ユーザーが入力した検索キーワードに基づいてUICollectionViewで結果を表示する映画検索アプリを構築します。

プロジェクトのセットアップ

Xcodeを開き、新しい空のiOS Appプロジェクトを作成しましょう。その際、SwiftUIではなくUIKitを選択してください。

このアプリではMVCパターンを採用するため、以下のようなグループとSwiftファイルを作成してプロジェクトを整理します。

iOSアプリでUISearchControllerを使う方法!TMDB APIと連携した映画検索アプリの作り方

次にXcodeプロジェクトを閉じ、ターミナルを開いてプロジェクトのディレクトリへ移動します。ここでは、映画のポスター画像を非同期でダウンロード・キャッシュするために、SD WebImageというCocoaPodsライブラリを追加します。

ターミナルで以下のコマンドを実行してください。

pod init

コマンド実行後、ディレクトリ内の一覧を表示すると、新しくPodfileが作成されているのが確認できます。任意のテキストエディタ(ここではVimを使用)でファイルを開き、下の画像のように編集して保存・閉じましょう。

iOSアプリでUISearchControllerを使う方法!TMDB APIと連携した映画検索アプリの作り方

SD WebImageの指定が完了したら、以下のコマンドで依存関係をインストールします。

pod install

これでSD WebImageの追加は完了です。続いて、以下のコマンドでXcodeからプロジェクトを開きます。

open PROJECT_NAME.xcworkspace

Xcodeが起動したら、Command+Bでプロジェクトをビルドしておきましょう。

UIKitとプログラマティックUIでインターフェースを設計する方法

このアプリには3つのUI要素が必要です。

  • 検索バーを配置するためのナビゲーションバー
  • 実際の検索処理を担うUISearchController
  • 検索結果を表示するUICollectionView

まずSceneDelegate.swiftファイルを開き、セッション接続メソッド内に以下のコードを追加します。

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: scene)
        window?.rootViewController = UINavigationController(rootViewController: HomeVC())
        window?.makeKeyAndVisible()
    }

プログラマティックUIを採用しているため、最初にルートビューコントローラー(アプリ起動時に最初に表示される画面)を指定する必要があります。

このアプリではビューコントローラーは1つだけなので、それをUINavigationControllerでラップします。こうすることでナビゲーションバーが提供され、そこにUISearchControllerを配置できるようになります。

次にHomeVC.swiftファイルを開き、以下のプロパティを追加します。

 private var SearchBar: UISearchController = {
        let sb = UISearchController()
        sb.searchBar.placeholder = "Enter the movie name"
        sb.searchBar.searchBarStyle = .minimal
        return sb
    }()
    
    private var MovieCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.itemSize = CGSize(width: UIScreen.main.bounds.width/3 - 10, height: 200)
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.register(MovieCell.self, forCellWithReuseIdentifier: MovieCell.ID)
        return cv
    }()

まずUISearchControllerを作成し、プレースホルダーのテキストやスタイルなどのプロパティを設定しています。

続いてUICollectionViewを作成し、コレクションビューが使用するレイアウトの種類を指定します。ここではUICollectionViewFlowLayoutを使用し、スクロール方向やアイテムサイズなどのプロパティ、さらに後ほど作成するカスタムセルクラスも登録しています。

HomeVCクラス内に新しい関数を作成し、UICollectionViewに対するAuto Layout制約をプログラムで設定するためのコードを追加します。

    //MARK: - HELPERS
    func configureUI(){
        MovieCollectionView.translatesAutoresizingMaskIntoConstraints = false
        MovieCollectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        MovieCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        MovieCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        MovieCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    }

まず、自動リサイジングマスクを制約に変換しないよう設定します。その後、コレクションビューをビューコントローラーの4辺すべてに固定(ピン留め)しています。

次に、viewDidLoad()メソッド内に以下のコードを追加します。

  override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.title  = "Movie Search"
        view.backgroundColor = .systemBackground
        SearchBar.searchResultsUpdater = self
        navigationItem.searchController = SearchBar
        view.addSubview(MovieCollectionView)
        MovieCollectionView.delegate = self
        MovieCollectionView.dataSource = self
        configureUI()
    }

ここでは、まずビューコントローラーのタイトルを指定し、背景色として.systemBackgroundを設定しています。この色は端末の外観モードに応じて変化し、ライトモードなら白背景、ダークモードなら暗い背景が表示されます。

その後、現在のビューコントローラーを検索結果アップデーターとして設定し、SearchControllerをナビゲーションバーに追加、さらにUICollectionViewをビューコントローラーに追加してデリゲートとデータソースを設定します。最後にAuto Layoutでコレクションビューを固定します。

HomeVCのextensionを作成し、UISearchResultsUpdatingプロトコルとその必須メソッドであるupdateSearchResultsを実装しましょう。

extension HomeVC: UISearchResultsUpdating{
    
    func updateSearchResults(for searchController: UISearchController) {
        guard let query = searchController.searchBar.text else{return}
        }
    }
}

updateSearchResults()メソッドは、検索バーに入力されたテキストが変更されるたび、またはユーザーがキーボードの検索ボタンをタップしたときに呼び出されます。

次に、カスタムUICollectionViewセルを作成します。MovieCell.swiftファイルに以下のコードを追加してください。

import Foundation
import UIKit
import SDWebImage

class MovieCell: UICollectionViewCell{
    
    static let ID = "MovieCell"
    private var MoviePosterImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(MoviePosterImageView)
        configureUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

extension MovieCell{
    func configureUI(){
        MoviePosterImageView.translatesAutoresizingMaskIntoConstraints = false
        MoviePosterImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        MoviePosterImageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        MoviePosterImageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        MoviePosterImageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    }
    func updateCell(posterURL: String?){
        if let posterURL = posterURL {
            guard let CompleteURL = URL(string: "https://image.tmdb.org/t/p/w500/\(posterURL)") else {return}
            self.MoviePosterImageView.sd_setImage(with: CompleteURL)
        }
       
    }
}

ここでは、UICollectionViewCellをサブクラス化してカスタムコレクションビューセルを作成し、init()関数を実装しています。

映画のポスター画像を表示するためのUIImageViewを作成し、Auto Layout制約を設定します。さらに、ポスター画像のURL文字列を引数として受け取り、UIスレッド(メインスレッド)に影響を与えることなく画像を非同期でダウンロードする独自の関数も作成しています。この処理には、先ほど追加したSD WebImageライブラリを利用しています。

APIのセットアップ方法

先へ進む前に、TMDB APIのAPIキーを取得しておく必要があります。アカウント作成は無料です。今回はAPIの「Movie Search」エンドポイントを使用します。このエンドポイントは、APIキーと映画名をパラメータとして受け取ります。

https://api.themoviedb.org/3/search/movie?api_key=API_KEY_HERE&query=batman

Postmanなどでリクエストを実行すれば、APIレスポンスの中身を確認できます。

iOSアプリでUISearchControllerを使う方法!TMDB APIと連携した映画検索アプリの作り方

APIレスポンス用のモデルを作成する方法

APIからはJSON形式のレスポンスが返ってきます。これをSwiftで扱うには、Codableプロトコルに準拠したモデル構造体を作成してデコードする必要があります。

JSONからSwiftモデルへの変換は、「JSON to Swift」系のWebツールを使えば簡単に生成できます。以下がAPIレスポンス用のモデルコードなので、そのままModel.swiftファイルにコピー&ペーストしてください。

import Foundation

struct TrendingTitleResponse: Codable {
    let results: [Title]
}

struct Title: Codable {
    let id: Int
    let media_type: String?
    let original_name: String?
    let original_title: String?
    let poster_path: String?
    let overview: String?
    let vote_count: Int
    let release_date: String?
    let vote_average: Double
}

struct YoutubeSearchResponse: Codable {
    let items: [VideoElement]
}

struct VideoElement: Codable {
    let id: IdVideoElement
}

struct IdVideoElement: Codable {
    let kind: String
    let videoId: String
}

SwiftでHTTPリクエストを実行する方法

次に、APIのJSONレスポンスを取得するためのHTTP GETリクエストを実行するSwiftコードを書いていきます。

SwiftにはURLSessionクラスが標準で用意されており、AFNetworkingやAlamofireといったサードパーティ製ライブラリを使わなくても、ネットワーク処理を簡単に記述できます。

APIService.swiftを開き、以下のコードを追加しましょう。

import Foundation

class APIService{
    static var shared = APIService()
    let session = URLSession(configuration: .default)
    
    func getMovies(for Query: String,completion:@escaping([Title]?,Error?)->Void){
        guard let FormatedQuery = Query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)  else{return}
        
        guard let  SEARCH_URL = URL(string: "https://api.themoviedb.org/3/search/movie?api_key=API_KEY_HERE&query=\(FormatedQuery)") else {print("INVALID")
            return}
        
        
        
        let task = session.dataTask(with: SEARCH_URL) { data, response, error in
            if let error = error {
                print(error.localizedDescription)
                completion(nil,error)
            }
            if let data = data {
                do{
                    let decodedData = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
                    completion(decodedData.results,nil)
                }
                catch{
                    print(error)
                }
            }
        }
        task.resume()
    }
}

ここではシングルトンパターンを採用したAPIServiceクラスを作成し、クラスの静的メンバーとしてインスタンスを保持しています。その後、デフォルト設定でネットワークタスク用のセッションを作成し、独自のメソッドgetMovies()を定義しました。

ネットワークタスクの作成には、URLSessionクラスのdataTask()メソッドを使用します。このメソッドはURLを引数に取り、APIから返却されたデータ、発生した場合はエラー情報、そしてステータスコードなどのHTTPレスポンス情報を含むコンプリーションハンドラを提供します。

エラーが発生した場合はエラーデータとともに関数を抜け、問題なければSwiftモデルに基づいてJSONデータをデコードし、デコード済みのデータとともに関数を抜けるようにしています。

検索結果をUICollectionViewに表示する方法

HomeVC.swift内に、APIから返却された各映画の情報を保持するTitleオブジェクトの配列をプライベートプロパティとして作成します。

private var Movies = [Title]()

続いてHomeVCクラスのextensionを作成し、UICollectionViewDelegateおよびUICollectionViewDataSourceプロトコルを実装します。numberOfItemsInSection(APIから返却された映画の数)とcellForItemAt(実際にセルへAPIレスポンスを反映させ、ポスター画像のダウンロードと設定を行う)を実装しましょう。

 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return Movies.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MovieCell.ID, for: indexPath) as? MovieCell{
            cell.updateCell(posterURL: Movies[indexPath.row].poster_path)
            return cell
        }
        return UICollectionViewCell()
        
    }

最後に、実際のAPI呼び出しを行います。これは先ほど実装したupdateSearchResults()デリゲートメソッド内で行います。以下のコードを追加してください。

    func updateSearchResults(for searchController: UISearchController) {
        guard let query = searchController.searchBar.text else{return}
        APIService.shared.getMovies(for:query.trimmingCharacters(in: .whitespaces)) { titles, error in
            if let titles = titles {
                self.Movies = titles
                DispatchQueue.main.async {
                    self.MovieCollectionView.reloadData()
                }
       
            }
        }
        
    }

ここでは、ユーザーが検索バーに入力するか検索ボタンを押すたびに、検索バーに入力された名前に基づいて映画情報を取得するHTTP GETリクエストを送信します。その後、CollectionViewを再読み込みすることで、映画のポスター画像がセルに反映されます。

注意点として、この更新処理はメインスレッド(UIスレッド)で行う必要があります。iOSではデフォルトでHTTPリクエストがバックグラウンドスレッドで実行されるため、UI要素の更新にはメインスレッドを使わなければならないからです。

それでは、シミュレーターでアプリを実行して結果を確認してみましょう。

iOSアプリでUISearchControllerを使う方法!TMDB APIと連携した映画検索アプリの作り方

おめでとうございます!これであなたも、iOSアプリでのUISearchControllerの使い方をマスターできました。

  1. iOS 11のポッドキャストアプリの使い方 – 新機能を徹底解説

    Appleはついに「Podcast」アプリの刷新が必要であることに気づき、iOS 11で同アプリに新たな命を吹き込みました。待ち望まれていたビジュアルのアップデートにより、デザインがより魅力的になり、ポッドキャストの追加や視聴がこれまで以上に簡単になりました。さらに、あなたの好みを学習するにつれて、おすすめのポッドキャストを自動的に提案してくれるようにもなっています。新機能は制作者とリスナーの両方にとって嬉しい内容となっています。革新的なタブデザインアプリを起動して最初に気づくのは、画面下部に追加された4つの新しいタブです。タブは「今すぐ聴く」「ライブラリ」「見る」「検索」の4つ構成で、「検索

  2. iOS 11でAirDropを使う方法:有効化からファイル共有まで完全解説

    AirDrop(エアドロップ)は、iPhoneに標準搭載された便利なファイル共有機能です。2台のAppleデバイス間で、写真や動画、連絡先、Webページなどをケーブル不要ですばやくやり取りできます。写真アプリ、Safari、連絡先などさまざまなアプリから利用可能です。なお、iOS 10まではコントロールセンターにAirDropボタンが直接表示されていましたが、iOS 11ではレイアウトが変更になり、ボタンが見えなくなっています。そこで本記事では、iOS 11でAirDropを有効・無効にする2つの方法と、実際にファイルを共有する手順をわかりやすく解説します。iOS 11でAirDropを有効化