Search code examples
variablesavaudioplayermemberstring-interpolation

Am I able to use the value of a variable to call a variable within my struct?


I have sounds stored for different events within the struct that the user will be able to change, and was hoping i could send the function a string to select a song for a specific event.

my function call would look like this:

func playSound(audio: Audio, soundSelect: String = "startSound"){

if let path = Bundle.main.path(forResource: audio.\(soundSelect), ofType: audio.soundType){
do {
        audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
        audioPlayer?.play()
    }catch{
        print("ERROR: Could not find and play the sound file!")
    }

and my struct would look something like this:

struct Audio {
  var startSound: String = "happyMusic"
  var endSound: String = "sadMusic"
  var soundType: String = "mp3"
}

as above i tried string interpolation which didn't seem to work I got the error

"Expected member name following '.'

What I expected was for "audio.\(soundSelect)" to be read like "audio.startSound"


Solution

  • You cannot build variable names at runtime because all variable names are evaluated at compile time.

    But if you define the type of the parameter as a KeyPath you are able to address different properties in the struct

    func playSound(audio: Audio, soundSelect: KeyPath<Audio,String> = \.startSound) {
        
        if let url = Bundle.main.url(forResource: audio[keyPath: soundSelect], withExtension: audio.soundType) {
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: url)
                audioPlayer?.play()
            } catch {
                print("ERROR: Could not find and play the sound file!", error)
            }
        }
    }
    

    Notes:

    • If you implement a do - catch block handle (removing the question mark in try?) and print the error rather than just a meaningless string literal.

    • Bundle provides an URL related API which avoids the extra URL(fileURLWithPath call


    A still swiftier syntax is to make the function throw and hand over the error(s) to the caller

    func playSound(audio: Audio, soundSelect: KeyPath<Audio,String> = \.startSound) throws {
        if let url = Bundle.main.url(forResource: audio[keyPath: soundSelect], withExtension: audio.soundType) {
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer?.play()
        } else {
            throw URLError(.badURL)
        }
    }