In our macOS SwiftUI app, we are experiencing some lag when typing into a TextField in a view that also contains Images. It's not bad, but if a user types pretty quickly, they can get ahead of the TextField displaying the typed characters.
We've stepped through the code, used Instruments and can't seem to nail down the source of the lag.
The structure of the app is straightforward: There's a top level Content view with a list of people, upon clicking a person, a EditPersonView displays that persons name (or some string) along with some (3) pictures they had previously selected from their Photos library. The persons name is a single TextField which is where we are seeing lag when typing quickly
The image data is stored in SwiftData but are added to the View in .onAppear and stored in an array of Images. That avoids doing the loading/conversion when the view redraws itself.
Edit to include complete code per a comment:
import SwiftUI
import SwiftData
@Model
class Person {
var name: String
@Attribute(.externalStorage) var photos: [Data] = []
init(name: String, emailAddress: String, details: String, metAt: Event? = nil) {
self.name = name
}
}
struct ContentView: View {
@State private var path = NavigationPath()
@Environment(\.modelContext) var modelContext
@State private var searchText = ""
@State private var sortOrder = [SortDescriptor(\Person.name)]
@Query var people: [Person]
var body: some View {
NavigationStack {
List {
ForEach(people) { person in
NavigationLink(value: person) {
Text(person.name)
}
}
}
.navigationDestination(for: Person.self) { person in
EditPersonView(person: person, navigationPath: $path)
}
}
}
func addPerson() {
let person = Person(name: "New Person", emailAddress:"", details: "")
modelContext.insert(person)
path.append(person)
}
}
#Preview {
ContentView()
}
and then the EditPersonView
import PhotosUI
import SwiftData
import SwiftUI
struct EditPersonView: View {
@Bindable var person: Person
@Binding var navigationPath: NavigationPath
@State private var pickerItems = [PhotosPickerItem]()
@State private var selectedImages = [Image]()
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 8) {
ForEach(0..<selectedImages.count, id: \.self) { index in
selectedImages[index]
.resizable()
.scaledToFit()
}
}
}
HStack {
PhotosPicker("Select images", selection: $pickerItems, matching: .images, photoLibrary: .shared())
Button("Remove photos") {
person.photos.removeAll()
selectedImages.removeAll()
pickerItems.removeAll()
}
}
TextField("Name", text: $person.name)
.textContentType(.name)
}
.navigationTitle("Edit Person")
.navigationDestination(for: Event.self) { event in
//show some info about the event. unrelated to issue
}
.onChange(of: pickerItems, addImages)
.padding()
.onAppear {
person.photos.forEach { photoData in
let nsImage = NSImage(data: photoData)
let image = Image(nsImage: nsImage!)
selectedImages.append(image)
}
}
}
func addImages() {
Task {
selectedImages.removeAll()
for item in pickerItems {
if let loadedImageData = try await item.loadTransferable(type: Data.self) {
person.photos.append(loadedImageData)
let nsImage = NSImage(data: loadedImageData)
let image = Image(nsImage: nsImage!)
selectedImages.append(image)
}
}
}
}
}
#Preview {
do {
let previewer = try Previewer()
return EditPersonView(
person: previewer.person,
navigationPath: .constant(NavigationPath())
)
.modelContainer(previewer.container)
} catch {
return Text("Failed to create preview: \(error.localizedDescription)")
}
}
and for completeness, here's Previewer
import Foundation
import SwiftData
import SwiftUICore
@MainActor
struct Previewer {
let container: ModelContainer
let event: Event
let person: Person
let ship: Image
init() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
container = try ModelContainer(for: Person.self, configurations: config)
event = Event(name: "Preview Event", location: "Preview Location")
person = Person(name: "Preview Name", emailAddress: "preview@preview.com", details: "", metAt: event)
ship = Image("Enterprise") //any image for the preview
container.mainContext.insert(person)
}
}
and the main app entry point
import SwiftUI
import SwiftData
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Person.self)
}
}
Here's an example - the last piece of text 'ok' displays a full 1 second after the user typed it.
We've eliminated environmental issues as this occurs across multiple Macs in different environments. The issue duplicates on Mac Studio M1 Max, iMac M4 and MacBook Pro M1 Pro. All running Sequoia 15.3.2, XCode 16.2
Your TextField
is updating the query with every letter you type, which seems to be reloading the image each time and thus creates a lag.
As a solution, try to replace $person.name
binding of the TextField
with a local @State
variable that you set in a task
. Then update the binding when the user has finished typing (presses return) with onSubmit
.
Like this:
import SwiftUI
struct EditPersonView: View {
@Bindable var person: Person
@State private var name = "" // <- this
@Binding var navigationPath: NavigationPath
@State private var pickerItems = [PhotosPickerItem]()
@State private var selectedImages = [Image]()
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 8) {
ForEach(0..<selectedImages.count, id: \.self) { index in
selectedImages[index]
.resizable()
.scaledToFit()
}
}
}
HStack {
PhotosPicker("Select images", selection: $pickerItems, matching: .images, photoLibrary: .shared())
Button("Remove photos") {
person.photos.removeAll()
selectedImages.removeAll()
pickerItems.removeAll()
}
}
TextField("Name", text: $name) // <- this
.textContentType(.name)
.onSubmit {
person.name = self.name
} // <- this
}
.navigationTitle("Edit Person")
.navigationDestination(for: Event.self) { event in
//show some info about the event. unrelated to issue
}
.onChange(of: pickerItems, addImages)
.padding()
.onAppear {
person.photos.forEach { photoData in
let nsImage = NSImage(data: photoData)
let image = Image(nsImage: nsImage!)
selectedImages.append(image)
}
}
.task {
self.name = person.name // <- this
}
}
func addImages() {
Task {
selectedImages.removeAll()
for item in pickerItems {
if let loadedImageData = try await item.loadTransferable(type: Data.self) {
person.photos.append(loadedImageData)
let nsImage = NSImage(data: loadedImageData)
let image = Image(nsImage: nsImage!)
selectedImages.append(image)
}
}
}
}
}
#Preview {
do {
let previewer = try Previewer()
return EditPersonView(
person: previewer.person,
navigationPath: .constant(NavigationPath())
)
.modelContainer(previewer.container)
} catch {
return Text("Failed to create preview: \(error.localizedDescription)")
}
}