Episode 92 — 實作 Table View 基本功能

Shien
彼得潘的 Swift iOS / Flutter App 開發教室
16 min readMay 14, 2022

完整操作

製作 Table View

使用技巧

  • 客製 cell 顯示表格的內容
  • 多個 section
  • 使用 view controller,在 view controller 上另外加入 table view
  • cell 採用固定高度,cell 裡的元件設定 auto layout 條件

步驟

  • Table View 放進 View Controller 中,自定元件設定 constraints 在 cell 裡。
  • Style 選擇 Custom,Identifier 取名 songCell
  • 建立歌手及歌曲 Structure
struct Song {
var name: String
var album: String
var image: String
var url: URL
var videoURL: URL
}
struct Singer {
var name: String
var songs: [Song]
}
  • 建立一個歌手實例,裡面排三個歌手、五首歌。
let singers = [
Singer(name: "Lauv", songs: [
Song(name: "I Like Me Better", album: "I Like Me Better-Single", image: "i like me better", url: Bundle.main.url(forResource: "I Like Me Better", withExtension: "mp3")!, videoURL: Bundle.main.url(forResource: "i like me better", withExtension: "mov")!),...]),
Singer(name: "Khalid", songs: [
Song(name: "Silence", album: "", image: "silence", url: Bundle.main.url(forResource: "Silence", withExtension: "mp3")!, videoURL: Bundle.main.url(forResource: "silence", withExtension: "MP4")!),...]),
Singer(name: "Justin Bieber", songs: [
Song(name: "Ghost", album: "", image: "ghost", url: Bundle.main.url(forResource: "Ghost", withExtension: "mp3")!,videoURL: Bundle.main.url(forResource: "ghost", withExtension: "mov")!),...])]
  • 將 table view 連到 view controller ,點選 dataSource 及 delegate 以指派 view controller 幫 table view 忙
  • 讓 ViewController 遵從 UITableViewDataSource 及 UITableViewDelegate 才能呼叫代理功能或屬性,使用 extension 不讓 ViewController 版面太複雜。
extension ViewController: UITableViewDataSource, UITableViewDelegate{ }
  • 這次有三位歌手所以我想做三個 sections,在 numberOfSections 功能裡回傳歌手實例的數量。
func numberOfSections(in tableView: UITableView) -> Int {
return singers.count
}
  • 使用參數有 numberOfRowsInSection 的功能顯示歌曲,由歌手實例裡的每位歌手的歌曲數量決定 row 的數量
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return singers[section].songs.count
}
  • 新增一個 file 使用 Cocoa Touch File 來創建一個型別為 UITableViewCell 的 class。把 storyboard 上 cell 裡面的自定元件建立 IBOutlet。
class SongTableViewCell: UITableViewCell {
@IBOutlet weak var songNameLabel: UILabel!
@IBOutlet weak var songImageView: UIImageView!
@IBOutlet weak var playButton: UIButton!

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code

}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
  • 回 ViewController 呼叫參數有 cellForRowAt 且回傳 UITableViewCell 的功能來顯示資料。讓 table view 使用 dequeueReusableCell(withIdentifier:,for:) 方法來重複使用 cell。withIdentifier 使用剛剛命名的 songCell。轉型為剛剛新建立的 SongTableViewCell。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "songCell", for: indexPath) as! SongTableViewCell

return cell
}
  • 利用 indexPath 的 section 及 row 將資料給元件。section 用來呼叫歌手,row 用來呼叫歌曲。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "songCell", for: indexPath) as! SongTableViewCell
cell.songImageView.image = UIImage(named: singers[indexPath.section].songs[indexPath.row].image)
cell.songNameLabel.text = singers[indexPath.section].songs[indexPath.row].name

return cell
}
  • 呼叫參數有 titleForHeaderInSection 的功能來將歌手名稱顯示在每個 section 的 header 上。
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return singers[section].name
}
  • 打開模擬器可以看到三個 section(歌手) 跟各五個 row (歌曲)了

播放歌曲

使用技巧

  • 使用 convert(at:, to:) 尋找元件位置
  • 維持 cell 元件狀態
  • 使用 AVPlayer 播音樂

步驟

  • 為了讓按鈕按下去直接播放音樂,先連接成一個 IBAction。必須要知道要播的歌是哪一個歌手跟歌曲,需要得到 section 跟 row。先用 convert 方法得知按到的按鈕是在 Table View 的哪個位置。
@IBAction func playMusic(_ sender: UIButton) {
let point = sender.convert(CGPoint.zero, to: tableView)
}
  • 用剛剛得到的位置後從 table view 呼叫 indexPathForRow(at:) 帶入座標,可以從中得到目前的 section 跟 row。用得到 section、row 取得歌曲並播放。最後把按鈕隱藏起來顯示(已事先排好的)暫停按鈕。
@IBAction func playMusic(_ sender: UIButton) {
let point = sender.convert(CGPoint.zero, to: tableView)

if let section = tableView.indexPathForRow(at: point)?.section, let row = tableView.indexPathForRow(at: point)?.row {
musicPlayer = AVPlayer(url: singers[section].songs[row].url)
musicPlayer?.play()
sender.isHidden = true
}
}
  • 模擬器測試有成功播放歌曲,但詭異的是每次播放後上下滾動,暫停鍵卻消失了。原因是因為每次滾動 cell 都會被更新,所以播放鍵又長回來了。
歌曲有在播放
  • 宣告 isPlaying 判斷歌曲是否正在播放、buttonSection 紀錄被按到的按鈕原本的 section、buttonRow 紀錄被按到按鈕原本的 row。
var isPlaying = false
var buttonSection = 0
var buttonRow = 0
  • 到 dequeueReusableCell 的方法中加入以下方法判斷。用isPlaying 判斷歌曲是否正在播放,是的話代表剛剛有一個播放件事消失的,我要用目前的 section、row 來判斷在在是不是在剛剛紀錄的 section 跟 row。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "songCell", for: indexPath) as! SongTableViewCell
cell.songImageView.image = UIImage(named: singers[indexPath.section].songs[indexPath.row].image)
cell.songNameLabel.text = singers[indexPath.section].songs[indexPath.row].name

if isPlaying {
if indexPath.section == buttonSection && indexPath.row == buttonRow {
cell.hidePlayButton()
} else {
cell.showPlayButton()
}
} else {
cell.showPlayButton()
}


return cell
}
  • 回到播放功能中,isPlaying 變成 true 代表正在播放。把按到的按鈕 section & row 記錄起來,就可以把剛剛 sender.isHidden 改為 tableView.reloadData 就會去重新呼叫 dequeueReusableCell 那個功能了。
@IBAction func playMusic(_ sender: UIButton) {
let point = sender.convert(CGPoint.zero, to: tableView)
isPlaying = true
if let section = tableView.indexPathForRow(at: point)?.section, let row = tableView.indexPathForRow(at: point)?.row {
musicPlayer = AVPlayer(url: singers[section].songs[row].url)
buttonSection = section
buttonRow = row
tableView.reloadData()

musicPlayer?.play()
}
}
  • 再跑一次模擬器成功讓按鈕維持在消失狀態了。
歌曲依然是有在播放的

播放 MV

使用技巧

  • 利用 IBSegueAction點選 cell 後到下一頁顯示詳細資訊
  • 使用 AVPlayerViewController 播影片

步驟

  • 匯入程式庫
import AVKit
  • 從 Library 叫出一個 AV Player View Controller 後,從 cell 連 segue 到那邊。
  • 幫這個 segue 取個小名叫 playVideo
  • 從 segue 連到 view controller 形成 IBSegueAction
@IBSegueAction func playVideo(_ coder: NSCoder) -> AVPlayerViewController? {

}
  • 先建立一個AVPlayerViewController 並傳入參數 code。先回傳這個 controller 消除錯誤訊息。
@IBSegueAction func playVideo(_ coder: NSCoder) -> AVPlayerViewController? {
let controller = AVPlayerViewController(coder: coder)

return controller

}
  • 使用 table view 的 indexPathForSelectedRow 屬性取得 section 跟 row
@IBSegueAction func playVideo(_ coder: NSCoder) -> AVPlayerViewController? {
let controller = AVPlayerViewController(coder: coder)
if let section = tableView.indexPathForSelectedRow?.section, let row = tableView.indexPathForSelectedRow?.row {

} else {
print("no section or no row")
}


return controller
}
  • 利用 section & row 取得影片來源並且播放
@IBSegueAction func playVideo(_ coder: NSCoder) -> AVPlayerViewController? {
let controller = AVPlayerViewController(coder: coder)
if let section = tableView.indexPathForSelectedRow?.section, let row = tableView.indexPathForSelectedRow?.row {
videoPlayer = AVPlayer(url: singers[section].songs[row].videoURL)
if let musicPlayer = musicPlayer {
musicPlayer.pause()
}
controller?.player = videoPlayer
videoPlayer = nil
controller?.player?.play()

pauseMusic()
} else {
print("no section or no row")
}

return controller
}
  • 使用模擬器可以播放影片

完整程式碼

資料來源

View Controller

Song Table View Cell

--

--