Search code examples
swiftuipicker

MPMediaPickerController in SwiftUI?


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")

    }
}

Solution

  • 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>) {
        }
    
    }