iOS/Toy project

[iOS : Toy Project] Apple Music App (2) : Track 모델 (데이터 구조 잡기)

yevdev 2022. 9. 13. 15:24

이전 포스팅

- AppleMusicApp(1) : 뷰 구성

 

자세한 코드는 여기로!

https://github.com/yexjin/iOS_Study/tree/main/AppleMusicApp

 

GitHub - yexjin/iOS_Study: iOS 토이프로젝트 모음집📱

iOS 토이프로젝트 모음집📱. Contribute to yexjin/iOS_Study development by creating an account on GitHub.

github.com

 

 

 


9/12

오늘은 Track 모델을 구현해봤다!

자세히 말하면 Track 구조체와 이 Track을 View와 연결해줄 Track Manager을 만들었다.

 

이 부분은 Track 폴더로 묶어줄건데, Track 폴더안의 파일은 

1. Track.swift : Track, Album 구조체

2. TrackModel.swift : Track Manager (Model 정의 + ViewModel의 메소드들)

3. Extension+AVPlayerItem.swift : AVPlayerItem 타입을 Track 타입으로 Casting 해주는 메서드 포함

이렇게 구성되어 있다.

 

 

Track.swift : Track, Album 구조체

//  Track.swift

import UIKit

struct Track {
    let title: String
    let artist: String
    let albumName: String
    let artwork: UIImage
    
    init(title: String, artist: String, albumName: String, artwork: UIImage) {
        self.title = title
        self.artist = artist
        self.albumName = albumName
        self.artwork = artwork
    }
}

struct Album {
    let title: String
    let tracks: [Track]
    
    var thumbnail: UIImage? {
        return tracks.first?.artwork
    }
    
    var artist: String? {
        return tracks.first?.artist
    }
    
    init(title: String, tracks: [Track]) {
        self.title = title
        self.tracks = tracks
    }
}

는 다음과 같고,

 

 

TrackManager.swift

//  TrackManager.swift

import UIKit
import AVFoundation

class TrackManager {
    // TODO: 프로퍼티 정의하기 - 트랙들, 앨범들, 오늘의 곡
    var tracks: [AVPlayerItem] = []
    var albums: [Album] = []
    var todayMusic: AVPlayerItem?
    
    // TODO: 생성자 정의하기
    init() {
        let tracks = loadTracks()
        self.tracks = tracks
        self.albums = loadAlbums(tracks: tracks)
        self.todayMusic = self.tracks.randomElement()
    }
    
    // TODO: 트랙 로드하기
    func loadTracks() -> [AVPlayerItem] {
    
    }
    
    // TODO: 인덱스에 맞는 트랙 로드하기
    func track(at index: Int) -> Track? {
    
    }
    
    // TODO: 앨범 로딩메소드 구현
    func loadAlbums(tracks: [AVPlayerItem]) -> [Album] {
        
    }
    
    // TODO: 오늘의 트랙이 랜덤으로 선택되게 하는 함수
    // - 헤더로 사용할..
    func loadOtherTodaysTrack(){

    }
 }

요 ViewModel 메서드들을 하나하나 꼼꼼히 살펴보자

 

 

 

1️⃣ loadTracks()

- 트랙로드하기

- 파일을 읽어서 AVPlayerItem을 만들어서 로드될 트랙 묶음으로 반환!

    func loadTracks() -> [AVPlayerItem] {
    
        let urls = Bundle.main.urls(forResourcesWithExtension: "mp3", subdirectory: nil) ?? []
        
        let items = urls.map { url  in
            return AVPlayerItem(url: url)
        }
   
        return items
        
    }

Bundle.main.urls = 해당 앱.메인.urls

Bundle : 앱 안의 Boundary, 트랙 파일 자체가 local에 있으니, Bundle을 사용

urls = URL의 배열 타입

forResourcesWithExtension : 확장자

subdirectory: 하위폴더

 

 

 

2️⃣ track()

- 지정된 인덱스에 맞는 트랙 로드하기 

 // TODO: 인덱스에 맞는 트랙 로드하기
    func track(at index: Int) -> Track? {
        let playerItem = tracks[index]
        // 여기서 가져온 playerItem은 타입이 AVPlayerItem
        
        let track = playerItem.convertToTrack()
        // convertToTrack : Track 타입으로 변경시키기 위한 함수
        // 이 'convertToTrack' 함수는 Extension+AVPlayerItem 파일에 정의
        return track
    }

로드된 트랙인 playerItem = track[index]은 타입이 AVPlayerItem이다.

convertToTrack() 메서드로 Track 타입으로 변경이 필요하다.

이 convertToTrack() 은 Extension+AVPlayerItem 파일에 정의되어 있다.

 


잠시, Extension+AVPlayerItem 파일에서 이 convertToTrack()을 살펴보자

//  Extension+AVPlayerItem.swift

import UIKit
import AVFoundation

extension AVPlayerItem {
    // type casting 기능 : Track 타입으로 Return!
    func convertToTrack() -> Track? {
        
        //AVPlayerItem -> asset -> metadata
        let metadataList = asset.metadata
        
        var trackTitle: String?
        var trackArtist: String?
        var trackAlbumName: String?
        var trackArtwork: UIImage?
        
        for metadata in metadataList {
            if let title = metadata.title {
                trackTitle = title
            }
            
            if let artist = metadata.artist {
               trackArtist = artist
            }
            
            if let albumName = metadata.albumName {
                trackAlbumName = albumName
            }
            
            if let artwork = metadata.artwork {
                trackArtwork = artwork
            }
        }
        
        guard let title = trackTitle,
            let artist = trackArtist,
            let albumName = trackAlbumName,
            let artwork = trackArtwork else {
                return nil
        }
        
        return Track(title: title, artist: artist, albumName: albumName, artwork: artwork)
    }
}


// AVPlayerItem의 Metadatas
// AVMetadataItem : AVPlayerItem의 assets 안의 metadata에 존재
extension AVMetadataItem {
    var title: String? {
        guard let key = commonKey?.rawValue, key == "title" else {
            return nil
        }
        return stringValue
    }
    
    var artist: String? {
        guard let key = commonKey?.rawValue, key == "artist" else {
            return nil
        }
        return stringValue
    }
    
    var albumName: String? {
        guard let key = commonKey?.rawValue, key == "albumName" else {
            return nil
        }
        return stringValue
    }
    
    var artwork: UIImage? {
        guard let key = commonKey?.rawValue, key == "artwork", let data = dataValue, let image = UIImage(data: data) else {
            return nil
        }
        return image
    }
}

 

해당 AVPlayerItem의 Metadatas까지 하나하나 묶어 Track 타입으로 반환시킨다

Metadatas?
- AVPlayerItem.assets.metadata 에 존재함 → AVMetaItem으로 extension하여 빼내왔음!

 

 


 

자, 다시 TrackManager.swift 파일로 돌아와서

특정 인덱스에 해당하는 트랙을 로드하는 것까지 살펴봤다.

 

 

3️⃣ loadAlbums() 

- 앨범 로딩 메소드

트랙 메소드를 그전에 다시 확인해보면

각 트랙에는 자신이 속한 앨범의 albumName이 포함되어 있는 것을 확인할 수 있음

→ 각 트랙들을 albumName을 기준으로 딕셔너리를 만들어 그룹핑하면 

이렇게 앨범들을 모아 띄울때 유용하게 쓰일 수 있음 !

 

그래서 앨범 로딩 메소드를 구현해보면 아래와 같다.

   func loadAlbums(tracks: [AVPlayerItem]) -> [Album] {

        let trackList: [Track] = tracks.compactMap { track in track.convertToTrack() }

        let albumDics = Dictionary(grouping: trackList) { track in track.albumName }
        
        var albums: [Album] = []
        for (key, value) in albumDics {
            let title = key
            let tracks = value
            let album = Album(title: title, tracks: tracks)
            albums.append(album)
        }
        
        return albums
    }

- trackList : 현재 tracks는 AVPlayItem의 array이며, 이 array 안에 존재하는 item들의 타입들을 Track으로 변경

- albumDics : albumName(=key)을 기준으로 만든 딕셔너리

   - key : albumName

   - value : trackList 안의 albumName이 같은 모든 tracks들

- albums : album array

 

 

 

4️⃣ loadOtherTodaysTrack()

- 오늘의 트랙이 random하게 선택될 함수

- 아래의 사진과 같이 헤더로 사용될 것

구현코드는 

func loadOtherTodaysTrack(){
    self.todayMusic = self.tracks.randomElement()
}

 

 

 

 

 


 

데이터 구조는 어느정도 완료.

이제, 다음은 이 Track 모델을 통해 데이터를 HomeViewd와 연결시켜 띄우는 것을 해볼 것이다!