#39 串接 TIH API - TableViewController, CollectionView 與 prepare 傳遞資料

TableViewController-Hotel List, CollectionView-show photos, prepare-transfer information

本篇為實作 TableViewController, CollectionView 與 prepare 傳遞資料

繼上一篇介紹了 TIH 及其 API

接下來將介紹應用在APP上的實作部分:

URLSession 抓取資料

關於解析 Date

顯示 data — TableViewController

prepare 傳遞資料

製作 CollectionView

URLSession 抓取資料

在解析好 JSON 後,就可以開始利用 URLSession 告訴程式 API 網址及轉換 JSON 到自訂好的型別,之後就可以使用它的資料了

可以先把API Key 的 Value 寫成屬性,以及宣告一個空array,裡面是自訂型別

    let APIValue = "2M.....eE"

var hotels = [Hotel]()

把抓取資料寫成 function,之後放在 viewDidLoad 裡,使其 APP 開啟就自動抓取

func fetchList() {
let urlString = "https://api.stb.gov.sg/content/accommodation/v2/search?searchType=keyword&searchValues=city%20hall&sort=name&sortOrder=asc"

if let url = URL(string: urlString) {
var request = URLRequest(url: url)
request.setValue(APIValue, forHTTPHeaderField: "X-API-Key")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data,
let response = response as? HTTPURLResponse {
//印出server狀態
print("statusCode",response.statusCode)
let decorder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
decorder.dateDecodingStrategy = .formatted(dateFormatter)
do {
//從JSON物件轉換成自訂型別
let result = try decorder.decode(Result.self, from: data)
//轉換好型別後,存入定義好相同型別的空 array
//重要:得一起寫在 URLSession.shared.dataTask (background thread)裡,不然會存到空的資料
self.hotels = result.data

DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}.resume()
}
}

注意:URLSession 是在 background thread 執行,若是跟UI有關的都要寫在 DispatchQueue.main.async { } 的大括號裡,就會在 main thread 執行。

關於解析 Date

其中的關鍵在於利用 DateFormatter() 以及 JSONDecoder.dateDecodingStrategy

時間在 reviews 的最下一行

它的時間格式是自訂的時間格式,這時需要參考下面網站,在程式裡用英文字母呈現它的格式

變成:

let decorder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
decorder.dateDecodingStrategy = .formatted(dateFormatter)

到這一步,只是讓程式看懂它的時間格式

顯示 data - TableViewController

解析好 JSON 就可以開始藉由自訂型別去使用 JSON 資料,這裡是去顯示在 TableViewController 上

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return hotels.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(ListTableViewCell.self)", for: indexPath) as! ListTableViewCell

let hotel = hotels[indexPath.row]
//顯示飯店照片
//防止cell重複利用顯示錯誤影像,方法一:先清空影像
cell.photoImageView.image = nil
cell.processIndicator.startAnimating()
/*
防止cell重複利用顯示錯誤影像,方法二:顯示預設照片
cell.photoImageView.image = UIImage(systemName: "photo")
cell.photoImageView.tintColor = .systemGray5
cell.photoImageView.contentMode = .scaleAspectFit
cell.photoImageView.layer.cornerRadius = 10
*/
//當uuid是空字串,從url取得圖片
if hotel.images.first?.uuid == "" {
let urlString = hotel.images.first?.url
let finalUrl = URL(string: urlString!)
print("imageURL", urlString!)
cell.photoImageView.kf.setImage(with: finalUrl)
cell.photoImageView.contentMode = .scaleAspectFill
cell.processIndicator.stopAnimating()
}
//當url是空字串,從uuid取得圖片
else if hotel.images.first?.url == "" {

let hotelUuid = hotel.images.first?.uuid
if let url = URL(string: "https://tih-api.stb.gov.sg/media/v1/download/uuid/\(hotelUuid!)?apikey=\(APIValue)") {
cell.photoImageView.kf.setImage(with: url)
cell.photoImageView.contentMode = .scaleAspectFill
cell.processIndicator.stopAnimating()
}
} else {
//當uuid跟url都沒有時,顯示特定圖示
cell.photoImageView.image = UIImage(systemName: "photo.on.rectangle.angled")
cell.photoImageView.tintColor = .systemGray4
cell.photoImageView.contentMode = .scaleAspectFit
cell.processIndicator.stopAnimating()
}

cell.titleLabel.text = hotel.name

cell.ratingLabel.text = hotel.rating.description
//show the stars of rating
//avoid showing wrong images on cell
for star in cell.ratingImageViews {
star.image = UIImage(systemName: "")
}

for star in cell.ratingImageViews {
star.image = UIImage(systemName: "star")
star.tintColor = .systemGray5
}

let rating = hotel.rating
let intStarRating = Int(rating)
let starRagne = intStarRating-1

for star in 0...starRagne {
cell.ratingImageViews[star].tintColor = .systemYellow
cell.ratingImageViews[star].image = UIImage(systemName: "star.fill")

if rating - Double(intStarRating) > 0 {
cell.ratingImageViews[intStarRating].tintColor = .systemYellow
cell.ratingImageViews[intStarRating].image = UIImage(systemName: "star.leadinghalf.filled")
}
}
//顯示是否有官方衛生認證 SG Clean
if hotel.group != "" {
cell.groupLabel.text = "SG Clean"
cell.groupLabel.backgroundColor = UIColor(red: 131/255, green: 181/255, blue: 87/255, alpha: 1)
}

cell.configuration()

return cell
}
TableViewController-GIF

prepare 傳遞資料

傳遞資料的方法有很多種,這裡練習用 prepare 把資料傳遞到別的頁面。

Step 1:
起始(source)頁面( TableViewController 的 cell ),拉線終點(destination)頁面( ViewController ),

Step 2:
終點頁面裡,宣告好跟傳遞資料一樣型別的屬性,去承接資料

    var APIValue = ""
var hotel: Hotel?

Step 3:
起始頁面( TableViewController ) 裡,輸入 prepare function 的程式碼

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? IntroViewController,
let row = self.tableView.indexPathForSelectedRow?.row {
let hotel = hotels[row]
destination.hotel = hotel
destination.APIValue = APIValue
}
}
GIF
ViewController

製作 CollectionView

在 ViewController 下方加了一個 CollectionView,希望能以水平捲動的方式顯示飯店的照片

可以參考:

import Foundation
import UIKit

extension IntroViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//若image array是空的,至少產生一個cell,去顯示APP預設圖片
if hotel!.images.isEmpty == true {
return 1
} else {
return hotel!.images.count
}
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(OneCollectionViewCell.self)", for: indexPath) as? OneCollectionViewCell else {
fatalError("dequeueReusableCell PhotoCollectionViewCell Failed")
}

if hotel?.images.isEmpty == true {
cell.oneImageView.image = UIImage(systemName: "photo.on.rectangle.angled")
cell.oneImageView.contentMode = .scaleAspectFit
cell.oneImageView.tintColor = .systemGray4

} else {
onePhotoActivityIndicator.startAnimating()
let newIndex = (indexPath.item + hotel!.images.count) % hotel!.images.count

if hotel?.images[newIndex].uuid == "" {
let urlString = hotel?.images[newIndex].url
if let url = URL(string: urlString!) {
cell.oneImageView.kf.setImage(with: url)
cell.oneImageView.contentMode = .scaleAspectFill
onePhotoActivityIndicator.stopAnimating()
}

} else if hotel?.images[newIndex].url == "" {
let imageUuid = hotel?.images[newIndex].uuid
if let url = URL(string: "https://tih-api.stb.gov.sg/media/v1/download/uuid/\(imageUuid!)?apikey=\(APIValue)") {
cell.oneImageView.kf.setImage(with: url)
cell.oneImageView.contentMode = .scaleAspectFill
onePhotoActivityIndicator.stopAnimating()
}
}
}
photoCollectionView.isPagingEnabled = true
photoCollectionView.showsHorizontalScrollIndicator = false
return cell
}
}

extension IntroViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x/scrollView.bounds.width
pageIndicator.currentPage = Int(pageNumber)
//print(scrollView.contentOffset.x)
//print(scrollView.bounds.width)
}
}

下方加了一個 PageControl 去提示使用者可以滑動照片

        pageIndicator.hidesForSinglePage = true
pageIndicator.numberOfPages = hotel!.images.count
pageIndicator.pageIndicatorTintColor = .systemGray5
pageIndicator.currentPageIndicatorTintColor = .systemCyan
CollectionView-GIF

下一篇將介紹 CollectionView 上方三個 Button ( reviews, website and map )的連結部分。

--

--