I am engaged on a voice recorder app utilizing SwiftUI. I presently have the participant interface in a DisclosureGroup, as so: https://imgur.com/a/GmLvmMh. The audio stops playback whenever you shut the present DisclosureGroup, and in addition just one DisclosureGroup may be opened at a time. Nevertheless, I can not make it in order that the audio stops playback when opening one other DisclosureGroup.
The code for my participant is right here:
import Basis
import AVFoundation
import SwiftUI
import Mix
class Participant: NSObject, ObservableObject, AVAudioPlayerDelegate {
var participant: AVAudioPlayer?
let objectWillChange = PassthroughSubject<Void, By no means>()
var isPlaying = false {
didSet {
objectWillChange.ship()
}
}
@Printed var currentTime: TimeInterval = 0
non-public var timer: AnyCancellable?
init(soundURL: URL) throws {
tremendous.init()
if FileManager().fileExists(atPath: soundURL.path) {
do {
self.participant = attempt AVAudioPlayer(contentsOf: soundURL)
participant?.prepareToPlay()
self.participant?.delegate = self
} catch {
throw Errors.FailedToPlayURL
}
} else {
print("URL not legitimate!")
}
}
func play() {
self.participant?.play()
isPlaying = true
startTimer()
}
func pause() {
if isPlaying {
self.participant?.pause()
isPlaying = false
stopTimer()
}
}
func cease() {
participant?.cease()
isPlaying = false
resetPlayback()
}
func search(to time: TimeInterval) {
participant?.currentTime = time
currentTime = time
}
var period: TimeInterval {
participant?.period ?? 0
}
non-public func startTimer() {
timer = Timer.publish(each: 0.1, on: .primary, in: .widespread)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
self.currentTime = self.participant?.currentTime ?? 0
self.objectWillChange.ship()
}
}
non-public func stopTimer() {
timer?.cancel()
timer = nil
}
func audioPlayerDidFinishPlaying(_ participant: AVAudioPlayer, efficiently flag: Bool) {
if flag {
isPlaying = false
stopTimer()
resetPlayback()
}
}
non-public func resetPlayback() {
currentTime = 0
participant?.currentTime = 0
objectWillChange.ship()
}
}
The code for my PlayerViewModel is right here:
import Basis
import Mix
class PlayerViewModel: ObservableObject {
@Printed var currentTime: TimeInterval = 0
var participant: Participant
non-public var cancellables = Set<AnyCancellable>()
non-public var seekingSubject = PassthroughSubject<TimeInterval, By no means>()
init(participant: Participant) {
self.participant = participant
self.participant.objectWillChange
.sink { [weak self] in
self?.currentTime = self?.participant.currentTime ?? 0
}
.retailer(in: &cancellables)
seekingSubject
.throttle(for: .milliseconds(100), scheduler: RunLoop.primary, newest: true)
.sink { [weak self] time in
self?.participant.search(to: time)
}
.retailer(in: &cancellables)
}
func play() {
participant.play()
}
func pause() {
participant.pause()
}
func cease() {
participant.cease()
}
func search(to time: TimeInterval) {
seekingSubject.ship(time)
}
var period: TimeInterval {
participant.period
}
}
Lastly, the code for my PlayerView is right here. At the moment, the best way I am detecting a disclosure group is opened is by the URL it is certain to. This fashion when a disclosure group is opened, opening one other one closes the present one. I attempted placing viewModel.cease()
in right here, however to no avail:
import SwiftUI
import AVFoundation
struct PlayerView: View {
@State var soundURL: URL
@Binding var openedGroup: URL?
@State non-public var isOpened: Bool = false
@StateObject non-public var viewModel: PlayerViewModel
@State non-public var sliderValue: TimeInterval = 0
init(soundURL: URL, openedGroup: Binding<URL?>) {
self._soundURL = State(initialValue: soundURL)
self._openedGroup = openedGroup
let participant = attempt? Participant(soundURL: soundURL)
self._viewModel = StateObject(wrappedValue: PlayerViewModel(participant: participant!))
}
var physique: some View {
DisclosureGroup(isExpanded: Binding(
get: { self.openedGroup == self.soundURL },
set: { newValue in
if newValue {
self.openedGroup = self.soundURL
} else if self.openedGroup == self.soundURL {
self.openedGroup = nil
viewModel.cease()
}
}
)) {
VStack {
Slider(worth: $sliderValue, in: 0...viewModel.period, onEditingChanged: { enhancing in
if enhancing {
viewModel.pause()
} else {
viewModel.search(to: sliderValue)
viewModel.play()
}
})
.padding()
.onChange(of: viewModel.currentTime) {
sliderValue = viewModel.currentTime
}
HStack {
Textual content(timeString(from: viewModel.currentTime))
Spacer()
Textual content(timeString(from: viewModel.period))
}
.padding(.horizontal)
HStack {
Spacer()
Picture(systemName: viewModel.participant.isPlaying ? "pause.fill" : "play.fill")
.onTapGesture {
if viewModel.participant.isPlaying {
viewModel.pause()
} else {
viewModel.play()
}
}
Spacer()
}
Spacer()
FileNameButtonView(soundURL: soundURL)
}
} label: {
Textual content(soundURL.lastPathComponent)
}
}
non-public func timeString(from timeInterval: TimeInterval) -> String {
let minutes = Int(timeInterval) / 60
let seconds = Int(timeInterval) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
If you’d like to try the complete repo, it’s right here: https://github.com/aabagdi/MemoMan/.
Thanks for any assist!