Big SwiftUI List on MacOS not fluid - macos

I have a List in SwiftUI App on MacOS with e.g. 10.000 entries.
Trying like the example below is horribly slow.
Adding .id(UUID()) to the List, which was advised in a prior post, makes it a bit quicker but still not fluid.
Even worst, adding .id(UUID()) to the list, the list then cannot be navigates by the arrow-key (up/down).
Is there a better way to achieve this?
struct TestViews_MacOS_BigList: View {
#State var selectedItem: String?
var items: [String]
var body: some View {
List(items,id: \.self, selection: $selectedItem ) { item in
Text("\(item)").tag("\(item)")
}
//.id(UUID())
}
}
func testnames()->[String]{
var list: [String] = []
for i in 1...10000 {
list.append("Sting Nr \(i)")
}
return list
}

Those are too many Views to have sitting around. You need to use CoreData or some other manual way to Batch load items and a way to only have a certain number of Views/items fetched/loaded at a time.
An NSFetchedResultsController that specifies a batch size can help with that
let fetchRequest: NSFetchRequest<Item> = NSFetchRequest<Item>(entityName: "Item")
fetchRequest.includesPendingChanges = false
fetchRequest.fetchBatchSize = 20
When the fetch is executed, the entire request is evaluated and the identities of all matching objects recorded, but only data for objects up to the batchSize will be fetched from the persistent store at a time.
#FetchRequest might do it as well, it was made for SwiftUI so it should compensate but the documentation does not specify.

Try using LazyVStack since it uses memory efficiently as below:
struct TestViews_MacOS_BigList: View {
#State var selectedItem: String?
var items: [String]
var body: some View {
ScrollView {
LazyVStack{
ForEach(items, id: \.self, content: { item in
HStack{
Button(action: {
selectedItem = item
}, label: {
Text("Select ")
})
Text("\(item)").tag("\(item)")
}
})
}
}
}
}

Related

Keystroke lag when typing in Textfield in Table or List

I'm creating a document-based application where data is represented by TextFields in a TableView (it could also be a List, the same issue occurs). When the app SwiftUI app on an Intel MacBook Air, I get a lot of keyboard lag whenever there are more than a dozen rows in my table. It's present on the Apple Studio too, but less noticeable. I've tried changing the table into a List and LazyVStack, but it doesn't seem to make much difference. Using the Swift UI instrument, it looks to me like every TextField on the page is being redrawn on every keystroke, even though their values haven't changed.
I also tried using a custom TextField with a debounce added in (with this as a starting point). This works well for reducing the lag, but I don't think this is how debouncing is intended to be used and I ended up with some strange behaviour.
I suspect that it is rather the case that I've misunderstood how to using #Binding variables in a Document Based application, or possibly I have misconfigured the Struct where I store the data. So here are the essential parts of my code, which will hopefully make it clear where I have gone wrong without having to run anything.
struct ClaraApp: App {
#StateObject var globalViewModel = GlobalViewModel()
var body: some Scene {
DocumentGroup(newDocument: ClaraDocument(claraDoc:GroupVocab())) { file in
MainContentView(data: file.$document)
.environmentObject(self.globalViewModel)
}
}
struct MainContentView: View {
#Binding var data: ClaraDocument // Binding to the document
#EnvironmentObject var globalViewModel : GlobalViewModel
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
HostingWindowFinder { window in
if let window = window {
self.globalViewModel.addWindow(window: window)
print("New Window", window.windowNumber)
self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
window.becomeKey()
}
}
.frame(width:0, height: 0)
VStack{
TabView(selection: $viewModel.activeTab){
VocabView(vocabs: $data.claraDoc.vocabs, selectedVocab: $viewModel.selectedVocab)
.tabItem{
Label("Vocabulary", systemImage: "tablecells")
}
.tag(TabType.vocab)
//more tabs here
}
}
}
}
struct VocabView: View{
#Binding var vocabs: [Vocab] // Binding to just the part of the document concerned by this view
#Binding var selectedVocab: Vocab.ID?
var body: some View{
VStack(){
VocabTable(vocabs: $vocabs, selection: $selectedVocab)
.padding([.top, .leading, .trailing])
HStack{
Button("-"){
if selectedVocab != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selectedVocab!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selectedVocab = vocabs[oldSelectionIndex! - 1].id
} else {
selectedVocab = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
.disabled(selectedVocab == nil)
Text("\(String(vocabs.count)) entries")
Button("+"){
let newVocab = Vocab(id: UUID(), word: "", def: "", trans: "", visNote: "", hidNote: "", link: Link(linked: false), date: Date())
vocabs.append(newVocab)
selectedVocab = newVocab.id
}
}
}
}
}
struct VocabTable: View{
#Binding var vocabs: [Vocab]
#Binding var selection: Vocab.ID?
var body: some View{
Table($vocabs, selection: $selection){
TableColumn("Word") {vocab in
TextField("Word", text: vocab.word)
}
TableColumn("Definition") {vocab in
TextField("Definition", text: vocab.def)
}
TableColumn("Translation") {vocab in
TextField("Translation", text: vocab.trans)
}
TableColumn("Visible note") {vocab in
TextField("Visible note", text: vocab.visNote)
}
TableColumn("Hidden note") {vocab in
TextField("Hidden note", text: vocab.hidNote)
}
TableColumn("Created") {vocab in
HStack{
Text(vocab.date.wrappedValue, style: .date)
Text(vocab.date.wrappedValue, style: .time)
}
}
}
.onDeleteCommand{
if selection != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selection!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selection = vocabs[oldSelectionIndex! - 1].id
} else {
selection = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
}
}
// vocab struct which is contained as an array [Vocab] inside the GroupVocab struct
struct Vocab: Identifiable, Codable, Equatable, Hashable {
let id: UUID
var word: String
var def: String
var trans: String
var visNote: String
var hidNote: String
var date: Date
init(id: UUID = UUID(), word: String? = "", def: String? = "", trans: String? = "", visNote: String? = "", hidNote: String? = "", date: Date? = Date()){
self.id = id
self.word = word ?? ""
self.def = def ?? ""
self.trans = trans ?? ""
self.visNote = visNote ?? ""
self.hidNote = hidNote ?? ""
self.date = date ?? Date()
}
static func == (lhs: Vocab, rhs: Vocab) -> Bool {
return lhs.id == rhs.id && lhs.word == rhs.word && lhs.def == rhs.def && lhs.trans == rhs.trans && lhs.visNote == rhs.visNote && lhs.date == rhs.date
}
}
struct GroupVocab: Codable{
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var groupName: String = ""
var vocabs = [Vocab]()
var learners = [Learner]()
var lessons = [Lesson]()
var startDate = Date()
var endDate = Date()
}
If that doesn't shed any light, here's my attempt at making a minimal example. It isn't nearly as laggy as the actual app, but from what I can tell it exhibits the same problems. Of course, it could be something in my actual app, which is not present in this minimal example, that I have overlooked. For example, I know that the menu bar is redrawn when editing the document, but removing the menu bar doesn't improve performance. So, I'm assuming that the reduced (albeit still present) lag is due to the general baggage of the program and not one specific element that I haven't taken into account. Obviously if there are no obvious problems in the above, I will need to go back and check everything again, but to my knowledge I have already tried removing and readding each part of the application individually to no avail.
Finally, this is what the actual app looks like in use:
As mentioned in the comments on original post.
TL;DR; For those encountering a similar lag issue, the solution here was to replace the declaration of Vocab as a struct with the use of an ObservableObject class, i.e. Vocab's definition becomes class Vocab: ObservableObject, Identifiable, Codable, Equatable.
Might also want to have a look at https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties if #Published properties in the class have to be Codable
In a bit more detail
When struct Vocab is used each keystroke (because it is a value type) creates a new (original data + change) instance of the Vocab struct.
The problem [0] with this struct new instance is that SwiftUI cannot detect the singular property change and trigger just updating its corresponding TextField [1].
Instead SwiftUI handles each new keystroke driven struct instance as if it is a completely unrelated Vocab object; for which it has to update every TextField in the Table's entry row.
It's the updating of all of the TextFields in the entry row that causes the perceived lag.
By contrast the solution - using an ObservableObject class - enables binding the TextFields to a property on an object where the instance does not change on each keystroke. Consequently SwiftUI is able to detect and update just the individual entry changes.
The final piece in the puzzle is that when using an ObservableObject. The #Published properties that update Views nicely take some extra effort to enable them to conform with the Codeable protocol. For how to add that conformance there is a nice explanation over [here[( https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties)
Other bits
If running on higher spec machines - or with fewer properties - issues like these can be difficult to spot.
Other approaches might be possible. For instance, if it's practicable within the context of the rest of the app, relaxing Vocab's Equatable compliance [1] might be enough to enable SwiftUI to do something more clever when determining what TextFields need recomputing.
[0] In this context; generally though, preferring value types such as structs is seen as good practice because it reduces the risk of unexpected side-effects.
[1] Possibly this might also be addressable by relaxing the implementation of Equatable conformance on the struct to just being based on id equivalence.

Extracting single String/Int from fetched API data list

I am struggling to extract single values (strings and Int) from the data I fetched from an API. I fetch the data in the form of a list:
class apiCall {
func getRockets(completion:#escaping ([Rockets]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let rockets = try! JSONDecoder().decode([Rockets].self, from: data!)
print(rockets)
DispatchQueue.main.async {
completion(rockets)
}
}
.resume()
}
}
Then when I try using it in a View I succeed when using a List to view the values, like it shows the list of the names from the API data. But then when I want to view a single value like this:
import SwiftUI
struct RocketStatistics: View {
#State var rockets: [Rockets] = []
var body: some View {
VStack{
Text(rockets[1].name)
}
.onAppear {
apiCall().getRockets { (rockets) in
self.rockets = rockets
}
}
}
}
struct RocketStatistics_Previews: PreviewProvider {
static var previews: some View {
RocketStatistics()
}
}
It does not even give me an error, but my preview just won't update and keeps crashing.
So my question is how can I extract single values from this API data in List form and use these single values in my whole project?
To keep it simple and make it work first I started just with fetching the "name" from the API:
import Foundation
import SwiftUI
struct Rockets: Codable, Identifiable {
let id = UUID()
let name : String
}
When it all works I would also want to use Integer values from the API in my project, so tips on how to that are also welcome!
Never ever get items by a hard-coded index in a SwiftUI view without any check. When the view is rendered the first time the array is empty and any index subscription will crash.
Always check if the array contains the required number of items. In this case the array must contain at least two items
VStack{
Text(rockets.count > 1 ? rockets[1].name : "")
}

Xcode View - trying to display the name (a var) of a struc as text

I have this model where I have a list of boat, and a list of works (which are linked to a boat). All are linked to the user. My data is stored in Firestore by my repository.
struct boat: Codable, Identifiable {
#DocumentID var id : String?
var name: String
var description: String
#ServerTimestamp var createdtime: Timestamp?
var userId: String?
}
struct Work: Identifiable, Codable {
#DocumentID var id : String?
var title: String
var descriptionpb: String
var urgent: Bool
#ServerTimestamp var createdtime: Timestamp?
var userId: String?
var boatId: String // (boatId = id of struct boat just above)
}
I have a view in which I want to display (and let the user edit) the details of the work (such as the title and descriptionpb), I manage to display the boatId (see below), however I want to display the boat name. How should I go about it?
import SwiftUI
struct WorkDetails: View {
#ObservedObject var wcvm: WorkCellVM
#ObservedObject var wlvm = WorklistVM()
#State var presentaddwork = false
var onCommit: (work) -> (Void) = { _ in }
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(wcvm.work.boatId) // <-- THIS IS WHAT I WANT TO CHANGE INTO boat name instead of boatId
.padding()
TextField("Enter work title", text: $wcvm.work.title, onCommit: {
self.onCommit(self.wcvm.work)
})
.font(.title)
.padding()
HStack {
TextField("Enter problem description", text: $wcvm.work.descriptionpb, onCommit: {
self.onCommit(self.wcvm.work)
})
}
.font(.subheadline)
.foregroundColor(.secondary)
.padding()
}
}
}
}
Essentially you have a Data Model problem, not a SwiftUI problem. I would be keeping all of this in Core Data and linking the various models(Entities in Core Data) with relationships. So your Work(Essentially a work order) would link to the boat that the work was being performed on.
Otherwise, you need to add a Boat as a parameter to Work. Either way would give you the desired syntax, but Core Data is much more efficient. It is also your data persistence model so you would kill two birds with one stone.
Solution found: when creating a work order, I was assigning the boat id, I am now assigning the boat name as well (and calling it in the work order display). Essentially keeping the same code as above, tweaking it a little bit so that it does what I want to do.

Does SwiftUI have a Picker with typing to filter a big list?

I wondering if I need to build this myself or if SwiftUI (or AppKit, I can use NSViewRepresentable) has something like it already. This is for a macOS app.
The user needs to choose from a very large list. In the example below I used animal names. There are dozens or maybe 100's of items. They can type some characters to narrow the list. Then they can click any item to choose it, or hit return to select the highlighted item, which could be the first item in the list, or maybe a recently used item.
Single filtering list written in SwiftUI:
let animals = ["Cat", "Camel", "Dog", "Crocodile"]
struct ContentView: View {
#State private var searchString = ""
var body: some View {
VStack {
TextField("Search", text: $searchString)
List(animals.filter({searchString == "" ? true : $0.contains(searchString)}), id: \.self) { animal in
Text(animal)
}
}
}
}
You can increase the model complexity according to your needs, and apply List selection (see documentation of SwiftUI). To support selection:
struct ContentView: View {
#State private var searchString = ""
#State private var listSelection: String? = ""
var body: some View {
VStack {
TextField("Search", text: $searchString)
List(animals.filter({searchString == "" ? true : $0.contains(searchString)}), id: \.self, selection: $listSelection) { animal in
Text(animal)
}
}.padding()
.onChange(of: searchString, perform: { value in
listSelection = animals.filter({searchString == "" ? true : $0.contains(searchString)}).first
})
}
}

SwiftUI performance issue showing conditional views in Items in a ForEach loop embedded in aScrollView

Working with SwiftUI:
I have a list of views in a ScrollView that I am creating using a ForEach loop. I want to show or hide a number of little flags depending on 4 different Bool properties in the struct I am using as the model for the objects in the list. The problem I'm having is the more If-statements I add, the worse the performance gets. With no If-statements the list loads without a hitch. I'm running into this problem with only 120 items in the list. I would love help figuring out what I'm doing wrong!
Here is an example of the Content View with the ScrollView and loop:
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
ScrollView(.vertical, showsIndicators: true){
VStack(spacing: 10){
ForEach(model.list){ item in
ItemView(item: item)
}
}
}
}// end body
} // end struct
And the Item Views and Struct I'm using as the model.
struct ListData: Identifiable {
var id = UUID()
var title: String
var subtitle: String
var isTrue: Bool
var isAlsoTrue:Bool
}
struct ItemView: View {
var item: ListData
var body: some View {
VStack(alignment: .leading){
Text(item.title)
HStack{
if item.isTrue{
FlagView(text: "True")
}
if item.isAlsoTrue{
FlagView(text: "Also")
}
Text(item.subtitle)
Spacer()
}
.font(.system(size: 12, weight: .semibold))
}
.font(.system(size: 14, weight: .semibold))
}
}
struct FlagView: View {
var text: String
var body: some View {
Text(text)
.padding(2)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 2).foregroundColor(.gray))
}
}
The Actual objects have 4 Bool properties I want to use. Is there a better way to hide or show these flags in my list? Any help figuring out what's wrong with the performance is greatly appreciated!

Resources