[UIKit] Spotify Clone: Save Albums

Junyoung Park·2022년 9월 11일
0

UIKit

목록 보기
25/142
post-thumbnail
post-custom-banner

Building Spotify App in Swift 5 & UIKit - Part 25 - Save Albums (Xcode 12, 2021, Swift 5) - App

Spotify Clone: Save Albums

구현 목표


  • 특정 앨범 저장 후 개인 목록에 불러오기
  • 앨범 저장 후 커스텀 노티피케이션 사용, 옵저버를 통해 자동 UI 패치

구현 태스크

  1. LibraryAlbumsViewController: 유저 선택한 앨범을 스포티파이 API로 호출
  2. AlbumViewController: 현재 앨범을 유저의 앨범에 추가
  3. 앨범 추가 시 LibraryAlbumsViewController의 앨범 테이블 뷰를 새롭게 그리는 함수를 호출하도록 옵저버 추가

핵심 코드

import UIKit

class LibraryAlbumsViewController: UIViewController {
    var albums = [Album]()
    
    private let noAlbumsView = ActionLabelView()
    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(SearchResultSubtitleTableViewCell.self, forCellReuseIdentifier: SearchResultSubtitleTableViewCell.identifier)
        tableView.isHidden = true
        return tableView
    }()
    private var observer: NSObjectProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        setLibraryAlbumsViewUI()
        fetchData()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        noAlbumsView.frame = CGRect(x: (view.width - 150) / 2, y: (view.height - 150) / 2, width: 150, height: 150)
        tableView.frame = view.bounds
    }
    
    private func setLibraryAlbumsViewUI() {
        view.backgroundColor = .clear
        view.addSubview(noAlbumsView)
        noAlbumsView.configure(with: ActionLabelViewViewModel(text: "You have no saved albums", actionTitle: "Browse"))
        noAlbumsView.delegate = self
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.dataSource = self
        observer = NotificationCenter.default.addObserver(forName: .albumSavedNotification, object: nil, queue: .main, using: { [weak self] _ in
            guard let self = self else { return }
            self.fetchData()
        })
    }
    
    private func fetchData() {
        albums.removeAll()
        APICaller.shared.getCurrentUserAlbums { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let albums):
                self.albums = albums
                DispatchQueue.main.async {
                    self.updateUI()
                }
            case .failure(let error):
                print(error.localizedDescription)
                break
            }
        }
    }
    
    private func updateUI() {
        if albums.isEmpty {
            noAlbumsView.isHidden = false
            tableView.isHidden = true
        } else {
            tableView.reloadData()
            noAlbumsView.isHidden = true
            tableView.isHidden = false
        }
    }
}

extension LibraryAlbumsViewController: ActionLabelViewDelegate {
    func actionLabelViewDidTapButton(_ actionView: ActionLabelView) {
        tabBarController?.selectedIndex = 0
    }
}

extension LibraryAlbumsViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return albums.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultSubtitleTableViewCell.identifier, for: indexPath) as? SearchResultSubtitleTableViewCell else {
            return UITableViewCell()
        }
        let album = albums[indexPath.row]
        cell.configure(with: SearchResultSubtitleTableViewCellViewModel(title: album.name, subtitle: album.artists.first?.name ?? "-", imageURL: URL(string: album.images.first?.url ?? "")))
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let album = albums[indexPath.row]
        
        let vc = AlbumViewController(album: album)
        vc.navigationItem.largeTitleDisplayMode = .never
        navigationController?.pushViewController(vc, animated: true)
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60
    }
}
  • noAlbumsViewtableView 모두를 뷰에 추가 → fetchData() 함수를 통해 리턴한 결과를 통해 보여줄 뷰를 선택
  • noAlbumView에서는 추가할 앨범을 고르기 위해 탭 인덱스를 곧바로 변경 가능
  • tableView에서는 API로 리턴받은 앨범 데이터를 각 셀로 바인딩
    @objc private func didTapActions() {
        let actionSheet = UIAlertController(title: album.name, message: "Actions", preferredStyle: .actionSheet)
        actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        actionSheet.addAction(UIAlertAction(title: "Save Album", style: .default, handler: { [weak self] _ in
            guard let self = self else { return }
            // Save album
            APICaller.shared.saveAlbum(album: self.album) { success in
                if success {
                    NotificationCenter.default.post(name: .albumSavedNotification, object: nil)
                    print("Saved: \(success)")
                }
            }
        }))
        
        present(actionSheet, animated: true, completion: nil)
    }
  • AlbumViewController 내에서의 우측 네비게이션 바 아이템을 클릭한 경우 실행되는 함수
  • 스포티파이 API를 통해 현재 유저의 앨범에 해당 앨범을 추가
  • 앨범 추가 성공 시 커스텀 노티피케이션 호출
extension Notification.Name {
    static let albumSavedNotification = Notification.Name("albumSavedNotification")
}
  • 앨범 추가 완료 시 해당 노티피케이션 호출
  • LibraryAlbumViewControllerobserver가 해당 노티피케이션을 관찰하고 있기 때문에 fetchData() 함수를 재호출 가능
public func getCurrentUserAlbums(completionHandler: @escaping (Result<[Album], Error>)->()) {
        createRequest(with: URL(string: Constants.baseAPIURL + "/me/albums"), type: .GET) { request in
            URLSession.shared.dataTask(with: request) { data, response, error in
                guard
                    let data = data,
                    error == nil else {
                        completionHandler(.failure(APIError.failedToGetData))
                    return
                }
                
                do {
                    let result = try JSONDecoder().decode(LibraryAlbumsResponse.self, from: data)
                    completionHandler(.success(result.items.map({$0.album})))
                } catch {
                    completionHandler(.failure(error))
                }
            }
            .resume()
        }
    }
    
    public func saveAlbum(album: Album, completionHandler: @escaping (Bool) -> ()) {
        createRequest(with: URL(string: Constants.baseAPIURL + "/me/albums?ids=\(album.id)"), type: .PUT) { request in
            var request = request
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            URLSession.shared.dataTask(with: request) { data, response, error in
                guard
                    let data = data,
                    let httpResponse = response as? HTTPURLResponse,
                    error == nil else {
                    completionHandler(false)
                    return
                }
                let code = httpResponse.statusCode
                print(code)
                completionHandler(true)
            }
            .resume()
        }
    }
  • 스포티파이 API 함수 → 현재 유저가 저장한 앨범을 모두 리턴하는 getCurrentUserAlbums, 특정 앨범을 유저의 저장 앨범 목록에 추가하는 saveAlbum 함수

구현 화면

옵저버를 통해 특정 뷰에서의 활동이 완료되었을 때 특정 함수(뷰를 다시 패치)를 사용하는 방법을 사용해 보았다. 델리게이트 패턴만이 아니라 다양한 방법이 있다는 데 주목!

profile
JUST DO IT
post-custom-banner

0개의 댓글