I have a menu where users can customize flashcards, and it is very laggy.
It is using iOS 17’s SwiftData framework, and I determined it is this code segment below causing the entire menu to be extremely laggy
import SwiftUI
import PhotosUI
import Profanity_Filter
struct EdibleCard: View {
@Bindable var studySetToEdit: StudySetItem
@Bindable var card: TermItem
#if !os(tvOS) && !os(watchOS)
@State private var imagePickerItem: PhotosPickerItem?
#endif
@State private var imageOptionsShown = false
@State private var imageSourceShown = false
@State private var filePickerShown = false
func deleteItem(_ item: TermItem) {
studySetToEdit.terms = studySetToEdit.terms?.filter { $0 != item }
studySetToEdit.terms = fixOrder(terms: studySetToEdit.terms!)
}
var buttonSize: CGFloat = {
#if os(iOS) || os(macOS) || os(watchOS) || os(visionOS)
return 32
#elseif os(tvOS)
return 100
#else
return 200
#endif
}()
var profanityFilter = ProfanityFilter()
#if os(tvOS)
//tvOS Focusing
@FocusState private var deleteIsFocused: Bool
@FocusState private var moveDownIsFocused: Bool
@FocusState private var moveUpIsFocused: Bool
@FocusState private var swapIsFocused: Bool
#endif
var body: some View {
VStack {
VStack {
HStack {
Text("(card.order + 1)")
.font(.headline)
Image(systemName: "arrowtriangle.up.fill")
.font(.headline)
.frame(width: buttonSize, height: buttonSize)
.contentShape(.circle)
#if os(tvOS)
.background {
Circle()
.foregroundStyle(moveUpIsFocused ? .gray : .gray.opacity(0.2))
}
#else
.background {
Circle()
.foregroundStyle(.gray.opacity(0.2))
}
#endif
#if os(tvOS)
.focusable()
.focused($moveUpIsFocused)
#endif
.onTapGesture {
withAnimation(.bouncy) {
let termForNewPos = studySetToEdit.terms!.first(where: { $0.order == card.order - 1 })
card.order -= 1
termForNewPos?.order += 1
}
}
#if os(visionOS) || os(iOS)
.contentShape(.hoverEffect, .circle)
.hoverEffect()
#endif
.withHoverEffect()
.hide(if: card.order == 0)
Image(systemName: "arrowtriangle.down.fill")
.font(.headline)
.frame(width: buttonSize, height: buttonSize)
.contentShape(.circle)
#if os(tvOS)
.background {
Circle()
.foregroundStyle(moveDownIsFocused ? .gray : .gray.opacity(0.2))
}
#else
.background {
Circle()
.foregroundStyle(.gray.opacity(0.2))
}
#endif
#if os(tvOS)
.focusable()
.focused($moveDownIsFocused)
#endif
.onTapGesture {
withAnimation(.bouncy) {
let termForNewPos = studySetToEdit.terms!.first(where: { $0.order == card.order + 1 })
card.order += 1
termForNewPos?.order -= 1
}
}
#if os(visionOS) || os(iOS)
.contentShape(.hoverEffect, .circle)
.hoverEffect()
#endif
.withHoverEffect()
.hide(if: card.order == (studySetToEdit.terms?.count ?? 0) - 1)
Spacer()
#if !os(tvOS)
VStack {
if let imageData = card.cardImageData {
Button(action: {
imageOptionsShown = true
}) {
#if os(macOS)
Image(nsImage: NSImage(data: imageData) ?? NSImage())
.resizable()
.frame(maxHeight: 32)
#else
Image(uiImage: UIImage(data: imageData) ?? UIImage())
.resizable()
.frame(maxHeight: 32)
#endif
}
.buttonStyle(.plain)
.frame(width: buttonSize, height: buttonSize)
.withHoverEffect()
} else {
Image(systemName: "photo")
.font(.headline)
.frame(width: buttonSize, height: buttonSize)
.contentShape(.circle)
.background {
Circle()
.foregroundStyle(.gray.opacity(0.2))
}
.onTapGesture {
imageOptionsShown = true
}
#if os(visionOS) || os(iOS)
.contentShape(.hoverEffect, .circle)
.hoverEffect()
#endif
.withHoverEffect()
}
}
#endif
Image(systemName: "trash")
#if os(tvOS)
.foregroundStyle(deleteIsFocused ? .white : .red)
#else
.foregroundStyle(.red)
#endif
.font(.headline)
.frame(width: buttonSize, height: buttonSize)
.contentShape(.circle)
#if os(tvOS)
.background {
Circle()
.foregroundStyle(deleteIsFocused ? .red : .red.opacity(0.2))
}
#else
.background {
Circle()
.foregroundStyle(.red.opacity(0.2))
}
#endif
#if os(tvOS)
.focusable()
.focused($deleteIsFocused)
#endif
.onTapGesture {
deleteItem(card)
}
#if os(visionOS) || os(iOS)
.contentShape(.hoverEffect, .circle)
.hoverEffect()
#endif
.withHoverEffect()
}
}
Divider()
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 0) {
Text("Term")
.font(.caption)
.multilineTextAlignment(.leading)
TextField("Term", text: $card.term)
.frame(maxWidth: 300)
.textFieldStyle(WillTextFieldStyle())
Spacer()
}
VStack(spacing: 0) {
Text("")
.font(.caption)
//.padding(.bottom, 8)
Image(systemName: "arrow.triangle.swap")
.font(.headline)
.frame(width: buttonSize, height: buttonSize)
.contentShape(.circle)
#if os(tvOS)
.background {
Circle()
.foregroundStyle(swapIsFocused ? .gray : .gray.opacity(0.2))
}
#else
.background {
Circle()
.foregroundStyle(.gray.opacity(0.2))
}
#endif
#if os(tvOS)
.focusable()
.focused($swapIsFocused)
#endif
.onTapGesture {
let temp = card.term
card.term = card.definition
card.definition = temp
}
#if os(visionOS) || os(iOS)
.contentShape(.hoverEffect, .circle)
.hoverEffect()
#endif
.withHoverEffect()
}
VStack(alignment: .leading, spacing: 0) {
Text("Definition")
.font(.caption)
.multilineTextAlignment(.leading)
TextField("Definition", text: $card.definition, axis: .vertical)
.textFieldStyle(WillTextFieldStyle())
Spacer()
}
}
.frame(minHeight: 50)
#if !os(tvOS) && !os(macOS)
.onChange(of: imagePickerItem) {
Task {
if let data = try? await imagePickerItem?.loadTransferable(type: Data.self) {
#if os(macOS)
let image = NSImage(data: data)
if let imageData = image?.tiffRepresentation {
// Convert TIFF representation to NSBitmapImageRep
if let bitmap = NSBitmapImageRep(data: imageData) {
// Set compression factor (0.7 means 70% compression)
let compressionFactor: CGFloat = 0.3
// Convert NSBitmapImageRep to NSData with compression
if let compressedData = bitmap.representation(using: .jpeg, properties: [.compressionFactor: compressionFactor]) {
// Now, 'compressedData' contains the compressed image data
// You can use 'compressedData' as Data for further processing or saving
card.cardImageData = compressedData
}
}
}
#else
let image = UIImage(data: data)
if let compressedData = image?.jpegData(compressionQuality: 0.3) {
card.cardImageData = compressedData
}
#endif
//studySetToEdit.image = data
studySetToEdit.lastModified = Date()
}
}
}
#endif
if profanityFilter.containsProfanity(text: card.term).containsProfanity || profanityFilter.containsProfanity(text: card.definition).containsProfanity {
VStack {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
Text("Contains Profanity")
}
.font(.headline)
Text("When uploading this study set to the StudyDirect Network, this card will be filtered.")
.font(.caption)
}
.foregroundStyle(.yellow)
}
}
.padding()
.background {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.gray.opacity(0.2))
}
#if !os(tvOS)
.sheet(isPresented: $imageOptionsShown) {
NavigationStack {
VStack {
if let imageData = card.cardImageData {
Image.fromData(data: imageData)
.resizable()
.scaledToFit()
.clipShape(.rect(cornerRadius: 18))
} else {
Image(systemName: "photo")
.resizable()
.scaledToFit()
}
Divider()
Text("Choose an image from:")
.font(.title3)
HStack {
PhotosPicker(selection: $imagePickerItem, matching: .images) {
VStack {
Image(systemName: "photo")
.font(.largeTitle)
Text("Photos")
}
}
.withHoverEffect()
Button(action: {
filePickerShown = true
}) {
VStack {
Image(systemName: "folder")
.font(.largeTitle)
Text(getPlatform() == .mac ? "Finder" : "Files")
}
}
.withHoverEffect()
}
.buttonStyle(.plain)
.toolbar {
#if os(iOS)
ToolbarItem(placement: .cancellationAction) {
Button(action: {
card.cardImageData = nil
imageOptionsShown = false
}) {
Text("Remove Image")
}
}
#else
ToolbarItem(placement: .destructiveAction) {
Button(action: {
card.cardImageData = nil
imageOptionsShown = false
}) {
Text("Remove Image")
}
}
#endif
ToolbarItem(placement: .confirmationAction) {
Button(action: {
imageOptionsShown = false
}) {
Text("Done")
}
}
}
}
.padding()
#if os(macOS)
.frame(width: 300, height: 300)
#endif
.fileImporter(isPresented: $filePickerShown, allowedContentTypes: [.image]) { result in
switch result {
case .success(let file):
let doesHaveAccess = file.startAccessingSecurityScopedResource()
if doesHaveAccess {
do {
// Read the data from the file URL
let fileData = try Data(contentsOf: file)
card.cardImageData = fileData
} catch {
// Handle any errors that might occur while reading the file
print("Error reading file: (error)")
}
}
file.stopAccessingSecurityScopedResource()
case .failure(let error):
fatalError("""
Failed to import image with:
(error)
""")
}
}
}
}
#endif
}
}
I tried removing .onTapGesture, removing .hide, removing the profanity detection, removing .contentShape, .withHoverEffect, yet no matter what I tried removing, the lag did not fix itself.