I have several views nested within each other in my Swift program. In the lowest level view, I have a slider. I'd like to pass it's value from this view to the main Swift file where the global variable is stored within a struct. This value will be used to control the volume of the audio being played using AudioKit.
Code for slider (not included the full view here as the rest is irrelevant):
struct MasterFaderView: View {
var body: some View {
Slider(
value: master.Value,
in: 0...127,
step: 1.0)
}
}
Top Level global variable being set:
struct channel {
var Value: Double = 0.0 // Default fader value
... // Other variables being stored
}
var master = channel()
@main
struct MEngProjectApp: App {
var body: some Scene {
WindowGroup {
FreePlayView()
}
}
}
FreePlayView() is the main view which contains the audioKit stuff and will control the volume:
class MixerClass: ObservableObject {
let engine = AudioEngine()
var fullTrack = AudioPlayer()
init() {
engine.output = fullTrack
loadFiles()
try? engine.start()
}
func loadFiles() {
do {
if let fileURL = Bundle.main.url(forResource: "DQ_FullTrack", withExtension: "wav") {
try fullTrack.load(url: fileURL)
} else {
Log("Could not find file")
}
} catch {
Log("Could not load full track")
}
}
}
struct FreePlayView: View {
@StateObject var conductor = MixerClass()
@State var trackPlaying: Bool = false
var body: some View {
Button(action: {
if trackPlaying == false {
conductor.fullTrack.start()
conductor.fullTrack.volume = master.Value
trackPlaying = true
} else {
conductor.fullTrack.stop()
trackPlaying = false
}
}) {
Text("Start Audio")
}
The error code I get within MasterFaderView is
Cannot convert value of type 'Double' to expected argument type 'Binding<Double>'
Anyone any ideas how to fix this?
Thanks in advance.
@Wpitchy,
In your code snippets, I'm not seeing where you're updating the fullTrack.volume
via the Slider's value.
I would also keep the @Published var fullTrackVolume
variable in the Conductor
, instead of in its own ChannelMode
class, because it affects the fullTrack
AudioPlayer that was instantiated there.
Here's what I would change:
MEngProjectApp.swift
import SwiftUI
@main
struct MEngProjectApp: App {
// If you want the MixerClass to be available in multiple views (not just FreePlayView), you will want to instantiate it as an EnvironmentObject in the App level.
@StateObject var conductor = MixerClass()
var body: some Scene {
WindowGroup {
FreePlayView()
.environmentObject(conductor) // <- Passing this object into this parent view will allow the MixerClass to be accessible from the FreePlayView and any of its child views.
}
}
}
MixerClass.swift
import AudioKit
import AVFoundation
import Foundation
final class MixerClass: ObservableObject {
let engine = AudioEngine()
@Published var fullTrack = AudioPlayer() // <- Published vars broadcast any updates that occur, such as volume level changes.
@Published var isTrackPlaying: Bool = false
init() {
// Importing AVFoundation is required in order to access the AVAudioSession settings.
do {
Settings.bufferLength = .medium
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(Settings.bufferLength.duration)
try AVAudioSession.sharedInstance().setCategory(.playAndRecord,
options: [.defaultToSpeaker,
.mixWithOthers,
.allowBluetoothA2DP])
try AVAudioSession.sharedInstance().setActive(true)
} catch let err {
print(err)
}
loadFiles()
fullTrack.isLooping = true // <- Set this to true, if you don't want the audio clip to stop playing.
engine.output = fullTrack
try? engine.start()
}
func loadFiles() {
do {
if let fileURL = Bundle.main.url(forResource: "DQ_FullTrack", withExtension: "wav") {
try fullTrack.load(url: fileURL)
} else {
Log("Could not find file")
}
} catch {
Log("Could not load full track")
}
}
func convertVolumeToPercent() -> String {
"\(Int(fullTrack.volume * 100))%" // <- Converts 0.5 to 50% and 1.0 to 100%, etc.
}
// MARK: User Intent Actions from any or multiple views.
// This way, the business logic "decision-making" from triggered events will remain outside of any of the views. These actions can be repurposed, and not tied to any view code.
func playStopToggle() {
isTrackPlaying ? fullTrack.stop() : fullTrack.start()
isTrackPlaying.toggle()
}
}
FreePlayView.swift
import SwiftUI
struct FreePlayView: View {
@EnvironmentObject var conductor: MixerClass
var body: some View {
VStack {
MasterFaderView()
playStopButton
}
}
private var playStopButton: some View {
Button(action: {
conductor.playStopToggle()
}) {
Text("\(Image(systemName: conductor.isTrackPlaying ? "stop.fill" : "play.fill")) \(conductor.isTrackPlaying ? "Stop" : "Start") Audio")
}
}
}
struct FreePlayView_Previews: PreviewProvider {
static var previews: some View {
FreePlayView()
.environmentObject(MixerClass()) // <- Pass in the MixerClass in order for the SwiftUI Preview to work without crashing.
}
}
MasterFaderView.swift
import SwiftUI
struct MasterFaderView: View {
@EnvironmentObject var conductor: MixerClass
var body: some View {
VStack {
Text(conductor.convertVolumeToPercent())
.font(.title).fontWeight(.thin)
Slider(value: $conductor.fullTrack.volume, in: 0...1) // <- Volume in the AudioPlayer is 0.0-1.0, not 0-127.
.padding()
}
}
}
struct MasterFaderView_Previews: PreviewProvider {
static var previews: some View {
MasterFaderView()
.environmentObject(MixerClass()) // <- Pass in the MixerClass in order for the SwiftUI Preview to work without crashing.
}
}
Please let me know if this helps.
If you'd like to see this in a larger Xcode project, please check out the following: