На 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. Кстати, для аутентификации необходимо иметь активный сертификат разработчика.
Вот что вам нужно сделать в своем профиле разработчика:
- Перейдите в меню «Идентификаторы» в личном кабинете.
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. Я постараюсь опубликовать его в течение следующей недели или двух;)