EmergeTools / Pow

Delightful SwiftUI effects for your app
https://movingparts.io/pow
MIT License
3.66k stars 153 forks source link

Feature Request: Sound ChangeEffect #11

Closed mergesort closed 1 year ago

mergesort commented 2 years ago

I really love the way that you've included a .haptic ChangeEffect, and was wondering if it'd be possible to do the same for playing a sound.

Right now I have my own homegrown that I expose through the environment like so

@Environment(\.sounds) private var sounds

And it's invoked by calling the playSound method like this.

sounds.playSound(effect: .plinkSound, source: self.preferences.preferredSoundSource)

I'm envisioning a solution that looks much like .changeEffect(.haptic(.success), value: someStateChanged), but instead of .haptic it would be .changeEffect(.sound(.plinkSound), value: someStateChanged).

I'm also including the complete solution if it can be of any help, or provide any more context, please let me know if there's any more information that would be helpful.

public enum SoundEffect {
    case dataAsset(NSDataAsset)
    case file(filename: String, `extension`: String)
}

public final class Sounds {

    public enum Source: String {
        case none
        case headphones
        case speakers
    }

    private var player: AVAudioPlayer?

    public init() {}

    public func playSound(effect: SoundEffect, source: Source) {
        let shouldPlaySound: Bool

#if targetEnvironment(simulator)
        shouldPlaySound = true
#else
        switch source {

        case .none:
            shouldPlaySound = false

        case .headphones:
            shouldPlaySound = AVAudioSession.sharedInstance().currentRoute.outputs.map(\.portType).contains(where: { $0 == .headphones || $0 == .bluetoothA2DP })

        case .speakers:
            shouldPlaySound = true

        }
#endif

        if shouldPlaySound {
            do {
                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
                try AVAudioSession.sharedInstance().setActive(true)

                switch effect {

                case .dataAsset(let dataAsset):
                    self.player = try AVAudioPlayer(data: dataAsset.data)

                case .file(let filename, let fileExtension):
                    guard let url = Bundle.module.url(forResource: filename, withExtension: fileExtension) else { return }
                    self.player = try AVAudioPlayer(contentsOf: url)
                }

                self.player?.play()
            } catch let error {
                print(error.localizedDescription)
            }
        }
    }

}

private enum SoundEnvironmentKey: EnvironmentKey {
    static let defaultValue = Sounds()
}

public extension EnvironmentValues {
    var sounds: Sounds {
        get { self[SoundEnvironmentKey.self] }
        set { self[SoundEnvironmentKey.self] = newValue }
    }
}
robb commented 2 years ago

Yeah, makes sense – we're looking into it.