UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: SwiftUI + CoreData unusual memory leak?

Forums > iOS

Hierarchy

memory leak?

My app is an app that records information about clothes purchased.

It consists of a total of 3 TabBars that show the items sorted by purchase date, brand, and category.

  • The purchase date sorted view shows the items based on the purchase date.
  • The brand-sorted view shows the registered brands, and when you tab on a brand, it shows the items from that brand sorted by category (again using SectionedFetchRequest).
  • The category sort view works the same as the brand sort view.

All 3 views use the SectionedFetchRequest property wrapper.

The problem is that it takes up an unusually large amount of memory as the number of logged items increases. I'm using over 100MB of memory when the number of items goes beyond about 20, and up to 450MB or more when scrolling through the items, especially if each item contains an image.

Images are compressed using jpegData(compressionQuality: 0.3) as a Binary Data type and stored in CoreData.

Is there any way to minimize the memory used by the app? Am I using SectionedFetchRequest unnecessarily? (Brand Or Category Tab View -> DetailView)

import SwiftUI
import CoreData

struct ContentView: View {
    @AppStorage("isFirstLaunch") private var isFirstLaunch = true

    var body: some View {
        TabView {
            MainListView()
                .tabItem {
                    Label("Inbox", systemImage: "clock")
                }

            MainBrandView()
                .tabItem {
                    Label("Brand", systemImage: "bag.fill")
                }

            MainCategoryView()
                .tabItem {
                    Label("Category", systemImage: "tray.full.fill")
                }
        }
        .tint(.heremesColor)
        .sheet(isPresented: $isFirstLaunch) {
            IntroView()
        }
    }
}
// MainListView
struct MainListView: View {
    @Environment(\.managedObjectContext) private var moc
    @SectionedFetchRequest<String, ItemEntity>(sectionIdentifier: \.contentViewSectionHeader, sortDescriptors: [
        SortDescriptor(\.orderDate, order: .reverse),
        SortDescriptor(\.createdDate, order: .reverse)
    ]) private var items: SectionedFetchResults<String, ItemEntity>

    ...

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { section in
                    Section {
                        ForEach(section) { item in
                            MainListRowView(item: item, isDetailView: false)
                                .contentShape(Rectangle())
                                .onTapGesture {
                                    selectedItem = item
                                }
                        }

                    } header: {
                        Text(section.id)
                    }
                }
            }
            .listStyle(.plain)
            .navigationTitle("Inbox")
            .sheet(isPresented: $showingAddView) { AddView() }
            .sheet(item: $selectedItem) { item in
                DetailView(item: item)
            }
        }
    }
// MainBrand&MainCategoryView
import SwiftUI

struct MainBrandView: View {
    @SectionedFetchRequest<String, ItemEntity>(sectionIdentifier: \.unwrappedBrand, sortDescriptors: [
        SortDescriptor(\.brand),
        SortDescriptor(\.name)
    ]) private var brands: SectionedFetchResults<String, ItemEntity>

    ...

    var body: some View {
        NavigationView {
            List {
                ForEach(brands) { brand in
                    NavigationLink {
                        MainBrandDetailView(name: brand.id)
                    } label: {
                        MainBrandRowView(name: brand.id, count: brand.count)
                    }
                }
                .listRowSeparatorTint(.strokeColor)
                .listSectionSeparator(.hidden, edges: .top)
            }
            .sheet(isPresented: $showingAddView) {
                AddView()
            }
            ...
        }
    }

    private func predicate(by text: String) -> NSPredicate {
        NSPredicate(format: "brand CONTAINS[cd] %@", text)
    }
}
// MainBrandDetailView
import SwiftUI

struct MainBrandDetailView: View {
    @Environment(\.managedObjectContext) private var moc
    @SectionedFetchRequest<String, ItemEntity> private var items: SectionedFetchResults<String, ItemEntity>

    private let name: String

    init(name: String) {
        self.name = name
        _items = SectionedFetchRequest<String, ItemEntity>(sectionIdentifier: \.unwrappedCategory,
                                                                sortDescriptors: [ SortDescriptor(\.category), SortDescriptor(\.orderDate, order: .reverse)],
                                                           predicate: NSPredicate(format: "brand == %@", name), animation: .default)
    }

    var body: some View {
        List {
            ForEach(items) { brand in
                Section {
                    ForEach(brand) { item in
                        MainListRowView(item: item, isDetailView: false)
                    }

                } header: {
                    Text(brand.id)
                }
            }
        }
        .sheet(isPresented: $showingAddView) {
            AddView()
        }
        .sheet(item: $selectedItem) { item in
            DetailView(item: item)
        }
        ...
    }

    private func predicate(by text: String) -> NSPredicate {
        NSPredicate(format: "brand == %@ && name CONTAINS[cd] %@ || category CONTAINS[cd] %@ || review CONTAINS[cd] %@ || fit CONTAINS[cd] %@ || #size CONTAINS[cd] %@", name, text, text, text, text, text)
    }

    private func save() {
        guard moc.hasChanges else { return }
        do {
            try moc.save()
            print("Success Save to Core Data")
        } catch {
            print("Failed Save to Core Data, \(error.localizedDescription)")
        }
    }
}

3      

Memory leaks can occur in SwiftUI apps that use CoreData, but they are not necessarily unusual. SwiftUI and CoreData are two powerful frameworks, and managing memory correctly when using them together is crucial to avoid leaks.

To help you diagnose and fix the memory leak in your SwiftUI app that uses CoreData, here are some common causes and potential solutions:

Retain cycles: Retain cycles occur when objects hold strong references to each other, preventing them from being deallocated. In the context of SwiftUI and CoreData, make sure you're not creating strong references to views or view models from your CoreData entities or managed object contexts. Instead, use weak or unowned references where appropriate.

Forgetting to unsubscribe from notifications: If you're observing notifications such as NSManagedObjectContextObjectsDidChangeNotification, make sure to remove your observers when they are no longer needed. Failure to unsubscribe from notifications can lead to objects being retained in memory even when they are no longer relevant.

Large fetch requests: If your app performs large CoreData fetch requests without proper batching or pagination, it can lead to excessive memory usage. Consider optimizing your fetch requests to fetch smaller batches of data at a time, especially if you're dealing with a large data set.

Using excessive memory in views: SwiftUI views can consume a significant amount of memory if not managed properly. Make sure you're not storing unnecessary data in your views or view models. Avoid keeping large arrays or other data structures directly in your views, and consider lazy-loading or paginating data to minimize memory usage.

Not using @FetchRequest correctly: SwiftUI provides the @FetchRequest property wrapper to simplify working with CoreData fetch requests. Ensure that you're using it correctly and efficiently. Make sure the fetch request is properly configured with predicates, sort descriptors, and other options to minimize the amount of data fetched from the persistent store.

Long-lived references to managed objects: Be cautious with long-lived references to managed objects, especially if you pass them between views or store them in view models. Consider using object IDs instead of directly passing or storing managed objects, and use the object ID to retrieve the object from the managed object context when needed.

Improper handling of CoreData contexts: Make sure you're using the appropriate types of CoreData contexts (e.g., main queue context, private queue context) and that you're using them on the correct queues. Perform CoreData operations on the appropriate queue to avoid potential concurrency issues and memory leaks.

Remember to use memory profiling tools like Instruments in Xcode to identify specific memory allocations and track down the source of the leaks. These tools can help you identify patterns and pinpoint areas of your code that may need attention.

If you're still unable to resolve the memory leak, consider posting a specific code snippet or providing more details about your implementation. That way, I can offer more targeted suggestions.

3      

extension UIImage {
    func downImageSize(size: CGSize) -> UIImage {
        let imageSize = self.size
        let aspectRatio = imageSize.width / imageSize.height

        var thumbnailSize = size
        if aspectRatio > 1 {
            thumbnailSize.height = size.width / aspectRatio
        } else {
            thumbnailSize.width = size.height * aspectRatio
        }

        let renderer = UIGraphicsImageRenderer(size: thumbnailSize)
        return renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: thumbnailSize))
        }
    }
}
    private func resizeImage(image: UIImage) -> Data? {
        let resizedImage = image.downImageSize(size: CGSize(width: 56, height: 56))
        let convertedData = compressionImage(image: resizedImage)
        return convertedData
    }

    private func compressionImage(image: UIImage) -> Data? {
        image.jpegData(compressionQuality: 0.7)
    }

    private func createNewItem() {
        let newItem = ItemEntity(context: moc)

        if !imageData.isEmpty {
            if let uiImage = UIImage(data: imageData) {
                newItem.imageData = compressionImage(image: uiImage)
                newItem.thumbnailData = resizeImage(image: uiImage)
            }
            ...
            try? moc.save()
        }

Thanks for your answer.

I noticed a significant increase in memory when retrieving an image from the List that I had stored in the core data, so when I saved the image, I saved the original image and the image I wanted to use for the thumbnail (CGSize(width:56, height:56)) so that I could minimize memory usage by showing the thumbnail image in the main List.

However, I implemented a stick HeaderView that uses a ScrollView + GeometryReader in a DetailView to show the image in a set proportion. I used the original image, and it consumes a lot of unnecessary energy, as the image keeps redrawing every time I scroll. (In the simulation, it stutters).

I'm trying to figure out how to solve this problem.

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.