λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
🍎 iOS/UIKit

[iOS] FLO μ•± 개발 일지 #2. TableView 둜 가사 ν™”λ©΄ κ°œλ°œν•˜κΈ°

by Danna 2021. 6. 30.
728x90
728x90

[iOS] FLO μ•± κ°œλ°œ μΌμ§€ #2. TableView λ‘œ κ°€μ‚¬ ν™”λ©΄ κ°œλ°œν•˜κΈ°

이번 개발 μΌμ§€λŠ” 전체 가사 보기 화면을 κ°œλ°œν•˜λ©΄μ„œ TableView λ₯Ό μ–΄λ–»κ²Œ μ΄μš©ν–ˆλŠ”μ§€μ— λŒ€ν•΄ μž‘μ„±ν•΄λ³Όκ²Œμš”! μ•„λž˜ μˆœμ„œλ‘œ μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€


1. λ¬Έμžμ—΄μ˜ 가사λ₯Ό λ”•μ…”λ„ˆλ¦¬λ‘œ λ³€ν™˜ν•˜κΈ° 

2. TableView 둜 전체 가사 ν™”λ©΄ UI κ°œλ°œν•˜κΈ°

3. Music Player 의 μ‹œκ°„μ΄ 지남에 따라 가사가 λ³€κ²½λ˜λ„λ‘ ν•˜κΈ°

* Issue :: Observer λ₯Ό λ“±λ‘ν•˜κ³  화면이 μ’…λ£Œλ λ•Œ ν•΄μ œν•΄μ£Όμ§€ μ•Šμ•„ 가사 싱크가 λ§žμ§€μ•ŠλŠ” μ΄μŠˆκ°€ λ°œμƒ (ν•΄κ²°)

4. TableView 둜 νŠΉμ • 가사 ν„°μΉ˜ μ‹œ ν•΄λ‹Ή ꡬ간뢀터 μž¬μƒλ˜λ„λ‘ κ°œλ°œν•˜κΈ°


 

πŸ‘€ 1. λ¬Έμžμ—΄μ˜ 가사λ₯Ό λ”•μ…”λ„ˆλ¦¬λ‘œ λ³€ν™˜ν•˜κΈ° 

JSON 으둜 λ°›μ•„μ˜¨ κ°€μ‚¬λŠ” String νƒ€μž…μœΌλ‘œ, "[mm:ss:mss]가사\n[mm:ss:mss]가사\n"  ν˜•νƒœλ‘œ λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€. λ¨Όμ € split ν•¨μˆ˜λ₯Ό μ΄μš©ν•΄ '\n' 을 κΈ°μ€€μœΌλ‘œ ν•˜λ‚˜μ˜ 가사씩 λΆ„λ¦¬ν•œ ν›„ μ •κ·œν‘œν˜„μ‹μ„ μ΄μš©ν•΄ μ‹œκ°„ 뢀뢄을 νŒŒμ‹±ν–ˆκ³ , ']' μ΄ν›„μ˜ 뢀뢄을 κ°€μ‚¬λ‘œ μ €μž₯ν–ˆμŠ΅λ‹ˆλ‹€.

"[00:16:200]we wish you a merry christmas\n[00:18:300]we wish you a merry christmas\n
  [00:21:100]we wish you a merry christmas\n[00:23:600]and a happy new year\n...
  [02:57:900]we wish you a merry christmas\n[03:00:500]and a happy new year"

 

JSON 데이터λ₯Ό λ°›μ•„μ˜€λ©΄μ„œ [μ‹œκ°„(Int): 가사(String)] νƒ€μž…μ˜ λ”•μ…”λ„ˆλ¦¬λ‘œ 값을 μ €μž₯ν•΄λ’€μŠ΅λ‹ˆλ‹€. 이 λ•Œ, ms λ‹¨μœ„λŠ” μ œμ™Έν•˜κ³ , λΆ„κ³Ό 초 λ‹¨μœ„λ§Œ κ³ λ €ν•΄μ„œ 초 λ‹¨μœ„ μ‹œκ°„μœΌλ‘œ Key λ₯Ό μ§€μ •ν–ˆμŠ΅λ‹ˆλ‹€! 그리고, 처음 μ‹œμž‘μΈ μ‹œκ°„ 0κ³Ό λλ‚˜λŠ” μ‹œκ°„(duration) 198 을 Key 둜 κ°–λŠ” 값을 μΆ”κ°€ν•΄μ€¬μŠ΅λ‹ˆλ‹€. 

{ 0: "간주쀑",
  16 : "we wish you a merry christmas",
  18 : "we wish you a merry christmas",
  ...
  177 : "we wish you a merry christmas",
  180 : "and a happy new year"
  198 : "간주쀑" }
* μ‹€μ œ λ”•μ…”λ„ˆλ¦¬λŠ” μ •λ ¬λ˜μ–΄μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

 

TableView μ—μ„œ 가사λ₯Ό μ‹œκ°„μˆœμœΌλ‘œ μ •λ ¬ν•΄μ„œ λ³΄μ—¬μ€˜μ•Ό ν•˜κΈ° λ•Œλ¬Έμ—, μœ„μ˜ λ”•μ…”λ„ˆλ¦¬λ₯Ό 가사 λ°°μ—΄λ‘œ λ§Œλ“€μ–΄μ€λ‹ˆλ‹€! 이 λ•Œ, λ”•μ…”λ„ˆλ¦¬λ₯Ό key κΈ°μ€€μœΌλ‘œ μ •λ ¬ν•œ ν›„ value 만 λ”°λ‘œ λΆ„λ¦¬ν•˜λ©΄ λ©λ‹ˆλ‹€. 참고둜, λ”•μ…”λ„ˆλ¦¬λ₯Ό μ •λ ¬ν–ˆμ„ λ•Œμ—λŠ” μ‹€μ œ λ”•μ…”λ„ˆλ¦¬κ°€ μ •λ ¬λ˜λŠ” 것이 μ•„λ‹Œ λ”•μ…”λ„ˆλ¦¬μ˜ μ—˜λ¦¬λ¨ΌνŠΈλ₯Ό μ •λ ¬ν•œ 배열이 λ°˜ν™˜λ©λ‹ˆλ‹€. 

// sorted ν•¨μˆ˜μ˜ λ°˜ν™˜κ°’ : [Dictionary<Int, String>.Element]
// lyricsArray 의 νƒ€μž… : [String]
lyricsArray = lyricsDict.sorted { $0.key < $1.key }.map { $0.value }

 

βœ”οΈŽ 2. TableView 둜 전체 가사 ν™”λ©΄ UI κ°œλ°œν•˜κΈ°

 

TableView 의 Cell 은 가사λ₯Ό λ³΄μ—¬μ£ΌλŠ” lyricsLabel ν•˜λ‚˜λ§Œμ„ κ°–λŠ” λ‹¨μˆœν•œ κ΅¬μ‘°μž…λ‹ˆλ‹€!

κ΅¬ν˜„λœ μ•±μ˜ TableView // Storyboard

 

βœ”οΈŽ lyricsArray 배열을 μ΄μš©ν•΄ Table View Data Source ν”„λ‘œν† μ½œμ˜ ν•„μˆ˜ λ©”μ†Œλ“œλ₯Ό κ΅¬ν˜„ν•΄μ€λ‹ˆλ‹€.

1. tableView(_:numberOfRowsInSection:)
Return the number of rows for the table. :: ν•΄λ‹Ή λ©”μ†Œλ“œλ₯Ό κ΅¬ν˜„ν•΄ ν…Œμ΄λΈ”μ˜ 전체 ν–‰ 개수λ₯Ό μ•Œλ €μ€˜μ•Ό ν•œλ‹€.

2. tableView(_:cellForRowAt:)
Provide a cell object for each row. :: ν…Œμ΄λΈ”μ˜ 각 ν–‰λ§ˆλ‹€ Cell 을 μ œκ³΅ν•΄μ•Όν•œλ‹€.
extension LyricsViewController: UITableViewDelegate, UITableViewDataSource {
    
    // MARK: - TableView method
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.lyricsCount
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "LyricsCell", for: indexPath) as? LyricsCell else {
            return UITableViewCell()
        }
        cell.setLyrics(text: viewModel.lyricsArray[indexPath.row])
        return cell
    }
    
    // other methods ...
}

 

βœ”οΈŽ Music Player 의 μ‹œκ°„μ΄ 지남에 따라 가사가 λ³€κ²½λ˜λ„λ‘ ν•˜κΈ°

  • μŒμ•… μž¬μƒ μ‹œκ°„μ΄ 변함에 따라, UI κ°€ λ³€κ²½λ˜μ–΄μ•Όν•˜λŠ”λ° 이 λ•Œ ν…Œμ΄λΈ”λ·°κ°€ κ°€λ¦¬ν‚€λŠ” 행도 λ³€κ²½λ˜μ–΄μ•Όν•©λ‹ˆλ‹€.
  • AVPlayer 에 Observer λ₯Ό λ“±λ‘ν•΄μ„œ μ‹œκ°„μ΄ λ³€ν• λ•Œλ§ˆλ‹€ Label, Slider κ°’, ν…Œμ΄λΈ”λ·°κ°€ κ°€λ¦¬ν‚€λŠ” 행을 λ³€κ²½ν•΄μ£Όλ©΄ λ©λ‹ˆλ‹€.
  • TableView λ₯Ό 슀크둀 ν•˜λŠ” λ™μ•ˆμ— κ°€λ¦¬ν‚€λŠ” 행이 λ³€κ²½λ˜λ©΄ λΆˆνŽΈν•¨μ΄ μžˆμ„ 수 μžˆμ–΄, μŠ€ν¬λ‘€μ€‘μž„μ„ μ²΄ν¬ν•˜μ—¬ μ—…λ°μ΄νŠΈν•˜μ§€ μ•Šλ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€.
  • λ“±λ‘ν•œ Observer λŠ” View κ°€ μ’…λ£Œλ  λ•Œ, ν•΄μ œν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.
πŸ€” Issue 
가사 전체 화면을 μ’…λ£Œν•˜κ³  λ‹€μ‹œ μ‹œμž‘ν–ˆμ„ λ•Œ, κ°€μ‚¬μ˜ 싱크가 μ •ν™•ν•˜κ²Œ λ§žμ§€ μ•ŠλŠ” μ΄μŠˆκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
Observer λ₯Ό λ“±λ‘ν•˜κ³  화면이 μ’…λ£Œλ λ•Œ ν•΄μ œν•΄μ£Όμ§€ μ•Šμ•„μ„œ, λ°œμƒν•œ λ¬Έμ œμ˜€μŠ΅λ‹ˆλ‹€.
View κ°€ λ‘œλ”©λ  λ•Œ Observer λ₯Ό λ“±λ‘ν•˜κ³ , View κ°€ μ’…λ£Œλ  λ•Œ Observer λ₯Ό ν•΄μ œν•΄μ£Όμ–΄ ν•΄κ²°ν–ˆμŠ΅λ‹ˆλ‹€.

 

1) LyricsViewController μ—μ„œ Music Player 에 Observer 등둝 / ν•΄μ œν•˜λŠ” μ½”λ“œ

func addObserverToPlayer() {
    timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 10), queue: DispatchQueue.main) { time in
        guard self.isScrolling == false else { return }
        self.updateTime(time)
    }
}
    
func removePeriodicTimeObserver() {
    if let token = timeObserver {
        player.removeTimeObserver(token: token)
        timeObserver = nil
    }
}

 

2) Music Player 의 μ‹œκ°„μ΄ 변경될 λ•Œλ§ˆλ‹€ ν˜ΈμΆœλ˜μ–΄ UI λ₯Ό μ—…λ°μ΄νŠΈ ν•˜λŠ” ν•¨μˆ˜ 

 

  • μœ„μ˜ Observer λ₯Ό λ“±λ‘ν•˜λŠ” ν•¨μˆ˜μ— μ˜ν•΄, 슀크둀이 λ˜μ§€ μ•ŠλŠ” κ²½μš°μ— updateTime λ©”μ†Œλ“œκ°€ ν˜ΈμΆœλ©λ‹ˆλ‹€.
  • updateTime λ©”μ†Œλ“œλŠ” μž¬μƒμ‹œκ°„μ„ λ‚˜νƒ€λ‚΄λŠ” Labelκ³Ό Slider 의 값을 λ³€κ²½ν•˜κ³  κ°€μ‚¬μ˜ index κ°€ λ³€κ²½λ˜λŠ” κ²½μš°μ—λ§Œ TableView 의 row λ₯Ό λ³€κ²½ν•΄μ€λ‹ˆλ‹€.
  • κ°€μ‚¬λŠ” 맀번 λ³€κ²½λ˜λŠ” 것이 μ•„λ‹ˆλΌ, μž¬μƒ μ‹œκ°„μ΄ ν˜„μž¬ κ°€μ‚¬μ˜ λ²”μœ„λ₯Ό λ„˜μ–΄κ°ˆ λ•Œμ—λ§Œ μ—…λ°μ΄νŠΈλ˜λ„λ‘ ν•˜κΈ° μœ„ν•¨μž…λ‹ˆλ‹€.

 

func updateTime(_ time: CMTime) {
    currentTimeLabel.text = player.currentTimeText
    progressSlider.value = Float(player.currentValue)
    let index = viewModel.getCurrentLyricsIndex()
    guard viewModel.prevIndex != index else { return }
    updateLyricsTableViewCell(prev: viewModel.prevIndex, index: index)
    viewModel.prevIndex = index
}

 

3) TableView μ—μ„œ ν˜„μž¬ μž¬μƒλ˜λŠ” κ°€μ‚¬λ‘œ λ³€κ²½ν•˜λŠ” ν•˜μ΄λΌμ΄νŒ…ν•˜λŠ” ν•¨μˆ˜

 

  • updateLyricsTableViewCell(prev: index:) λ©”μ†Œλ“œμ—μ„œ prev λŠ” 이전에 μž¬μƒλœ κ°€μ‚¬μ˜ 인덱슀, index λŠ” ν˜„μž¬ μž¬μƒμ€‘μΈ 가사 μΈλ±μŠ€μž…λ‹ˆλ‹€.
  • ν˜„μž¬ μž¬μƒν•˜λŠ” κ°€μ‚¬μ˜ row 둜 ν…Œμ΄λΈ”μ„ μŠ€ν¬λ‘€μ‹œν‚€κ³ , Cell 의 κΈ€μž 색상 배경등을 λ°”κΎΈλŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€. 
  • UITableView.scrollToRow(at:at:animated:) λ©”μ†Œλ“œλ₯Ό 톡해 ν˜„μž¬ μž¬μƒλ˜λŠ” 가사가 μžˆλŠ” ν–‰μœΌλ‘œ μ΄λ™λ˜λ„λ‘ ν•©λ‹ˆλ‹€.
  • index 값을 톡해 ν˜„μž¬ μž¬μƒμ€‘μΈ κ°€μ‚¬μ˜ Cell 에 λŒ€ν•΄ .setNowLyrics() λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•΄ 색상을 κ°•μ‘°μ‹œμΌœμ€λ‹ˆλ‹€.
  • prev 값을 톡해 이전 μž¬μƒμ€‘μΈ κ°€μ‚¬μ˜ Cell 에 λŒ€ν•΄ .desetPrevLyrics() λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•΄ κΈ°λ³Έ μƒ‰μƒμœΌλ‘œ λ³€κ²½ν•΄μ€λ‹ˆλ‹€.

 

func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool)
* 첫번째 νŒŒλΌλ―Έν„° indexPathλŠ” IndexPath νƒ€μž…μœΌλ‘œ 이동할 row λ₯Ό μ§€μ •ν•©λ‹ˆλ‹€.
* λ‘λ²ˆμ§Έ νŒŒλΌλ―Έν„° scrollPositionλŠ” ν•΄λ‹Ή 행이 ν…Œμ΄λΈ”λ·°μ˜ 상단, 쀑간, ν•˜λ‹¨ 쀑 어디에 μœ„μΉ˜ν•  지 μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
* μ„Έλ²ˆμ§Έ νŒŒλΌλ―Έν„° animated λŠ” 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜μ˜ μ—¬λΆ€μž…λ‹ˆλ‹€.
func updateLyricsTableViewCell(prev: Int, index: Int) {
    lyricsTableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
        
    let indexPath = IndexPath(row: index, section: 0)
    if let cell = lyricsTableView.cellForRow(at: indexPath) as? LyricsCell {
        cell.setNowLyrics()
    }

    guard prev >= 0 else { return }
    let prevIndexPath = IndexPath(row: prev, section: 0)
    if let prevCell = lyricsTableView.cellForRow(at: prevIndexPath) as? LyricsCell {
        prevCell.desetPrevLyrics()
    }
}

 

βœ”οΈŽ TableView 둜 νŠΉμ • 가사 ν„°μΉ˜ μ‹œ ν•΄λ‹Ή ꡬ간뢀터 μž¬μƒλ˜λ„λ‘ κ°œλ°œν•˜κΈ°

  • νŠΉμ • 가사λ₯Ό ν„°μΉ˜ν•œλ‹€λŠ” 것은, TableView 의 Cell 을 ν΄λ¦­ν•œ 것과 κ°™λ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.
  • UITableViewDelegate ν”„λ‘œν† μ½œμ˜ λ©”μ†Œλ“œ 쀑, 셀을 ν΄λ¦­ν–ˆμ„ λ•Œ ν˜ΈμΆœλ˜λŠ” ν•¨μˆ˜λ₯Ό μ΄μš©ν–ˆμŠ΅λ‹ˆλ‹€.
  • μš”κ΅¬μ‚¬ν•­μ— 있던, toggleButton 이 선택됨에 따라 μž¬μƒκ΅¬κ°„μ„ λ³€κ²½ν•˜κ±°λ‚˜, View λ₯Ό μ’…λ£Œν•˜λ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€.
πŸ’‘ tableView(_:didSelectRowAt:)
Tells the delegate a row is selected. :: νŠΉμ • 행이 선택됐을 λ•Œ, Delegate μ—κ²Œ μ•Œλ €μ€€λ‹€.

 

βœ”οΈŽ μž¬μƒκ΅¬κ°„μ„ λ³€κ²½ν•˜κΈ° μœ„ν•œ 방법

  • μž¬μƒκ΅¬κ°„μ„ λ³€κ²½ν•˜κΈ° μœ„ν•΄μ„œ, ν΄λ¦­λœ κ°€μ‚¬μ˜ indexλ₯Ό 톡해 λ”•μ…”λ„ˆλ¦¬μ˜ key λ₯Ό κ°€μ Έμ™”μŠ΅λ‹ˆλ‹€.
  • λ”•μ…”λ„ˆλ¦¬μ˜ key 인 μ‹œκ°„κ°’μ„ 톡해 AVPlayer 의 seek ν•¨μˆ˜λ₯Ό μ΄μš©ν•΄ μž¬μƒμ‹œκ°„μ„ λ³€κ²½ν•˜κ³  UIλ₯Ό μ—…λ°μ΄νŠΈν–ˆμŠ΅λ‹ˆλ‹€.
extension LyricsViewController: UITableViewDelegate, UITableViewDataSource {

    // other methods ...

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if toggleButton.isSelected {
            let seconds = viewModel.lyricsDict.sorted { $0.key < $1.key }[indexPath.row].key
            let time = CMTime(seconds: Double(seconds), preferredTimescale: 100)
            player.seek(time)
            updateTime(time)
        } else {
            dismiss(animated: true, completion: nil)
        }
    }
}

 

 

πŸ‘€ Github

 

songda515/FLO

ν”„λ‘œκ·Έλž˜λ¨ΈμŠ€ κ³Όμ œκ΄€ - FLO μ•± 개발. Contribute to songda515/FLO development by creating an account on GitHub.

github.com

 

728x90
728x90