Computer >> Hướng Dẫn Máy Tính >  >> Điện Thoại Thông Minh >> iPhone

Làm Chủ UISearchController: Xây Dựng App Tìm Kiếm Phim Mạnh Mẽ Trên iOS

Xin chào mọi người! Trong bài này chúng ta sẽ học cách dùng UISearchController trong app iOS.

Chúng Ta Sẽ Xây Dựng Gì?

Chúng ta sẽ xây dựng app tìm kiếm phim dùng TMDB API để lấy thông tin phim và hiển thị bằng UICollectionView dựa trên truy vấn tìm kiếm của user.

Thiết Lập Dự Án

Mở Xcode và tạo project iOS App trống – chắc chắn chọn UIKit chứ không phải SwiftUI.

Trong app này chúng ta dùng pattern MVC nên tổ chức project bằng cách tạo các group và file Swift sau:

Làm Chủ UISearchController: Xây Dựng App Tìm Kiếm Phim Mạnh Mẽ Trên iOS

Đóng project Xcode. Mở terminal và cd vào thư mục project. Ở đây cần thêm SD WebImage Cocoa Pods để tải và cache ảnh poster phim bất đồng bộ.

Gõ lệnh sau trong terminal:

pod init

Khi list nội dung thư mục, bạn thấy Podfile mới. Mở file bằng text editor bất kỳ (ở đây dùng Vim). Sửa Podfile như hình dưới. Save và đóng Podfile.

Làm Chủ UISearchController: Xây Dựng App Tìm Kiếm Phim Mạnh Mẽ Trên iOS

Sau khi chỉ định SD WebImage, cài dependencies bằng lệnh:

pod install

Như bạn thấy, đã thêm SD WebImage pod vào project iOS thành công. Chạy lệnh sau để mở project trong Xcode.

open PROJECT_NAME.xcworkspace

Sau khi mở Xcode, chắc chắn build project bằng Command+B.

Cách Thiết Kế Giao Diện Người Dùng Với UIKit Và Programmatic UI

App cần ba UIElements: navigation bar chứa search bar, UISearchBarController cho tìm kiếm thực tế, và UICollectionView hiển thị kết quả.

Mở Scenedelegate.swift và thêm code sau vào method kết nối session:

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() }

Vì dùng programatic UI, trước tiên cần chỉ định Root View Controller – màn hình đầu tiên khi user launch app.

Ở đây app chỉ dùng một View Controller, nên wrap nó trong UINavigationController. Điều này cung cấp navigation bar để đặt UISearchController.

Mở HomeVC.swift và thêm properties sau:

 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 }()

Đầu tiên tạo UISearchController và config properties như placeholder text và style.

Sau đó tạo UICollectionView và chỉ định loại layout. Ở đây dùng UICollectionViewFlowLayout và các properties như scroll direction, item size, và chỉ định custom CollectionView cell class sẽ tạo sau.

Trong class HomeVC tạo function mới và thêm code sau để config auto-layout constraints cho UICollectionView:

 //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 } 

Đầu tiên nói rằng không cần convert auto resizing mask thành constraints. Sau đó gán collection view vào bốn cạnh của View Controller.

Trong method viewDidLoad() thêm các dòng sau:

 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() }

Ở đây chỉ định title cho ViewController theo sau bởi background color là systemBackground. Nếu device ở light mode thì nền trắng, dark mode thì nền tối.

Sau đó set current view controller làm search result updater, thêm SearchController vào navigation bar, thêm UICollectionView vào ViewController, setup delegate và datasource. Cuối cùng gán UICollectionView bằng auto-layout.

Tạo extension cho HomeVC và implement protocol UISearchResultsUpdating và stub method updateSearchResults.

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

Method updateSearchResults() sẽ được gọi mỗi khi text trong search bar thay đổi hoặc user tap nút search trên bàn phím.

Tiếp theo cần tạo custom UICollectionView cell. Trong file MovieCell.swift, thêm code sau:

import Foundation import UIKit import SDWebImage class MovieCell: UICollectionViewCell{ static let ID = "MovieCell" private var MoviePosterImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit // imageView.image = UIImage(systemName: "house") 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) } } }

Ở đây tạo custom collection view cell bằng cách sub-class UICollectionView và implement các function init().

Tạo UIImageView hiển thị ảnh poster phim và setup auto-layout constraints cho nó. Sau đó tạo function user-defined nhận Movie poster URL string làm parameter và tải nó bất đồng bộ không ảnh hưởng UI/Main thread. Nó làm điều này bằng SD WebImage CocoaPod thêm trước đó.

Cách Thiết Lập API Của Chúng Ta

Trước khi tiếp tục, bạn cần API key cho TMDB API bằng cách tạo tài khoản (miễn phí). Chúng ta dùng endpoint Movie Search của API nhận API Key và tên phim làm tham số.

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

Bạn có thể xem API response bằng cách chạy nó trong Postman.

Làm Chủ UISearchController: Xây Dựng App Tìm Kiếm Phim Mạnh Mẽ Trên iOS

Cách Tạo Model Cho API Response

Bây giờ nhận JSON response từ API. Cần decode sang Swift bằng cách tạo model struct implement protocol Codable.

Chúng ta có thể generate model struct cho JSON response dễ dàng bằng website JSON to Swift. Đây là code model cho API response – copy paste vào file 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 } 

Cách Thực Hiện HTTP Requests Bằng Swift

Bây giờ cần viết code Swift thực hiện HTTP GET requests trả về JSON response của API.

Swift cung cấp class URLSession giúp viết networking code dễ dàng không cần thư viện thứ 3 như AFNetworking, AlamoFire, v.v.

Mở APIService.swift và thêm code sau:

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) // print(decodedData) completion(decodedData.results,nil) } catch{ print(error) } } } task.resume() } } 

Ở đây tạo class API Service với singleton pattern nên cần instance cho class này làm static member. Sau đó tạo session cho networking task với default configuration, theo sau bởi user-defined method getMovies().

Sau đó tạo networking task – ở đây cần HTTP GET requests có thể thực hiện bằng method dataTask() của URLSession. Nó nhận URL làm parameter và cho completion handler chứa data từ API, error data nếu có lỗi, và response với thông tin HTTP Response như status codes và messages.

Nếu có error, thoát function với error data. Nếu không, decode JSON data dựa trên Swift Model và thoát function với data đã decode.

Cách Hiển Thị Kết Quả Tìm Kiếm Trên UICollectionView

Trong HomeVC.swift, tạo private property là array của object Title. Chúng sẽ chứa info từng phim trả về bởi API.

private var Movies = [Title]()

Trong HomeVC.swift tạo extension cho class HomeVC và implement UIColletionViewDelegate và UICollectionViewDatasource protocols. Sau đó implement numberOfItemsInSection (bằng số phim trả về API) và cellForItemAt (thực sự populate cell với API response, như tải và set ảnh poster).

 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.backgroundColor = .systemBackground cell.updateCell(posterURL: Movies[indexPath.row].poster_path) return cell } return UICollectionViewCell() }

Cuối cùng cần thực hiện API call thực tế, làm bên trong delegate method updateSearchResults() đã implement trước. Thêm code sau vào method đó:

 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() } } } }

Ở đây, mỗi khi user gõ vào search bar hoặc nhấn nút search, chúng ta thực hiện HTTP GET request lấy phim (dựa trên tên nhập vào search bar). Sau đó reload CollectionView cập nhật cell với poster phim.

Lưu ý cần làm trên Main/UI Thread, vì mặc định iOS tự động làm HTTP request trên background thread. Nghĩa là cần dùng UI/Main Thread để update UI elements.

Bây giờ chạy app trên simulator để xem kết quả:

Làm Chủ UISearchController: Xây Dựng App Tìm Kiếm Phim Mạnh Mẽ Trên iOS

Chúc mừng! Bạn đã học dùng UISearchController trong app iOS.