สวัสดีทุกคน! ในบทความนี้เราจะเรียนรู้การใช้ UISearchController ในแอป iOS
เราจะสร้างอะไร?
เราจะสร้างแอปค้นหาหนังที่ใช้ TMDB API ดึงข้อมูลหนังและแสดงด้วย UICollectionView ตามคำค้นหาของผู้ใช้
การตั้งค่าโปรเจกต์
เปิด Xcode สร้างโปรเจกต์ iOS App ใหม่แบบว่าง — ตรวจสอบให้แน่ใจว่าเลือก UIKit ไม่ใช่ SwiftUI
ในแอปนี้เราจะใช้รูปแบบ MVC จัดกลุ่มโปรเจกต์โดยสร้างกลุ่มและไฟล์ Swift ต่อไปนี้:
ปิดโปรเจกต์ Xcode เปิด Terminal ไปที่ไดเรกทอรีโปรเจกต์ ต้องเพิ่ม SD WebImage CocoaPods เพื่อดาวน์โหลดและแคชรูปโปสเตอร์หนังแบบ Asynchronous
พิมพ์คำสั่งใน Terminal:
pod init
ตอนนี้ list เนื้อหาไดเรกทอรี จะเห็น Podfile ใหม่ เปิดไฟล์ด้วย Text Editor ใดๆ (ที่นี่ใช้ Vim) แก้ Podfile ให้คล้ายภาพด้านล่าง Save และปิด
ตอนนี้เราระบุ SD WebImage แล้ว ติดตั้ง dependencies ด้วยคำสั่ง:
pod install
เห็นว่าเพิ่ม SD WebImage pod ในโปรเจกต์ iOS สำเร็จ รันคำสั่งเพื่อเปิดโปรเจกต์ใน Xcode:
open PROJECT_NAME.xcworkspace
หลังเปิด Xcode ตรวจสอบให้แน่ใจว่า Build โปรเจกต์ด้วย Command+B
วิธีออกแบบ UI ด้วย UIKit และ Programmatic UI
แอปของเราต้องการ UI 3 อย่าง: Navigation Bar เพื่อใส่ Search Bar, UISearchBarController สำหรับค้นหาจริง, และ UICollectionView แสดงผลลัพธ์
เปิด Scenedelegate.swift ใส่โค้ดต่อไปนี้ภายในเพื่อเชื่อมต่อกับ session method:
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()
}
เนื่องจากเราใช้ Programmatic UI ก่อนอื่นต้องระบุ Root View Controller — หน้าจอแรกเมื่อผู้ใช้เปิดแอป
ในแอปนี้เราใช้ View Controller เดียว จึงห่อด้วย UINavigationController ซึ่งให้ Navigation Bar ที่เราสามารถวาง UISearchController ได้
เปิด HomeVC.swift เพิ่ม Properties ต่อไปนี้:
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 กำหนดคุณสมบัติ เช่น placeholder text และ style
จากนั้นสร้าง UICollectionView ระบุชนิด Layout ที่ควรใช้ กรณีนี้คือ UICollectionViewFlowLayout และคุณสมบัติอื่นๆ เช่น scroll direction, item size, และระบุ Custom CollectionView Cell Class ที่เราจะสร้างภายหลัง
ภายในคลาส HomeVC สร้างฟังก์ชันใหม่ ใส่โค้ดต่อไปนี้เพื่อกำหนด Auto Layout Constraints แบบโปรแกรมสำหรับ 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
}
ก่อนอื่นเราบอกว่าไม่ต้องแปลง auto resizing mask เป็น constraints จากนั้นยึด Collection View ทั้ง 4 ด้านของ View Controller
ภายในเมธอด 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()
}
ที่นี่เราระบุ Title ของ ViewController ตามด้วยสีพื้นหลังเป็น systemBackground ถ้าอุปกรณ์อยู่ใน Light Mode จะแสดงพื้นหลังสีขาว ถ้า Dark Mode จะเป็นสีเข้ม
จากนั้นตั้ง View Controller ปัจจุบันเป็น Search Results Updater เพิ่ม SearchController ลง Navigation Bar เพิ่ม UICollectionView ให้ ViewController ตั้งค่า Delegate และ DataSource สุดท้ายยึด UICollectionView ด้วย Auto Layout
สร้าง Extension สำหรับ HomeVC 实作 UISearchResultsUpdating protocol และ stub method updateSearchResults
extension HomeVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text else { return }
}
}
เมธอด updateSearchResults() จะถูกเรียกทุกครั้งที่ข้อความใน Search Bar เปลี่ยน หรือเมื่อผู้ใช้แตะปุ่มค้นหาบนคีย์บอร์ด
ต่อไปเราต้องสร้าง Custom UICollectionView Cell ในไฟล์ 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
// 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)
}
}
}
ที่นี่เราสร้าง Custom Collection View Cell โดย subclass UICollectionView และ implement init() functions
เราสร้าง UIImageView แสดงรูปโปสเตอร์หนัง ตั้ง Auto Layout Constraints สร้างฟังก์ชันผู้ใช้รับ URL โปสเตอร์หนังเป็นพารามิเตอร์ และดาวน์โหลดแบบ Asynchronous โดยไม่กระทบ UI Thread/Main Thread ทำได้โดย SD WebImage CocoaPod ที่เพิ่มไว้ก่อนหน้า
วิธีตั้งค่า API ของเรา
ก่อนต่อไป คุณต้องได้ API Key สำหรับ TMDB API โดยสร้างบัญชี (ฟรี) เราจะใช้ Endpoint Movie Search ของ API ซึ่งรับ API Key และชื่อหนังเป็นพารามิเตอร์
https://api.themoviedb.org/3/search/movie?api_key=API_KEY_HERE&query=batman
คุณสามารถตรวจสอบ API Response โดยรันใน Postman
วิธีสร้าง Model สำหรับ API Response
ตอนนี้เราได้ JSON Response จาก API ต้อง Decode เป็น Swift ซึ่งทำได้โดยสร้าง Struct Model ที่ implement Codable Protocol
เราสร้าง Model Struct สำหรับ JSON Response ได้ง่ายด้วยเว็บ JSON to Swift นี่คือโค้ด Model สำหรับ API Response — คุณคัดลอกวางในไฟล์ 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
}
วิธีทำ HTTP Requests ด้วย Swift
ตอนนี้เราต้องเขียนโค้ด Swift ทำ HTTP GET Requests คืนค่า JSON Response ของ API
Swift มี Class URLSession ทำให้เขียน Networking Code ง่ายขึ้น ไม่ต้องไลบรารีบุคคลที่สามอย่าง 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)
// print(decodedData)
completion(decodedData.results, nil)
} catch {
print(error)
}
}
}
task.resume()
}
}
ที่นี่เราสร้าง Class API Service แบบ Singleton ต้องมี Instance เป็น Static Member ของ Class จากนั้นสร้าง Session สำหรับ Networking Task ด้วย Default Configuration ตามด้วย User Defined Method getMovies()
จากนั้นสร้าง Networking Task — ในกรณีนี้เราต้องทำ HTTP GET Requests ซึ่งทำได้ด้วยเมธอด dataTask() ของ URLSession Class รับ URL เป็นพารามิเตอร์ และให้ Completion Handler ที่มี Data ที่คืนจาก API, Error Data ถ้ามี Error, และ Response ที่มีข้อมูล HTTP Response เช่น Status Codes และข้อความที่สอดคล้อง
ถ้ามี Error เราจะ Escape ออกจากฟังก์ชันพร้อม Error Data ถ้าไม่ เรา Decode JSON Data ตาม Swift Model และ Escape ออกพร้อม Decoded Data
วิธีแสดงผลลัพธ์การค้นหาบน UICollectionView
ใน HomeVC.swift สร้าง Private Property เป็น Array ของ Object Title จะเก็บข้อมูลแต่ละหนังที่ API คืน
private var Movies = [Title]()
ใน HomeVC.swift สร้าง Extension สำหรับ HomeVC Class 实作 UIColletionViewDelegate และ UICollectionViewDatasource Protocols จากนั้น implement numberOfItemsInSection (เท่ากับจำนวนหนังที่ API คืน) และ cellForItemAt (ซึ่งจริงๆ populate Cell ด้วย API Responses เช่น ดาวน์โหลดและตั้งรูปโปสเตอร์)
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()
}
สุดท้ายเราต้องทำ Actual API Call ซึ่งทำภายใน Delegate Method updateSearchResults() ที่เรา implement ไว้ก่อนหน้า ภายในเมธอดนั้น ใส่โค้ดต่อไปนี้:
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()
}
}
}
}
ที่นี่ ทุกครั้งที่ผู้ใช้พิมพ์ใน Search Bar หรือกดปุ่มค้นหา เราทำ HTTP GET Request ดึงหนัง (ตามชื่อที่พิมพ์ใน Search Bar) จากนั้น Reload CollectionView ซึ่งอัปเดต Collection View Cells ด้วยรูปโปสเตอร์หนัง
สังเกตว่าเราต้องทำใน Main Thread/UI Thread เพราะ iOS ทำ HTTP Request ใน Background Thread โดยอัตโนมัติ หมายความว่าเราต้องใช้ UI/Main Thread อัปเดต UI Elements
ตอนนี้รันแอปใน Simulator ดูผลลัพธ์:
ยินดีด้วย! คุณเรียนรู้การใช้ UISearchController ในแอป iOS แล้ว