Search code examples
swiftswiftuiswiftdata

How to fetch swift data objects without causing memory issues?


I have been building an audio player application and have a tab in my app that queries the Song model (songs added by the user). The issue is that whenever the UI changes, swiftdata fetches the entire model multiple times; this would be fine if I had less than 50 Song objects but as the database grows, fetches put more strain on the main thread even when users perform simple action. I do not know how to fetch in a more efficient way whenever the UI changes.

this is a snippet from my code, whenever I select the ellipsis on the song from the foreach loop, a sheet comes up.

struct FrPlayerApp: App {
    @AppStorage("isDarkMode") private var isDarkMode = false
    @StateObject private var timerManager = SleepTimerManager()

    var body: some Scene {
        WindowGroup {
            TabControl()
                .preferredColorScheme(isDarkMode ? .dark: .light)
        }
        .environmentObject(timerManager)
        .modelContainer(
            for: [Song.self, Library.self, Album.self], isAutosaveEnabled: false)
}
struct LibraryView: View {

    @Environment(.modelContext) private var context

    @Query(sort: \Song.title, order: .forward) var songs: [Song]

    @State private var isPresentingUser: Song? = nil

    var body: some View {

        ForEach(songs, id: .id) { song in

            VStack {

                HStack(spacing: 5) {

                    HStack {

                        Text(song.title)
                            .frame(height: 40)
                            .lineLimit(2)
                            .multilineTextAlignment(.leading)
                            .font(.subheadline)

                        Spacer()

                    }

                    // ... (other code)

                    HStack {

                        Image(systemName: "ellipsis")

                    }
                    .contentShape(Rectangle())
                    .frame(width: 50, height: 50)
                    .onTapGesture {
                        isPresentingUser = song
                    }

                }
                .padding(.horizontal)
                .frame(height: 60)

            }

            Divider()

        }
        .sheet(item: $isPresentingUser) { song in
            librarySongSheet(song: song)
                .presentationDetents([.height(CGFloat(310))])
        }

    }

}

at 23 seconds I clicked on the ellipsis to open up the sheet and all of the objects in the database got fetched multiple times, this happens whenever the UI changes.

enter image description here


Solution

  • One option is to perform the fetching in a function and decide yourself when that function should be called to update the UI.

    Change the songs array first

    @State private var songs = [Song]()
    

    And you need access to a ModelContext

    @Environment(\.modelContext) private var modelContext
    

    Add a function

    private func loadSongs() {
        let fetchDescriptor = FetchDescriptor<Song>(sortBy: [SortDescriptor(\Song.title)])
        
        do {
             songs = try modelContext.fetch(fetchDescriptor)
        } catch {
             // Error handling here or make the function throw
        }
    }
    

    And then use some relevant modifier to call the function

    //... view code
    }.onAppear(loadSongs)