WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Increasing Performance in SwiftData

Forums > SwiftUI

I am trying to increase the performance of my SwiftData app. I have a class that contains as follows:

import Foundation
import SwiftData
import UIKit

@Model
final class GreetingCard: CustomDebugStringConvertible {
    ///     This is an optional String, you can use this to describe the card with a descriptive name
    var cardName: String = ""
    ///     This is the only required property.  An image of the front of the card must be supplied
    @Attribute(.externalStorage) var cardFront: Data?
    ///     This is the of event the Greeting Card is for
    var eventType: EventType?
    ///     This allows you to capture who sells the card. Companies like Hallmark, American Greetings, etc.
    var cardManufacturer: String = ""
    ///     A URL can be stored to allow fo identifying where you bought the card
    var cardURL: String = ""
    ///     A greeting card will be used by various cards being sent.  We have an inverse relationship.
    @Relationship(deleteRule: .cascade, inverse: \Card.cardFront) var cards: [Card]? = [Card]()

    var debugDescription: String {
        "\(cardName ), \(eventType?.eventName ?? "No Event Type"), \(cardManufacturer ), \(cardURL), Used - \(cardsCount()) "
    }

    init(cardName: String, cardFront: Data?, eventType: EventType? = nil, cardManufacturer: String, cardURL: String) {
        /// When creating a new Greeting Card, you have to have an image, all other properties are optional.
        self.cardName = cardName
        self.cardFront = cardFront ?? UIImage(named: "frontImage")!.pngData()!
        self.eventType = eventType
        self.cardManufacturer = cardManufacturer
        self.cardURL = cardURL
    }

    /// A helper value that exposes the card as an Image either a blank image or the value of the image from the realted GreetingCard
    func cardUIImage() -> UIImage {
        let defaultImage: UIImage = UIImage(data: (cardFront)!) ?? UIImage(named: "frontImage")!
        return defaultImage
    }
}

When loading a LazyGrid and displaying the various cards, I would like to use AsyncImage to increase the performance when there are a lot of GreetingCards, I believe the approch would be to load all the columns except for cardFront, and then somehow have cardFront be loaded in the AsyncImageView I created. But I can't seem to figure out how to load a subset of columns in the initial Fetch for the LazyGrid.

Is this possible?

   

I found the following in SwiftData by Example (also here - https://www.hackingwithswift.com/quick-start/swiftdata/how-to-create-a-custom-fetchdescriptor):

var fetchDescriptor = FetchDescriptor<Movie>(sortBy: [SortDescriptor(\.releaseDate, order: .reverse)])
fetchDescriptor.propertiesToFetch = [\.name, \.releaseDate]

Which seems to imply that I could load my LazyVGrid with just the Names of the events

    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: gridLayout, alignment: .center, spacing: 5) {
                    ForEach(cards) { card in
                        ScreenView(card: card, greetingCard: nil, isEventType: .events, navigationPath: $navigationPath)
                    }
                    .padding()
                }
            }
            .navigationTitle("Cards Sent")
            .navigationBarTitleDisplayMode(.inline)
        }
    }

I am not sure how I can use this FetchDescriptor with a Query. Right now in the Init for the above code, I create a Query and assign the results to an array of Cards. Can you add a fetchDescriptor to the query somehow?

        _cards = Query(
            filter: #Predicate {$0.eventType?.persistentModelID == eventTypeID },
            sort: [
                SortDescriptor(\Card.cardDate, order: .reverse),
            ]
        )

But I am thinking this will not address the issue in ScreenView where I am trying to do the AsyncImage

struct ScreenView: View {
    private let blankCardFront = UIImage(named: "frontImage")
    private var iPhone = false
    private var card: Card?
    private var greetingCard: GreetingCard?
    var isEventType: ListView = .recipients
    @Binding var navigationPath: NavigationPath

    init(card: Card?, greetingCard: GreetingCard?, isEventType: ListView, navigationPath: Binding<NavigationPath>) {
        self.card = card
        self.greetingCard = greetingCard
        self.isEventType = isEventType
        self._navigationPath = navigationPath

        if UIDevice.current.userInterfaceIdiom == .pad {
            iPhone = false
        } else {
            iPhone = true
        }
    }

    var body: some View {
        HStack {
            VStack {
                if isEventType != .greetingCard {
                    AsyncImageView(imageData: card!.cardFront?.cardFront)
                } else {
                    AsyncImageView(imageData: greetingCard!.cardFront)
                }
                HStack {
                // removed my other cases for clarity in this example
                    VStack {
                            Text("\(card?.eventType?.eventName ?? "Unknown")")
                                .foregroundColor(.green)
                            HStack {
                                Text("\(card?.cardDate ?? Date(), formatter: cardDateFormatter)")
                                    .fixedSize()
                                    .foregroundColor(.green)
                                MenuOverlayView(card: card!, greetingCard: greetingCard, isEventType: .recipients, navigationPath: $navigationPath)
                        }
                    }
                    .padding(iPhone ? 1 : 5)
                    .font(iPhone ? .caption : .title3)
                    .foregroundColor(.primary)
                }
            }
        }
        .padding()
        .frame(minWidth: iPhone ? 160 : 320, maxWidth: .infinity,
               minHeight: iPhone ? 160 : 320, maxHeight: .infinity)
        .background(Color(UIColor.systemGroupedBackground))
        .mask(RoundedRectangle(cornerRadius: 20))
        .shadow(radius: 5)
        .padding(iPhone ? 5: 10)
    }
}

I am still not sure how to have the card.eventType.eventName, and card.cardDate load immediately and then the card.cardFront?.cardFront load Async

   

After working with other slack forums, it looks like my issue was not in the this code, but my print routine. I have temporarily removed that function and performance was fine.

   

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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.