Michael St Clair
Michael St Clair

Reputation: 6615

Swift slow performance when filtering array

I have an array of a Category class that has a name and parentName. I have a search bar to allow users search for categories by the category name or parentName. My full array has about 600 items. On typing the first letter it takes about 2-3 seconds and freezes all other input on the keyboard. After the first letter everything is fast.

Here is how I am filtering

return self.userData.categories.filter({$0.name.lowercased().hasPrefix(searchText.lowercased()) || ($0.parentName != nil && $0.parentName!.lowercased().hasPrefix(searchText.lowercased()))})

One piece I think it may be is SwiftUI rendering all of the rows, however the initial render is fast.

This is how I render the categories.

List(categories) { category in
    CategoryPickerRowView(category: category, isSelected: category.id == self.transaction.categoryId)
        .onTapGesture { self.transaction.categoryId = category.id }
}

Update: I noticed when the first letter is typed or deleted (when it is slow) I get this message in the logs

[Snapshotting] Snapshotting a view (0x7fb6bd4b8080, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.

Upvotes: 0

Views: 1753

Answers (3)

Michael St Clair
Michael St Clair

Reputation: 6615

By adding .id(UUID()) to the list it fixes the problem.

List(categories) { category in
    CategoryPickerRowView(category: category, isSelected: category.id == self.transaction.categoryId)
        .onTapGesture { self.transaction.categoryId = category.id }
}.id(UUID())

Description found here: https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui

Upvotes: 1

Josh Homann
Josh Homann

Reputation: 16327

You can use Combine to debounce your search so it happens only after the user stops typing. You can also use Combine to move your filter to the background and then move your assign back to the main queue.

class Model: ObservableObject {
  @Published var searchResults: [String] = []
  let searchTermSubject = CurrentValueSubject<String, Never>("")
  let categorySubject = CurrentValueSubject<String, Never>("")
  private var subscriptions = Set<AnyCancellable>()
  init() {
    Publishers
      .CombineLatest(
        searchTermSubject
          .debounce(for: .milliseconds(250), scheduler: RunLoop.main),
        categorySubject
      )
      .receive(on: DispatchQueue.global(qos: .userInteractive))
      .map { combined -> [String] in
        // Do search here
      }
      .receive(on: RunLoop.main)
      .assign(to: \.searchResults, on: self)
      .store(in: &subscriptions)
  }
}

Upvotes: 1

Shashank
Shashank

Reputation: 21

Use textfield delegate : -

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
    {
if string.isEmpty
        {
            search = String(search.dropLast())
}else{
      search=textField.text!+string
}

and use ".contain" to search name. eg: -

var search:String = ""
var searchData:NSArray?

let filtered = rewardArray?.filter { ($0.name .lowercased()).contains(search.lowercased()) }


searchData = filtered as NSArray?

and reload your collection or table with searchData

Upvotes: -1

Related Questions