Make voice note loading completely lazy with deferred initialization

Aggressive performance optimization to prevent UI freezes:

Problem: Even with async loading, creating 10+ VoiceNotePlaybackController
instances simultaneously (when scrolling past multiple voice notes) spawned
20+ concurrent background tasks, potentially starving main thread.

Solution - Ultra-lazy loading:
1. VoiceNotePlaybackController.init() now does ZERO work
   - No duration loading
   - No player creation
   - Instant initialization

2. Duration loaded on-demand via public loadDuration() method
   - Called from VoiceNoteView.onAppear after 150ms delay
   - Reduced priority: .utility instead of .userInitiated
   - Guard prevents duplicate loading

3. Waveform loading also deferred 150ms
   - Gives UI time to settle after message appears
   - Prevents task storms when multiple voice notes appear

This spreads the work over time instead of all at once.
This commit is contained in:
jack
2025-10-14 22:29:38 +02:00
parent 8f62dd1776
commit 2040e94b83
2 changed files with 15 additions and 9 deletions
@@ -16,12 +16,14 @@ final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlay
init(url: URL) {
self.url = url
super.init()
// Load duration asynchronously to avoid blocking main thread
loadDurationAsync()
// Don't load anything eagerly - wait until user interaction or view is fully displayed
}
private func loadDurationAsync() {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
func loadDuration() {
// Only load if not already loaded
guard duration == 0 else { return }
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let self = self else { return }
do {
let player = try AVAudioPlayer(contentsOf: self.url)
@@ -44,7 +46,8 @@ final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlay
stop()
self.url = url
player = nil
loadDurationAsync()
duration = 0
// Duration will be loaded on demand when needed
}
func togglePlayback() {
+7 -4
View File
@@ -100,10 +100,13 @@ struct VoiceNoteView: View {
.stroke(borderColor, lineWidth: 1)
)
.onAppear {
WaveformCache.shared.waveform(for: url, completion: { bins in
self.waveform = bins
})
// No need to call playback.replaceURL - already initialized with correct URL
// Defer both duration and waveform loading to let UI settle after message appears
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
playback.loadDuration()
WaveformCache.shared.waveform(for: url, completion: { bins in
self.waveform = bins
})
}
}
.onChange(of: url) { newValue in
WaveformCache.shared.waveform(for: newValue, completion: { bins in