На WWDC 2017 Apple анонсировала MusicKit — платформу, помогающую разработчикам создавать приложения, которые позволяют пользователям воспроизводить Apple Music и их локальную музыкальную библиотеку. В отличие от большинства фреймворков, таких как ARKitили CoreML, MusicKit нельзя добавить в код с помощью простой функции import. Скорее, это комбинация Apple Music API, фреймворка StoreKit, фреймворка MediaPlayer и некоторых других веб-технологий.

При разработке приложения RebFit я интегрировал API Apple Music в раздел кардио, чтобы дать пользователям возможность выбирать музыку из своей музыкальной библиотеки Apple из приложения. Примерно так же, как это реализовано в приложении Nike Run Club. Не так много полной информации или руководств, которые научат вас, как реализовать Apple Music API от начала до конца, поэтому я решил помочь вам, мои коллеги-разработчики!

В первой части мы собираемся создать точную копию страницы библиотеки пользователей в приложении Apple Music. Что будет включать в себя запрос и отображение музыкальной библиотеки Apple Music Library (плейлисты и музыка в этой части). Во второй и третьей части мы узнаем, как делать такие запросы, как «Жанры», «Исполнители» и т. д. из пользовательской библиотеки, так что следите за обновлениями!

В качестве небольшого бонуса я также покажу вам, как сделать анимированное пользовательское всплывающее оповещение о запросе, похожее на вид.

Во-первых, нам нужно будет аутентифицировать MusicKit, чтобы получить доступ к Apple Music API.

В течение довольно долгого времени интеграция Apple Music API в ваше приложение была суетой (довольно запутанной, но не сложной). Давайте разберем старый процесс аутентификации:

  • Зарегистрируйте новый ключ с включенным сервисом MusicKit.
  • Используйте этот ключ для создания веб-токена JSON.
  • Отправлять заголовок с JWT для каждого запроса Apple Music API.
  • Для доступа к библиотеке пользователя также сгенерируйте токен пользователя.

Кажется, что достаточно много работы только для того, чтобы пройти аутентификацию, не так ли? Но, к счастью, Apple упростила процесс, и теперь это так же просто, как поставить галочку в своей учетной записи разработчика Apple. Кстати, для аутентификации необходимо иметь активный сертификат разработчика.

Вот что вам нужно сделать в своем профиле разработчика:

  1. Перейдите в меню «Идентификаторы» в личном кабинете.

2. Выберите идентификатор вашего приложения из списка и перейдите в раздел Службы приложений. Оттуда выберите галочку MusicKit.

Вот и все! Нет необходимости генерировать токен разработчика или пользователя. Он генерируется «под капотом».

Info.plist

Чтобы запросить у пользователей разрешение на доступ к их медиа-сервисам, нам нужно будет поместить свойство Information в Info.plist.

Теперь приступим к кодированию!

Давайте начнем с создания запросов к API и сохранения данных. Здесь мы собираемся установить пользовательский popUpRequestView. Во-первых, давайте установим элементы нашего popUpView.

Чтобы затемнить элементы viewController за нашим popUpView, нам нужно установить два затемняющих представления. Первый — затемнить элементы представления, а второй — затемнить большой заголовок navigationControllers. Но зачем нам нужно размещать еще одно представление поверх панели навигации? Это связано с тем, что панель навигации располагается поверх любого представления, добавленного в UIViewController.

let activityIndicator: UIActivityIndicatorView = {
let view = UIActivityIndicatorView()
view.hidesWhenStopped = true
view.style = UIActivityIndicatorView.Style.large
view.backgroundColor = .clear
view.layer.cornerRadius = 5
view.color = .black
return view
}()
let dimmingBgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.3)
view.clipsToBounds = true
return view
}()
let dimmingNavView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.3)
view.clipsToBounds = true
return view
}()

Далее идет containerView для popUpView и closeButton. Причина помещения popUpView в другой containerView заключается в том, что мы собираемся использовать анимацию «сжатия», когда popUpView закрывается, а также кнопка закрытия расположена поверх popUpView и немного за его рамкой.

let containerForPopUpRequestView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.clipsToBounds = true
return view
}()
let popUpRequestView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 7.5
view.clipsToBounds = true
return view
}()
lazy var closeRequestButton: UIButton = { //declare as lazy var to prevent 'self' warning
let button = UIButton()
button.backgroundColor = .white
button.setImage(UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15.0, weight: .semibold)), for: .normal)
button.tintColor = .black
button.addTarget(self, action: #selector(closeRequest), for: .touchUpInside)
button.clipsToBounds = false
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOpacity = 0.5 //Play with this setting to adjust intensity of the shadows (Changes the opacity of color)
button.layer.shadowRadius = 3 //Play with this setting to adjust intensity of the shadows (Increases the shadow)
button.layer.shadowOffset = CGSize(width: 0, height: 0)
return button
}()

В более новой версии Xcode (начиная с 14.0, если я не ошибаюсь) при объявлении UIButton как константы «let» выдаст вам предупреждение (что-то вроде «self» относится к методу «MyLibrary.self», что может быть неожиданным). Это происходит потому, что мы создаем переменную\константу и присваиваем этой переменной результат замыкания. Код этого замыкания выполняется до того, как появятся какие-либо экземпляры нашего класса (до того, как появится self для ссылки). Self не существует во время выполнения кода. Когда мы объявляем ленивую переменную, она создается только при первой ссылке на нее.

Следующее, что нужно сделать, это настроить элементы внутри popUpView.

let requestImageView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = .clear
imageView.clipsToBounds = true
imageView.contentMode = .center
imageView.image = UIImage(systemName: "music.note", withConfiguration: UIImage.SymbolConfiguration(pointSize: 150, weight: .medium))
imageView.tintColor = .systemPink
return imageView
}()
let requestTitleLabel: UILabel = {
let label = UILabel()
label.backgroundColor = .clear
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 19.0, weight: .bold)
label.numberOfLines = 3
label.lineBreakMode = .byTruncatingMiddle
label.textAlignment = .center
label.clipsToBounds = false
label.layer.shadowColor = UIColor.black.cgColor
label.layer.shadowOpacity = 0.3 //Play with this setting to adjust intensity of the shadows (Changes the opacity of color)
label.layer.shadowRadius = 2.5 //Play with this setting to adjust intensity of the shadows (Increases the shadow)
label.layer.shadowOffset = CGSize(width: 0, height: 0)
label.text = "'MyLibrary' needs access to Apple Music to let you play your music"
return label
}()
lazy var allowAccessButton: UIButton = { //declare as lazy var to prevent 'self' warning
var configuration = UIButton.Configuration.filled()
configuration.buttonSize = .large
configuration.baseBackgroundColor = .systemPink //rgb(252, 60, 68)
configuration.baseForegroundColor = .white
configuration.title = "ALLOW ACCESS"
let button = UIButton(configuration: configuration)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.lightGray, for: .highlighted)
button.titleLabel?.textAlignment = .center
button.addTarget(self, action: #selector(allowAccess), for: .touchUpInside) //Sends user to apps settings
button.clipsToBounds = false
button.layer.shadowColor = UIColor.systemPink.cgColor
button.layer.shadowOpacity = 0.75 //Play with this setting to adjust intensity of the shadows (Changes the opacity of color)
button.layer.shadowRadius = 4 //Play with this setting to adjust intensity of the shadows (Increases the shadow)
button.layer.shadowOffset = CGSize(width: 0, height: 0)
return button
}()
lazy var notNowButton: UIButton = { //declare as lazy var to prevent 'self' warning
var configuration = UIButton.Configuration.filled()
configuration.buttonSize = .large
configuration.baseBackgroundColor = .lightGray.withAlphaComponent(0.75)
configuration.baseForegroundColor = .darkGray
configuration.title = "NOT NOW"
let button = UIButton(configuration: configuration)
button.setTitleColor(.darkGray, for: .normal)
button.setTitleColor(.lightGray, for: .highlighted)
button.titleLabel?.textAlignment = .center
button.addTarget(self, action: #selector(closeRequest), for: .touchUpInside) //Sends user to apps settings
button.clipsToBounds = true
return button
}()

Далее мы установим функцию hideRequestElements(), чтобы скрыть все элементы popUpView, когда они отсутствуют.

public func hideRequestElements(_ hide: Bool) {
self.dimmingBgView.isHidden = hide
self.dimmingNavView.isHidden = hide
self.containerForPopUpRequestView.isHidden = hide
self.popUpRequestView.isHidden = hide
self.closeRequestButton.isHidden = hide
self.requestImageView.isHidden = hide
self.requestTitleLabel.isHidden = hide
self.notNowButton.isHidden = hide
self.allowAccessButton.isHidden = hide
}

Теперь пришло время добавить подпредставления и ограничения для всплывающего окна.

Мы устанавливаем ограничения в функции, чтобы наш viewDidLoad оставался чистым (я советую новичкам всегда поддерживать его как можно более чистым. Он просто чувствует себя намного лучше и выглядит намного более профессионально и организованно).

func setUpRequestView() {
navigationController?.navigationBar.addSubview(dimmingNavView)
view.addSubview(dimmingBgView)
view.addSubview(containerForPopUpRequestView)
containerForPopUpRequestView.addSubview(popUpRequestView)
containerForPopUpRequestView.addSubview(closeRequestButton)
popUpRequestView.addSubview(requestImageView)
popUpRequestView.addSubview(requestTitleLabel)
popUpRequestView.addSubview(notNowButton)
popUpRequestView.addSubview(allowAccessButton)
dimmingNavView.translatesAutoresizingMaskIntoConstraints = false
dimmingNavView.topAnchor.constraint(equalTo: (navigationController?.navigationBar.topAnchor)!).isActive = true
dimmingNavView.bottomAnchor.constraint(equalTo: (navigationController?.navigationBar.bottomAnchor)!).isActive = true
dimmingNavView.leadingAnchor.constraint(equalTo: (navigationController?.navigationBar.leadingAnchor)!).isActive = true
dimmingNavView.trailingAnchor.constraint(equalTo: (navigationController?.navigationBar.trailingAnchor)!).isActive = true
dimmingBgView.translatesAutoresizingMaskIntoConstraints = false
dimmingBgView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
dimmingBgView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
dimmingBgView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
dimmingBgView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
containerForPopUpRequestView.translatesAutoresizingMaskIntoConstraints = false
containerForPopUpRequestView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
containerForPopUpRequestView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
containerForPopUpRequestView.heightAnchor.constraint(equalToConstant: self.view.frame.height / 1.5).isActive = true
containerForPopUpRequestView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
popUpRequestView.translatesAutoresizingMaskIntoConstraints = false
popUpRequestView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25).isActive = true
popUpRequestView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -25).isActive = true
popUpRequestView.heightAnchor.constraint(equalToConstant: self.view.frame.height / 1.75).isActive = true
popUpRequestView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
closeRequestButton.translatesAutoresizingMaskIntoConstraints = false
closeRequestButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
closeRequestButton.widthAnchor.constraint(equalToConstant: 40).isActive = true
closeRequestButton.rightAnchor.constraint(equalTo: popUpRequestView.rightAnchor, constant: 15).isActive = true
closeRequestButton.topAnchor.constraint(equalTo: popUpRequestView.topAnchor, constant: -15).isActive = true
closeRequestButton.layer.cornerRadius = 20
requestImageView.translatesAutoresizingMaskIntoConstraints = false
requestImageView.topAnchor.constraint(equalTo: popUpRequestView.topAnchor, constant: 25).isActive = true
requestImageView.leadingAnchor.constraint(equalTo: popUpRequestView.leadingAnchor, constant: 25).isActive = true
requestImageView.trailingAnchor.constraint(equalTo: popUpRequestView.trailingAnchor, constant: -25).isActive = true
requestImageView.bottomAnchor.constraint(equalTo: popUpRequestView.centerYAnchor, constant: -20).isActive = true
requestTitleLabel.translatesAutoresizingMaskIntoConstraints = false
requestTitleLabel.topAnchor.constraint(equalTo: requestImageView.bottomAnchor, constant: 20).isActive = true
requestTitleLabel.leadingAnchor.constraint(equalTo: popUpRequestView.leadingAnchor, constant: 30).isActive = true
requestTitleLabel.trailingAnchor.constraint(equalTo: popUpRequestView.trailingAnchor, constant: -30).isActive = true
requestTitleLabel.sizeToFit()
notNowButton.translatesAutoresizingMaskIntoConstraints = false
notNowButton.bottomAnchor.constraint(equalTo: popUpRequestView.bottomAnchor, constant: -20).isActive = true
notNowButton.leadingAnchor.constraint(equalTo: popUpRequestView.leadingAnchor, constant: 20).isActive = true
notNowButton.trailingAnchor.constraint(equalTo: popUpRequestView.trailingAnchor, constant: -20).isActive = true
notNowButton.heightAnchor.constraint(equalToConstant: 55).isActive = true
allowAccessButton.translatesAutoresizingMaskIntoConstraints = false
allowAccessButton.bottomAnchor.constraint(equalTo: notNowButton.topAnchor, constant: -10).isActive = true
allowAccessButton.leadingAnchor.constraint(equalTo: popUpRequestView.leadingAnchor, constant: 20).isActive = true
allowAccessButton.trailingAnchor.constraint(equalTo: popUpRequestView.trailingAnchor, constant: -20).isActive = true
allowAccessButton.heightAnchor.constraint(equalToConstant: 55).isActive = true
}

Последними фрагментами кода для popUpView будут функции showPopUpRequestView(), closeRequest() и allowAccess(). AllowAccess либо покажет предупреждение о запросе по умолчанию, если статус авторизации еще не определен .notDetermined (это означает, что мы еще не запрашивали разрешение), либо отправит пользователя в настройки приложений, чтобы разрешить доступ к медиа-сервису.

func showPopUpRequestView() {
setUpRequestView()
hideRequestElements(false)
containerForPopUpRequestView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
dimmingNavView.alpha = 0
dimmingBgView.alpha = 0
popUpRequestView.alpha = 0
closeRequestButton.alpha = 0
UIView.animate(withDuration: 0.2) { [self] in
dimmingNavView.alpha = 1
dimmingBgView.alpha = 1
popUpRequestView.alpha = 1
closeRequestButton.alpha = 1
containerForPopUpRequestView.transform = CGAffineTransform.identity
}
}
@objc func closePopUpRequestView() {
UIView.animate(withDuration: 0.2, animations: { [self] in
containerForPopUpRequestView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
dimmingNavView.alpha = 0
popUpRequestView.alpha = 0
dimmingBgView.alpha = 0
closeRequestButton.alpha = 0
}) { (success: Bool) in
self.navigationController?.navigationBar.isHidden = false
self.hideRequestElements(true)
self.dimmingBgView.removeFromSuperview()
self.containerForPopUpRequestView.removeFromSuperview()
}
}
@objc func allowAccess() {
if SKCloudServiceController.authorizationStatus() == .notDetermined {
SKCloudServiceController.requestAuthorization { _ in
self.checkIfAppleMusicIsAvailable()
}
} else if SKCloudServiceController.authorizationStatus() == .restricted {
//Sending user to apps settings
if let appSettings = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings)
}
} else if SKCloudServiceController.authorizationStatus() == .denied {
//Sending user to apps settings
if let appSettings = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings)
  
}
}
}

Теперь, наконец, пришло время сделать запрос API для списков воспроизведения пользователей. Мы настроим функцию с именем checkIfAppleMusicIsAvailable(), которая проверит статус авторизации через SKCloudServiceController.authorizationStatus() и начнет извлекать списки воспроизведения, если доступ предоставлен или представит popUpRequestView.

Во-первых, объявите serviceController в своем классе, который будет иметь значение SKCloudServiceController(), и импортируйте StoreKit и MusicKit. Также установите структуры LibraryElementsStructure и PlaylistWithMusicStructure, которые мы будем использовать для установки нашей коллекции.

import StoreKit
import MusicKit
let serviceController = SKCloudServiceController()
struct LibraryElementsStructure {
var title: String
var icon: String
}
struct PlaylistWithMusicStructure: MusicItem {
var id: MusicItemID
var Playlist: Playlist
var Tracks: [Song]
}

Функция showAppleMusicSignup() срабатывает, если у пользователя нет учетной записи Apple Music, и представляет SKCloudServiceSetupViewController. Этот viewController предложит пользователю купить подписку на музыку Apple. Чтобы проверить, есть ли у пользователя учетная запись Apple Music, мы используем функцию SKCloudServiceController.requestCapabilities(), которая возвращает нам массив SKCloudServiceCapabilities.

Поместите следующий код в расширение SKCloudServiceSetupViewControllerDelegate

@objc func showAppleMusicSignup() {
let vc = SKCloudServiceSetupViewController()
vc.delegate = self
let options: [SKCloudServiceSetupOptionsKey: Any] = [.action: SKCloudServiceSetupAction.subscribe, .messageIdentifier: SKCloudServiceSetupMessageIdentifier.playMusic]
vc.load(options: options) { success, error in
if success {
self.present(vc, animated: true)
}
}
}
func checkIfAppleMusicIsAvailable() {
if SKCloudServiceController.authorizationStatus() == .authorized {
self.view.addSubview(self.activityIndicator)
self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false
self.activityIndicator.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
self.activityIndicator.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.activityIndicator.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.activityIndicator.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
self.activityIndicator.startAnimating()
serviceController.requestCapabilities { capabilities, error in
DispatchQueue.global(qos: .background).async {
if capabilities.contains(.musicCatalogPlayback) {
// User has Apple Music account
self.appleMusicFetchStorefrontRegion()
} else if capabilities.contains(.musicCatalogSubscriptionEligible) {
// User can sign up to Apple Music
self.activityIndicator.stopAnimating()
self.showAppleMusicSignup()
}
}
}
} else if SKCloudServiceController.authorizationStatus() == .denied {
self.showPopUpRequestView()
} else if SKCloudServiceController.authorizationStatus() == .notDetermined {
self.showPopUpRequestView()
} else if SKCloudServiceController.authorizationStatus() == .restricted {
self.showPopUpRequestView()
} else {}
}

Теперь, когда мы авторизованы и знаем, что у пользователя есть учетная запись, мы можем перейти к запросу пользователей StorefrontRegion. StorefrontRegion — это код текущего местоположения пользователя. Причина, по которой нам нужен storefrontRegion, заключается в том, что не все исполнители и музыка разрешены во всех странах :(

Запрос serviceController.requestStorefrontIdentifier() возвращает storefrontId, который имеет тип String и имеет длину 9 цифр, но на самом деле нам нужно только 5, поэтому мы собираемся обрезать его с помощью функции String prefix().

func appleMusicFetchStorefrontRegion() {
serviceController.requestStorefrontIdentifier { storefrontId, error in
DispatchQueue.global(qos: .background).async {
guard error == nil else {
print("An error occured. Handle it here.")
self.activityIndicator.stopAnimating()
self.checkIfAppleMusicIsAvailable()
return
}
guard let storefrontId = storefrontId else {
print("Handle the error - the callback didn't contain a storefront ID.")
self.activityIndicator.stopAnimating()
self.checkIfAppleMusicIsAvailable()
return
}
let trimmedId = storefrontId.prefix(5)
self.storeFrontId = String(trimmedId)
self.appleMusicFetchUsersPlaylists(storeFrontId: String(trimmedId))
print("Success! The Storefront ID fetched was: \(trimmedId)")
}
}
}

Прежде чем мы перейдем к части «PlaylistFetching», давайте установим две переменные playlistsAreFetched и numberOfPlaylists, которые мы собираемся использовать в следующей части при создании музыкального проигрывателя. Следите за обновлениями;)

var storeFrontId: String? = ""
var fetchedPlaylists: [PlaylistWithMusicStructure] = []
var playlistsAreFetched: Bool = false
var numberOfPlaylists: Int = 0 {
didSet {
playlistsAreFetched = true
}
}

А вот и самая интересная часть этого урока😂

func appleMusicFetchUsersPlaylists(storeFrontId: String) {
fetchedPlaylists.removeAll()
Task {
if let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists?") {
do {
let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
let playlistsResponse = try await dataRequest.response()
let decoder = JSONDecoder()
let playlists = try? decoder.decode(MusicItemCollection<Playlist>.self, from: playlistsResponse.data)
for playlist in playlists! {
appleMusicFetchMusicFromPlaylist(playlistId: playlist.id.rawValue, storeFrontId: storeFrontId, playlist: playlist, lastPlaylistsId: (playlists?.last!.id)!.rawValue, numberOfPlaylists: playlists!.count)
}
self.activityIndicator.stopAnimating()
self.numberOfPlaylists = playlists?.count ?? 0
self.usersPlaylistCollectionView.reloadData()
} catch { print("Error Occured When fetching users playlists") }
}
}
}

Давайте немного разберем, что здесь происходит. Чтобы получить списки воспроизведения пользователей, мы выполняем MusicDataRequest, который извлекает данные из конечной точки Apple Music API: https://api.music.apple.com/v1/me/library/playlists?. Apple предоставила нам все конечные точки, которые нам когда-либо могут понадобиться, которые вы можете найти здесь. После получения ответа от конечной точки мы используем декодер JSON, потому что ответ приходит в виде необработанного JSON. Мы декодируем его, используя структуру списка воспроизведения MusicItemCollection, которая имеет набор свойств (id, artwork, curatorName, isChart, kind и т. д.). Вы можете более подробно узнать, что они означают, в документации из xcode.

После получения списков воспроизведения мы вызовем функцию appleMusicFetchMusicFromPlaylist() для извлечения музыкального содержимого списка воспроизведения.Во второй части руководства я покажу вам, как настроить музыкальный проигрыватель и воспроизвести музыку.

func appleMusicFetchMusicFromPlaylist(playlistId: String, storeFrontId: String, playlist: Playlist, lastPlaylistsId: String, numberOfPlaylists: Int) {
Task {
var playlistTracksRequestURLComponents = URLComponents()
playlistTracksRequestURLComponents.scheme = "https"
playlistTracksRequestURLComponents.host = "api.music.apple.com"
playlistTracksRequestURLComponents.path = "/v1/me/library/playlists/\(playlistId)/tracks"
playlistTracksRequestURLComponents.queryItems = [URLQueryItem(name: "include", value: "catalog")]
do {
let playlistTracksRequestURL = playlistTracksRequestURLComponents.url!
let playlistTracksRequest = MusicDataRequest(urlRequest: URLRequest(url: playlistTracksRequestURL))
let playlistTracksResponse = try await playlistTracksRequest.response()
let decoder = JSONDecoder()
let playlistTracks = try decoder.decode(MusicItemCollection<Song>.self, from: playlistTracksResponse.data)
var tracks: [Song] = []
for track in playlistTracks {
tracks.append(track)
}
DispatchQueue.main.async { [self] in
fetchedPlaylists.append(PlaylistWithMusicStructure(id: playlist.id, Playlist: playlist, Tracks: tracks))
self.usersPlaylistCollectionView.reloadData()
}
} catch { print("Error Occured When fetching users playlists") }
}
}

Процесс здесь такой же, как и при получении списка воспроизведения, но на этот раз нам нужно будет указать playlistId и добавить элементы запроса ([URLQueryItem (имя: «включить», значение: «каталог»)]), чтобы получить каталог треков.

Наконец, когда у нас есть все данные, мы можем настроить collectionView для их представления. Сначала добавьте переменные collectionView, его ячейки и идентификаторы заголовков.

var usersPlaylistCollectionView: UICollectionView!
var usersPlaylistCollectionViewCellID = "PlaylistCell"
let headerId = "playlistsHeader"
let libraryElementsCellId = "libraryElementsCellId"

Добавьте этот код в свой viewDidLoad()

view.backgroundColor = .white
navigationItem.title = "Playlists"
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .automatic
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.backgroundColor = .white
setPlaylistsCollectionView()
checkIfAppleMusicIsAvailable()

Создайте функцию для установки collectionView

func setPlaylistsCollectionView() {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
usersPlaylistCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
usersPlaylistCollectionView.register(libraryElementsCell.self, forCellWithReuseIdentifier: libraryElementsCellId)
usersPlaylistCollectionView.register(usersPlaylistCollectionViewCell.self, forCellWithReuseIdentifier: usersPlaylistCollectionViewCellID)
usersPlaylistCollectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId)
usersPlaylistCollectionView.showsVerticalScrollIndicator = true
usersPlaylistCollectionView.showsHorizontalScrollIndicator = false
usersPlaylistCollectionView.backgroundColor = UIColor.clear
usersPlaylistCollectionView.indicatorStyle = .default
usersPlaylistCollectionView.isPagingEnabled = false
usersPlaylistCollectionView.bounces = true
usersPlaylistCollectionView.delegate = self
usersPlaylistCollectionView.dataSource = self
usersPlaylistCollectionView.contentInset = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0)
view.addSubview(usersPlaylistCollectionView)
usersPlaylistCollectionView.translatesAutoresizingMaskIntoConstraints = false
usersPlaylistCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
usersPlaylistCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
usersPlaylistCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
usersPlaylistCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
}

Добавьте UICollectionViewDelegateFlowLayout и UICollectionViewDataSource в качестве расширения для вашего ViewController.

extension MyLibrary: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var numberOfItems: Int? = nil
if section == 0 {
numberOfItems = libraryElementsArray.count
} else if section == 1 {
numberOfItems = fetchedPlaylists.count
}
return numberOfItems ?? fetchedPlaylists.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
var insets: UIEdgeInsets? = nil
if section == 0 {
insets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0)
} else if section == 1 {
insets = UIEdgeInsets(top: 15, left: 15, bottom: 0, right: 15)
}
return insets ?? UIEdgeInsets(top: 15, left: 15, bottom: 0, right: 15)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
var headerSize: CGSize? = nil
if section == 0 {
headerSize = CGSize(width: collectionView.bounds.width, height: 0)
} else if section == 1 {
headerSize = CGSize(width: collectionView.bounds.width, height: 60)
}
return headerSize ?? CGSize(width: collectionView.bounds.width, height: 40)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId, for: indexPath) as! HeaderView
if indexPath.section == 0 {
headerView.titleLabel.removeFromSuperview()
}
return headerView
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: libraryElementsCellId, for: indexPath) as! libraryElementsCell
cell.elementsTitleLabel.text = libraryElementsArray[indexPath.row].title
cell.elementsIcon.image = UIImage(systemName: libraryElementsArray[indexPath.row].icon, withConfiguration: UIImage.SymbolConfiguration(pointSize: 24.0, weight: .medium))
return cell
} else if indexPath.section == 1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: usersPlaylistCollectionViewCellID, for: indexPath) as! usersPlaylistCollectionViewCell
cell.playlistNameLabel.text = fetchedPlaylists.reversed()[indexPath.row].Playlist.name
DispatchQueue.main.async { [self] in
URLSession.shared.dataTask(with: (fetchedPlaylists.reversed()[indexPath.row].Tracks.first?.artwork?.url(width: 500, height: 500))!) { (data, response, error) in
//Download hit error returning out
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
cell.imageViewMusic.image = UIImage(data: data!)
}
}.resume()
}
return cell
}
return UICollectionViewCell()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var cellSize: CGSize? = nil
if indexPath.section == 0 {
cellSize = CGSize(width: (self.view.frame.width - 20), height: 45)
} else if indexPath.section == 1 {
cellSize = CGSize(width: (self.view.frame.width / 2) - 20, height: (self.view.frame.width / 2) + 50)
}
return cellSize ?? CGSize(width: (self.view.frame.width / 2) - 20, height: (self.view.frame.width / 2) + 50)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//MARK: -ActionForCustomCells
if let _ = collectionView.cellForItem(at: indexPath) as? usersPlaylistCollectionViewCell {
} else if let _ = collectionView.cellForItem(at: indexPath) as? libraryElementsCell {
}
}
}

Я думаю, что процесс настройки collectionView Delegate довольно прост, но единственное, что я хотел бы объяснить, это URL dataTask. Мы используем его для загрузки обложки обложки (изображения) для наших плейлистов, так как она содержится в виде URL-ссылки в структуре плейлиста.

Наконец, нам нужно будет установить headerView как класс UICollectionReusableView, libraryElements и usersPlaylistCollectionViewCell.

Код для заголовка

import Foundation
import UIKit
class libraryElementsCell: UICollectionViewCell {
let elementsIcon: UIImageView = {
let image = UIImageView()
image.backgroundColor = UIColor.white
image.clipsToBounds = true
image.tintColor = .systemPink
image.contentMode = .center
return image
}()
let elementsTitleLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.clear
label.textColor = UIColor.black
label.textAlignment = .left
label.numberOfLines = 1
label.text = “Artists”
label.font = UIFont.systemFont(ofSize: 24.0, weight: .regular)
return label
}()
let underline: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.clipsToBounds = true
return view
}()
let arrowIcon: UIImageView = {
let image = UIImageView()
image.backgroundColor = UIColor.white
image.clipsToBounds = true
image.contentMode = .center
image.image = UIImage(systemName: “chevron.right”, withConfiguration: UIImage.SymbolConfiguration(pointSize: 17.0, weight: .bold))
image.tintColor = UIColor.lightGray
return image
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
addSubview(elementsIcon)
addSubview(elementsTitleLabel)
addSubview(arrowIcon)
addSubview(underline)
elementsIcon.translatesAutoresizingMaskIntoConstraints = false
elementsTitleLabel.translatesAutoresizingMaskIntoConstraints = false
arrowIcon.translatesAutoresizingMaskIntoConstraints = false
underline.translatesAutoresizingMaskIntoConstraints = false
setConstraints()
}
func setConstraints() {
elementsIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -5).isActive = true
elementsIcon.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
elementsIcon.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
elementsIcon.widthAnchor.constraint(equalToConstant: self.frame.height).isActive = true
arrowIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -5).isActive = true
arrowIcon.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -15).isActive = true
arrowIcon.widthAnchor.constraint(equalToConstant: self.frame.height — 5).isActive = true
arrowIcon.heightAnchor.constraint(equalToConstant: self.frame.height — 5).isActive = true
underline.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
underline.leadingAnchor.constraint(equalTo: elementsIcon.trailingAnchor, constant: 5).isActive = true
underline.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
underline.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
elementsTitleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -5).isActive = true
elementsTitleLabel.leadingAnchor.constraint(equalTo: elementsIcon.trailingAnchor, constant: 5).isActive = true
elementsTitleLabel.trailingAnchor.constraint(equalTo: arrowIcon.leadingAnchor, constant: -5).isActive = true
elementsTitleLabel.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
}
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}

Код для библиотеки ElementsCell

import Foundation
import UIKit
class libraryElementsCell: UICollectionViewCell {
let elementsIcon: UIImageView = {
let image = UIImageView()
image.backgroundColor = UIColor.white
image.clipsToBounds = true
image.tintColor = .systemPink
image.contentMode = .center
return image
}()
let elementsTitleLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.clear
label.textColor = UIColor.black
label.textAlignment = .left
label.numberOfLines = 1
label.text = “Artists”
label.font = UIFont.systemFont(ofSize: 24.0, weight: .regular)
return label
}()
let underline: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.clipsToBounds = true
return view
}()
let arrowIcon: UIImageView = {
let image = UIImageView()
image.backgroundColor = UIColor.white
image.clipsToBounds = true
image.contentMode = .center
image.image = UIImage(systemName: “chevron.right”, withConfiguration: UIImage.SymbolConfiguration(pointSize: 17.0, weight: .bold))
image.tintColor = UIColor.lightGray
return image
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
addSubview(elementsIcon)
addSubview(elementsTitleLabel)
addSubview(arrowIcon)
addSubview(underline)
elementsIcon.translatesAutoresizingMaskIntoConstraints = false
elementsTitleLabel.translatesAutoresizingMaskIntoConstraints = false
arrowIcon.translatesAutoresizingMaskIntoConstraints = false
underline.translatesAutoresizingMaskIntoConstraints = false
setConstraints()
}
func setConstraints() {
elementsIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -5).isActive = true
elementsIcon.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
elementsIcon.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
elementsIcon.widthAnchor.constraint(equalToConstant: self.frame.height).isActive = true
arrowIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -5).isActive = true
arrowIcon.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -15).isActive = true
arrowIcon.widthAnchor.constraint(equalToConstant: self.frame.height — 5).isActive = true
arrowIcon.heightAnchor.constraint(equalToConstant: self.frame.height — 5).isActive = true
underline.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
underline.leadingAnchor.constraint(equalTo: elementsIcon.trailingAnchor, constant: 5).isActive = true
underline.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
underline.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
elementsTitleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -5).isActive = true
elementsTitleLabel.leadingAnchor.constraint(equalTo: elementsIcon.trailingAnchor, constant: 5).isActive = true
elementsTitleLabel.trailingAnchor.constraint(equalTo: arrowIcon.leadingAnchor, constant: -5).isActive = true
elementsTitleLabel.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
}
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}

Код для пользователейPlaylistCollectionViewCell

let imageViewMusic: UIImageView = {
let image = UIImageView()
image.backgroundColor = UIColor.white
image.clipsToBounds = true
image.image = UIImage(named: “music cover”)
image.layer.cornerRadius = 7.5
return image
}()
let playlistNameLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.clear
label.textColor = UIColor.black
label.textAlignment = .left
label.numberOfLines = 1
label.text = “Fake”
label.font = UIFont.systemFont(ofSize: 16.5, weight: .regular)
label.clipsToBounds = false
label.layer.shadowColor = UIColor.black.withAlphaComponent(1.0).cgColor
label.layer.shadowOpacity = 1.0
label.layer.shadowRadius = 6
label.layer.shadowOffset = CGSize(width: 0, height: 0)
return label
}()
let playlistsCuratorName: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.clear
label.textColor = UIColor.systemGray
label.textAlignment = .left
label.numberOfLines = 1
label.text = “The Tech Thieves”
label.font = UIFont.systemFont(ofSize: 16.5, weight: .regular)
label.clipsToBounds = false
label.layer.shadowColor = UIColor.black.withAlphaComponent(1.0).cgColor
label.layer.shadowOpacity = 0.65
label.layer.shadowRadius = 6
label.layer.shadowOffset = CGSize(width: 0, height: 0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
addSubview(imageViewMusic)
addSubview(playlistNameLabel)
addSubview(playlistsCuratorName)
imageViewMusic.translatesAutoresizingMaskIntoConstraints = false
playlistNameLabel.translatesAutoresizingMaskIntoConstraints = false
playlistsCuratorName.translatesAutoresizingMaskIntoConstraints = false
setConstraints()
}
func setConstraints() {
imageViewMusic.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5).isActive = true
imageViewMusic.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5).isActive = true
imageViewMusic.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
imageViewMusic.heightAnchor.constraint(equalToConstant: self.frame.width — 10).isActive = true
playlistNameLabel.topAnchor.constraint(equalTo: imageViewMusic.bottomAnchor, constant: 10).isActive = true
playlistNameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5).isActive = true
playlistNameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5).isActive = true
playlistNameLabel.sizeToFit()
playlistsCuratorName.topAnchor.constraint(equalTo: playlistNameLabel.bottomAnchor, constant: 3.5).isActive = true
playlistsCuratorName.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5).isActive = true
playlistsCuratorName.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5).isActive = true
playlistsCuratorName.sizeToFit()
}
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}

Здесь вы найдете исходный код этого проекта: https://github.com/AisultanAskarov/MyPlaylists

Вот и все для первой части! Во второй части мы узнаем, как запрашивать все элементы пользовательских библиотек и представлять их, а также как воспроизводить музыку с помощью MpMusicPlayerController. Я постараюсь опубликовать его в течение следующей недели или двух;)