2

When I have multiple text fields rendered in SwiftUI in a given view, I am getting noticeable lag that is directly proportional to the number of text fields. If I change these to simple text views, the lag goes down considerably.

I have looked at SO and found a few questions about lag with TextField but generally it seems like there's a preponderance that the lag is caused by the data source because when using a constant value, the lag is not observed.

I have created a demo project to illustrate the issue. I have an array of 20 contact names and for each name create a contact card with three email addresses. If I toggle the view between rendering the email addresses as Text vs TextField Views (with a constant value), the time taken from button tap to the last view's .onAppear is 80-100 ms (Text) and 300-320 ms (TextField).

Both views take a noticeable time to render, but clearly the TextFields take a significantly longer time to render on this contrived, trivial app. In our app, we are rendering significantly more information and not using constant values for the TextFields so this lag produces more pronounced effects (sometimes a few seconds). Is there some way around this issue for SwiftUI TextFields? Below is the code for the demo project. I know there are better ways to write the code, just threw it together quickly to demonstrate the speed issues.

Also, interestingly, if I put the ForEach into a List (or just try to use a list directly from the array data), no ContactCard views are rendered at all.

Any help is greatly appreciated!

import SwiftUI

var formatter: DateFormatter {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
    return formatter
}

struct ContentView: View {
    
    let contacts: Array<(first: String, last: String)> = [
        ("John", "Stone"),
        ("Ponnappa", "Priya"),
        ("Mia", "Wong"),
        ("Peter", "Stanbridge"),
        ("Natalie", "Lee-Walsh"),
        ("Ang", "Li"),
        ("Nguta", "Ithya"),
        ("Tamzyn", "French"),
        ("Salome", "Simoes"),
        ("Trevor", "Virtue"),
        ("Tarryn", "Campbell-Gillies"),
        ("Eugenia", "Anders"),
        ("Andrew", "Kazantzis"),
        ("Verona", "Blair"),
        ("Jane", "Meldrum"),
        (" Maureen", "M. Smith"),
        ("Desiree", "Burch"),
        ("Daly", "Harry"),
        ("Hayman", "Andrews"),
        ("Ruveni", "Ellawala")
    ]
    
    @State var isTextField = false
    
    var body: some View {
        ScrollView {
            VStack {
                HStack {
                    Button("Text") {
                        print("text tapped: \(formatter.string(from: Date()))")
                        isTextField = false
                    }
                    Button("TextField") {
                        print("text tapped: \(formatter.string(from: Date()))")
                        isTextField = true
                    }
                }
                ForEach(contacts, id: \.self.last) { contact in
                    ContactCard(name: contact, isTextField: $isTextField)
                }
            }
        }
    }
}

struct ContactCard: View {
    
    var name: (first: String, last: String)
    
    @Binding var isTextField: Bool
    
    var emailAddresses: Array<String> {
        [
        "\(name.first).\(name.last)@home.com",
        "\(name.first).\(name.last)@work.com",
        "\(name.first).\(name.last)@work.org",
    ]
    }
    
    var body: some View {
        VStack {
            Text("\(name.first) \(name.last)")
            .font(.headline)
            ForEach(emailAddresses, id: \.self) { email in
                HStack {
                    Text("Email")
                        .frame(width: 100)
                    if isTextField {
                        TextField("", text: .constant(email))
                            .onAppear(){
                                print("view appeared: \(formatter.string(from: Date()))")
                            }
                    } else {
                        Text(email)
                            .onAppear(){
                                print("view appeared: \(formatter.string(from: Date()))")
                            }
                    }
                    Spacer()
                }
                .font(.body)
            }
        }
        .padding()
    }
}

1 Answer 1

1

Use LazyVStack in your scroll view instead of VStack. It worked for me, tested using 200 contact names.

Sign up to request clarification or add additional context in comments.

3 Comments

So replacing the VStack in the body of content view with LazyVstack dropped the time to load for Text Views to 50-60 ms, and the TextField to 230-240 ms. The load time is still roughly 3x longer. This is all in simulator running iOS 14.5. The issue with LazyVStack is we are trying to maintain compatibility with iOS 13 and LazyVStack is 14+. I will test on physical device today to see what else I find.
Also, creating a custom TextField by embedding a UITextView in UIViewRepresentable with a binding String (no title since we don't use that feature) cuts the time to load in about half, so 135-145 ms for the list described in the OP. I will test this solution in the full app to see how performance is affected.
I'm on macos 12.beta, xcode 13.beta, target ios 15 and macCatalyst. Tested on real devices ios 15 and macos 12. I added a few hundred more contacts without any problems.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.