I am rebuilding one of my apps in SwiftUI but I have hit a snag.
My question is this.. My app is an AudioVisual Test Generator and currently has the ability to select a test song from the user device's music library, and set that song as the test song for the application. Other tests include Left Speaker, Right Speaker, etc, but this feature allows a user to select their own custom Test Song from their device's library, (or it defaults to a default test song.)
My app is currently written in Swift and uses the MPMediaPickerController to select the song and it works just fine. However, I am having a real hard time making the music library picker controller work with my SwiftUI rebuild. Anyone know a solid way to access the user's Music Library in SwiftUI?
I keep getting stuck with objects not conforming to class protocol 'NSObjectProtocol' which leads me to believe there is a more SwiftUI-y way of doing it? Or perhaps I can use SwiftUI for most of my app but transition to a UIView for the song selection?
Here's some code that doesn't work yet and isn's pretty.. I was just throwing everything I could at the problem and planning on refactoring if I got it to work.
//probably importing a few more things than i need.
import UIKit
import SwiftUI
import AVFoundation
import AVKit
import MediaPlayer
class SongPickerController: UIViewControllerRepresentable, MPMediaPickerControllerDelegate {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = context.coordinator
return imagePickerController
}
func makePickerController(context: Context) -> MPMediaPickerController {
var picker = MPMediaPickerController()
picker = MPMediaPickerController(mediaTypes: .anyAudio)
//picker.delegate = self
picker.allowsPickingMultipleItems = false
picker.showsCloudItems = false
picker.prompt = NSLocalizedString(Texts.pickerDetail, comment: Texts.pickerComment)
return picker
}
func songSelectButtonClicked () {
picker = MPMediaPickerController(mediaTypes: .anyAudio)
picker?.delegate = self
picker?.allowsPickingMultipleItems = false
picker?.showsCloudItems = false
picker?.prompt = NSLocalizedString(Texts.pickerDetail, comment: Texts.pickerComment)
self.present(picker!, animated: false, completion: nil)
func updateUIViewController(_ uiViewController: MPMediaPickerController, context: Context) {
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: SongPickerController
init(_ imagePickerController: SongPickerController) {
self.parent = imagePickerController
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)}}}}
struct SongUIView: UIViewController, MPMediaPickerControllerDelegate {
var picker: MPMediaPickerController?
func songSelectButtonClicked () {
picker = MPMediaPickerController(mediaTypes: .anyAudio)
picker?.delegate = self
picker?.allowsPickingMultipleItems = false
picker?.showsCloudItems = false
picker?.prompt = NSLocalizedString(Texts.pickerDetail, comment: Texts.pickerComment)
self.present(picker!, animated: false, completion: nil)
}
func mediaPicker(_ mediaPicker: MPMediaPickerController,
didPickMediaItems mediaItemCollection: MPMediaItemCollection){
let selectedSong = mediaItemCollection.items
if (selectedSong.count) > 0 {
let songItem = selectedSong[0]
let songURL = songItem.value(forProperty: MPMediaItemPropertyAssetURL)
let saveString = "\(songURL!)"
let saveTitle = "\(songItem.title!) by \(songItem.artist!)"
saveDefaultSong(saveString as NSString, title: saveTitle as NSString)
mediaPicker.dismiss(animated: true, completion: nil)
///selectSongButton.setTitle("Custom song assigned!", for: UIControl.State())
}
}
func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
mediaPicker.dismiss(animated: true, completion: nil)
}
func saveDefaultSong (_ name: NSString, title: NSString) {
UserDefaults.standard.set(name, forKey: "Default Song")
UserDefaults.standard.set(title, forKey: "Default Song Title")
}
}
This solution is based almost entirely on Dave's post of Jul 28th. That previous code did not yet have a way of selecting the song and passing it to a View. In the example below it is passing the selected song to an @EnvironmentObject, but it could just as easily be used with a @Binding as Dave suggests.
import SwiftUI
import MediaPlayer
struct MusicPicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var player: AudioPlayer
class Coordinator: NSObject, UINavigationControllerDelegate, MPMediaPickerControllerDelegate {
var parent: MusicPicker
init(_ parent: MusicPicker) {
self.parent = parent
}
func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
let selectedSong = mediaItemCollection.items
if (selectedSong.count) > 0 {
let songItem = selectedSong[0]
parent.setSong(song: songItem)
mediaPicker.dismiss(animated: true, completion: nil)
}
}
}
func setSong(song: MPMediaItem) {
player.setAudioTrack(song: song)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MusicPicker>) -> MPMediaPickerController {
let picker = MPMediaPickerController()
picker.allowsPickingMultipleItems = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: MPMediaPickerController, context: UIViewControllerRepresentableContext<MusicPicker>) {
}
}