admin管理员组文章数量:1356936
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: "[email protected]", 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
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: "[email protected]", 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
Share Improve this question edited Apr 1 at 19:22 soundflix 2,86312 gold badges16 silver badges34 bronze badges asked Mar 30 at 14:12 JayJay 35.7k19 gold badges59 silver badges88 bronze badges 8 | Show 3 more comments2 Answers
Reset to default 1Your 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)")
}
}
In the question, the code provides a somewhat automated experience for the user where if they change the text, it's auto-saved. The wonderful answer from @soundflix provides a solution but after implementing that, there's another option to provide a similar experience: save the changes when the user clicks or taps the back < button.
From the answer I omitted the onSubmit
code
.onSubmit {
person.name = self.name
}
and then crafted my own toolbar "back" button that saves the string when the user clicks back
VStack {
//
}
.navigationBarBackButtonHidden(true) //hide the default back button
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigation) {
Button {
person.name = self.name
navigationPath.removeLast()
} label: {
Image(systemName: "chevron.left")
}
}
}
本文标签: swiftuiView with Images And TextField lagStack Overflow
版权声明:本文标题:swiftui - View with Images And TextField lag - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1743986229a2571317.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
let _ = Self._printChanges()
in both views and you will see that each typed character in the text field fires an update of theQuery
in theContentView
. You need to separate the two views so you don't use@Bindable
– Joakim Danielson Commented Mar 30 at 17:38@Query
and@Bindable
? My code separates the@Query
macro inContentView
(like your ContentView) and@Bindable
in theEditPersonView
, like yourItemRow
view. Or am I misunderstanding? – Jay Commented Mar 31 at 20:30let
declare thePerson
property and instead work with a local@State
property for the name just to see if the performance problem goes away. – Joakim Danielson Commented Mar 31 at 20:41