Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> สมาร์ทโฟน >> iPhone

เชี่ยวชาญ UISearchController: สร้างแอปค้นหาหนังแรงใน iOS

สวัสดีทุกคน! ในบทความนี้เราจะเรียนรู้การใช้ UISearchController ในแอป iOS

เราจะสร้างอะไร?

เราจะสร้างแอปค้นหาหนังที่ใช้ TMDB API ดึงข้อมูลหนังและแสดงด้วย UICollectionView ตามคำค้นหาของผู้ใช้

การตั้งค่าโปรเจกต์

เปิด Xcode สร้างโปรเจกต์ iOS App ใหม่แบบว่าง — ตรวจสอบให้แน่ใจว่าเลือก UIKit ไม่ใช่ SwiftUI

ในแอปนี้เราจะใช้รูปแบบ MVC จัดกลุ่มโปรเจกต์โดยสร้างกลุ่มและไฟล์ Swift ต่อไปนี้:

เชี่ยวชาญ UISearchController: สร้างแอปค้นหาหนังแรงใน iOS

ปิดโปรเจกต์ Xcode เปิด Terminal ไปที่ไดเรกทอรีโปรเจกต์ ต้องเพิ่ม SD WebImage CocoaPods เพื่อดาวน์โหลดและแคชรูปโปสเตอร์หนังแบบ Asynchronous

พิมพ์คำสั่งใน Terminal:

pod init

ตอนนี้ list เนื้อหาไดเรกทอรี จะเห็น Podfile ใหม่ เปิดไฟล์ด้วย Text Editor ใดๆ (ที่นี่ใช้ Vim) แก้ Podfile ให้คล้ายภาพด้านล่าง Save และปิด

เชี่ยวชาญ UISearchController: สร้างแอปค้นหาหนังแรงใน iOS

ตอนนี้เราระบุ 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

เชี่ยวชาญ UISearchController: สร้างแอปค้นหาหนังแรงใน iOS

วิธีสร้าง 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

ยินดีด้วย! คุณเรียนรู้การใช้ UISearchController ในแอป iOS แล้ว