利用JSONDecoder串接第三方API

Una
彼得潘的 Swift iOS / Flutter App 開發教室
12 min readAug 22, 2023

拖了好久才又回來補文章,深怕再不寫,可能就會忘了當初怎麼寫的了。

一開始要著手這個作業時,最苦惱的就是要串什麼第三方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

在style選擇custom
table view也選擇Dynamic Prototypes模式,因為要經由api拿取資料
然後記得要拉dataSource到controllerView上

最後就是要讓高度正常顯示,可參考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

--

--