AudioManager
is a Swift package that provides a modular and easy-to-use interface for implementing audio feedback in your applications. It integrates seamlessly with SwiftUI, enabling you to enhance user experience through customisable audible feedback.
- Custom Audio: Easily define and trigger audio feedback.
- SwiftUI Extensions: Add audio feedback to SwiftUI views in a declarative way.
- User Preferences: Enable or disable audio feedback based on user settings through simple configuration.
- Custom Audio Patterns: Extend and create your own audio feedback.
Add AudioManager
to your Swift project using Swift Package Manager.
dependencies: [
.package(url: "https://github.com/markbattistella/AudioManager", from: "1.0.0")
]
There are three type of ways to use the AudioManager
:
- Static Action: This is the simplest method, used when you want to trigger audio feedback for a particular state change. It's consistent and straightforward — ideal when the audio feedback needs to occur every time a specific condition (like a state variable changing) is met.
- Static Action with Condition: This approach adds more control compared to the standard static action. Here, you specify a set of conditions to determine when the audio feedback should be triggered. This allows you to handle more nuanced scenarios — such as only playing feedback when transitioning from one specific state to another, while ignoring others.
- Dynamic Action: The most flexible of the three, dynamic actions let you determine the type of audio feedback based on the old and new values during a state change. This means you can implement complex feedback behaviours that respond differently based on how the state transitions, allowing for a more dynamic and tailored user experience.
Note
Though there is the .system()
Playback
call, it is only available on iOS as that is where those in-build sounds live. All other uses can utilise the .custom()
option.
The static action format allows you to trigger audio feedback consistently and simply. In the example below, audio feedback is triggered whenever the isSuccess
state changes.
@State private var isSuccess: Bool = false
Button("isSuccess: \(isSuccess)") {
isSuccess.toggle()
}
.audioFeedback(.system(.ui(.tock)), trigger: isSuccess)
You can also use a condition to control when the audio feedback should be triggered, allowing for more focused control over when feedback occurs.
enum Phase { case inactive, active, completed }
@State private var phase: Phase = .inactive
Button("Update phase") {
switch phase {
case .inactive: phase = .active
case .active: phase = .completed
case .completed: phase = .inactive
}
}
.audioFeedback(.system(.ui(.tink)), trigger: phase) { oldValue, newValue in
oldValue != .completed && newValue == .completed
}
enum Phase { case inactive, active, completed }
@State private var phase: Phase = .inactive
Button("Update phase") {
switch phase {
case .inactive: phase = .active
case .active: phase = .completed
case .completed: phase = .inactive
}
}
.audioFeedback(.system(.ui(.tink)), trigger: phase) { newValue in
newValue == .completed
}
@State private var phase: Bool = false
Button("Toggle Phase") {
phase.toggle()
}
.audioFeedback(.system(.ui(.tink)), trigger: phase) {
// Audio feedback triggered
}
The dynamic action approach gives you full control over both the type of feedback and the conditions under which it's triggered.
enum LoadingState { case ready, success, failure }
@State private var loadingState: LoadingState = .ready
Button("Update loading state") {
switch loadingState {
case .ready: loadingState = .success
case .success: loadingState = .failure
case .failure: loadingState = .ready
}
}
.audioFeedback(trigger: loadingState) { oldValue, newValue in
switch (oldValue, newValue) {
case (.failure, .ready):
return .system(.modern(.cameraShutterBurstBegin))
case (.ready, .success):
return .system(.nano(.screenCapture))
case (.success, .failure):
return .system(.new(.update))
default:
return nil
}
}
enum LoadingState { case ready, success, failure }
@State private var loadingState: LoadingState = .ready
Button("Update loading state") {
switch loadingState {
case .ready: loadingState = .success
case .success: loadingState = .failure
case .failure: loadingState = .ready
}
}
.audioFeedback(trigger: loadingState) { newValue in
switch newValue {
case .success: return .system(.modern(.cameraShutterBurstBegin))
case .failure: return .system(.nano(.screenCapture))
default: return nil
}
}
enum LoadingState { case ready, success, failure }
@State private var loadingState: LoadingState = .ready
Button("Update loading state") {
switch loadingState {
case .ready: loadingState = .success
case .success: loadingState = .failure
case .failure: loadingState = .ready
}
}
.audioFeedback(trigger: loadingState) {
// Audio feedback triggered
}
AudioManager
includes a .audioEffectsEnabled
UserDefaults
key, allowing you to dynamically enable or disable audio based on user settings.
This is helpful if you want to add a settings screen for toggling audio sound effects, or if you need an overall logic to control audio — for example, making it a premium feature.
The package uses an internal, publicly exposed UserDefaults
suite for storing audio-related settings:
@main
struct MyAwesomeApp: App {
init() {
UserDefaults.audio.register([
AudioUserDefaultsKey.audioEffectsEnabled : true
])
}
var body: some Scene {
WindowGroup { ... }
}
}
Or manually updating it:
Button("Turn audio off") {
UserDefaults.audio.set(false, for: AudioUserDefaultKeys.isAudioEnabled)
}
Button("Turn audio on") {
UserDefaults.audio.set(true, for: AudioUserDefaultKeys.isAudioEnabled)
}
Important
Although you can register UserDefaults
to any suite (.standard
or custom), the package will only respond to the internal .audio
suite to prevent unintended clashes across different parts of the application.
If the built-in feedback types are not sufficient, you can use your own custom audio files using the .custom(CustomSoundRepresentable)
Playback
case.
There are a limited accepted audio file extensions allowed: .aif
, .aiff
, .caf
, .mp3
, and .wav
.
Also your audio file is limited to 30
seconds. The playback will simply not occur if it is longer than 30 seconds.
To add a custom audio feedback type:
- Define a
CustomSoundRepresentable
conforming enum:
enum MyCustomSound: CustomSoundRepresentable {
case tupperwareFallingOutOfCrampedCupboard
var soundFile: SoundFile {
switch self {
case .tupperwareFallingOutOfCrampedCupboard:
Soundfile(name: "tupperwareFallingOutOfCrampedCupboard", extension: .aif)
}
}
}
-
Ensure that the filename and extension are correct, and loaded into your app main bundle.
-
Use the custom feedback in your app:
@State private var isError: Bool = false
Button("Show error") {
isError.toggle()
}
.audioFeedback(.custom(MyCustomSound.tupperwareFallingOutOfCrampedCupboard), trigger: isError)
Contributions are always welcome! Feel free to submit a pull request or open an issue for any suggestions or improvements you have.
AudioManager
is licensed under the MIT License. See the LICENCE file for more details.