利用JSONDecoder串接第三方API
拖了好久才又回來補文章,深怕再不寫,可能就會忘了當初怎麼寫的了。
一開始要著手這個作業時,最苦惱的就是要串什麼第三方API,在查的時候看到有神秘的open apiヽ(́◕◞౪◟◕‵)ノ最後還是選擇來串這個可愛的api就好哈哈,好了,那正片開始~
學習目標
- table view + controller view
- JSONDecoder()
- searchBar
- 使用第三方套件Kingfisher
- WebKit
最終成果
參考API來源:
📍api回傳類別定義:
因為api回傳的資料有底線(snake case)因此在定義時,需要另外轉成我們的命名方法camel case。
struct Movie: Decodable {
let id: String
let title: String
let originalTitle: String
let originalTitleRomanised: String
let image: URL
let movieBanner: URL
let description: String
let director: String
let producer: String
let releaseDate: String
let runningTime: String
let rtScore: String
let url: String
enum CodingKeys: String, CodingKey {
case id
case title
case originalTitle = "original_title"
case originalTitleRomanised = "original_title_romanised"
case image
case movieBanner = "movie_banner"
case description
case director
case producer
case releaseDate = "release_date"
case runningTime = "running_time"
case rtScore = "rt_score"
case url
}
}
📍串接api:
在電影列表的地方載入時就希望能拿到列表資料,因此使用 URLSession.shared.dataTask
,並使用 JSONDecoder
串接api,最後當取得資要需要在main thread更新資料於是必須寫 Dispatch.main.async
其中因為tableview的UITableViewDataSource是用extension的方式寫在下方因此table view需要另外拉線定義使用
@IBOutlet weak var ListTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
ListTableView.dataSource = self // 設定 tableView 的 dataSource
fetchMovieList()
}
func fetchMovieList() {
let urlString = "https://ghibliapi.vercel.app/films"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
let decoder = JSONDecoder()
do {
self.movies = try decoder.decode([Movie].self, from: data)
self.showMovies = self.movies
DispatchQueue.main.async {
self.ListTableView.reloadData()
}
} catch {
print("Erro decoding JSON: \(error)")
}
}
}.resume()
}
📍設定table view:
在table view的每個cell有三個movie因此需要自定義cell
最後就是要讓高度正常顯示,可參考Peter的教學文章:
📍處理cell中三個電影的顯示:
在table view中,有處理cell的方法,可以用indexPath.row直接拿取陣列的資料,但因為cell有三個項目,因此需要另外處理。
每個cell都讀取api資料的3個項目:
// 實作 UITableViewDataSource 協定
extension MovieListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return showMovies.count / 3 // 計算顯示的資料列數
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 取得自定義的 ListTableViewCell
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(ListTableViewCell.self)", for: indexPath) as? ListTableViewCell else {
fatalError("Something went wrong with dequeuing the cell.")
}
// 取消選取效果
cell.selectionStyle = .none
// 計算每個資料列要顯示的電影範圍
let startMovieIndex = indexPath.row * 3
let endMovieIndex = min(startMovieIndex + 3, showMovies.count)
let rowMovies = Array(showMovies[startMovieIndex..<endMovieIndex])
// 更新 cell 中的電影標題、發行日期和海報圖片
for (index, label) in cell.movieNameLabels.enumerated() {
if index < rowMovies.count {
label.text = rowMovies[index].title
} else {
label.text = nil
}
}
for (index, label) in cell.yearLabels.enumerated() {
if index < rowMovies.count {
label.text = rowMovies[index].releaseDate
} else {
label.text = nil
}
}
for (index, pic) in cell.posterImageViews.enumerated() {
if index < rowMovies.count {
pic.kf.setImage(with: rowMovies[index].image)
} else {
pic.image = nil
}
}
return cell
}
}
📍把資料傳到下一頁:
每個cell有3個項目因此會有3個@IBSegueAction來傳他們的id,在下一頁拿到id後可以再用其他api拿取電影的詳細資料。
@IBSegueAction func showDetail3(_ coder: NSCoder, sender: Any?) -> MovieDetailViewController? {
let controller = MovieDetailViewController(coder: coder)
controller?.movieId = selectedMovie?.id
return controller
}
@IBSegueAction func showDetail2(_ coder: NSCoder, sender: Any?) -> MovieDetailViewController? {
let controller = MovieDetailViewController(coder: coder)
controller?.movieId = selectedMovie?.id
return controller
}
@IBSegueAction func showDetail1(_ coder: NSCoder, sender: Any?) -> MovieDetailViewController? {
let controller = MovieDetailViewController(coder: coder)
controller?.movieId = selectedMovie?.id
return controller
}
📍searchBar:
因為searchBar搜尋後條件會改變,並不想每次搜尋都打api因此需要用兩個陣列來儲存,一個用來儲存api回傳的所有的電影資料,一個用來儲存需要顯示的電影資料,最後也需要reloadData(),table view才會進行更新
var movies: [Movie] = [] // api拿的所有movie
var showMovies: [Movie] = [] // 需要顯示在畫面的movie
extension MovieListViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let searchText = searchBar.text, searchBar.text != ""{
// 過濾出搜尋列有包含的的單字電影
let searchMovie = movies.filter { movie in
movie.title.contains(searchText)
}
showMovies = searchMovie
ListTableView.reloadData() // 重新載入 TableView
} else {
showMovies = movies
ListTableView.reloadData()
}
searchBar.resignFirstResponder() // 關閉鍵盤
}
}
附上github