Hi,
if you have an array of object IDs, with CoreData you can fetch all of the objects for those IDs at once with the right predicate. It would look something like this:
let objectIDs: [NSManagedObjectID] = [id1,id2,id3,id4]
let predicate = NSPredicate(format: "self in %@", objectIDs)
Can something similar be created with a SwiftData predicate? For now I fetch all ids one by one and as you might guess, performance wise this is not great.
Post
Replies
Boosts
Views
Activity
Hi, I am trying to create somethings like an infinite scroll view with SwiftData but I am not able to figure out how to append elements to the @Query result. Let's say I have 1000 entries in my DB but I only want to load 40 at a time. If the user scrolls to the last element of the result I want to load the next batch of 40 entries.
That is the code:
import SwiftData
import OSLog
private let logger = Logger(subsystem: "SetsTab", category: "SetsTab")
struct SetsTab: View {
@Environment(\.modelContext) private var modelContext
@Binding var searchText: String
@Query private var sets: [SetModel]
@State private var fetchLimit = 40
@State private var offset = 0
init(searchText: Binding<String>) {
_searchText = searchText
if searchText.wrappedValue.isEmpty {
var fetchDesc = FetchDescriptor<SetModel>()
fetchDesc.fetchLimit = fetchLimit
fetchDesc.fetchOffset = offset
_sets = Query(fetchDesc)
} else {
let term = searchText.wrappedValue
let setCodePred = #Predicate<SetModel> {
$0.name.contains(term)
}
var fetchDesc = FetchDescriptor<SetModel>(predicate: setCodePred)
fetchDesc.fetchLimit = fetchLimit
fetchDesc.fetchOffset = offset
_sets = Query(fetchDesc)
}
}
var body: some View {
ScrollView{
LazyVStack{
ForEach(sets){actSet in
VStack{
Text("Set: \(actSet.name)")
}
.onAppear {
self.loadMoreContentIfNeeded(currentItem: actSet)
}
}
}}
.onAppear {
for i in 1...1000 {
let set = SetModel(name: "Set \(i)")
modelContext.insert(set)
}
try! modelContext.save()
}
}
func loadMoreContentIfNeeded(currentItem item: SetModel?) {
if sets.endIndex < self.fetchLimit
{
return
}
guard let item = item else {
loadMoreContent()
return
}
let thresholdIndex = sets.index(sets.endIndex, offsetBy: -5)
if sets.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
loadMoreContent()
}
}
private func loadMoreContent() {
self.offset += self.fetchLimit
loadMoreSets()
}
private func loadMoreSets(){
logger.info("Loading more Sets with offset \(self.offset)")
let term = searchText
let setCodePred = #Predicate<SetModel> {
$0.name.contains(term)
}
do{
var fetchDesc = FetchDescriptor<SetModel>(predicate: setCodePred)
fetchDesc.fetchLimit = self.fetchLimit
fetchDesc.fetchOffset = self.offset
var newBatchOfSets = try modelContext.fetch(fetchDesc)
logger.info("new Batch of Sets count: \(newBatchOfSets.count)")
// sets.append(contentsOf: newBatchOfSets)
}
catch{
logger.error("\(error.localizedDescription)")
}
}
}
The logic for registering when to load the new batch works so loadMoreSets() is called correctly but to actually fetch the new batch and appending to the already fetched data is missing.
I commented this part: sets.append(contentsOf: newBatchOfSets)
because it is giving me the error:
Cannot use mutating member on immutable value: 'sets' is a get-only property
so basically telling me that the @Query var sets can only be set inside the initializer.
So how can we realise something like an infinite scroll view with SwiftData? I feel that this is an valid and for me mandatory use case when working with thousands of db entries in order to be performant.
Has anybody realised this in a different way?
Best regards,
Sven
Hi,
has anybody managed to get two sqlite stores working? If I define the stores with a configuration for each it seems like that only the first configuration and and therefore the store is recognised.
This is how I define the configuration and container:
import SwiftData
@main
struct SwiftDataTestApp: App {
var modelContainer: ModelContainer
init() {
let fullSchema = Schema([
SetModel.self,
NewsModel.self
])
let setConfiguration = ModelConfiguration(
"setconfig",
schema: Schema([SetModel.self]),
url: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("Sets.sqlite"),
readOnly: false)
let newsConfiguration = ModelConfiguration(
"newsconfig",
schema: Schema([NewsModel.self]),
url: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("News.sqlite"),
readOnly: false)
modelContainer = try! ModelContainer(for: fullSchema, configurations: [setConfiguration,newsConfiguration])
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(modelContainer)
}
}
ContentView is just a basic TabView with a tab for news and a tab for sets.
If I run the program this way the sets tab is shown correctly but switching to News fails. If I change the order of the configurations and write the one for news first like this:
modelContainer = try! ModelContainer(for: fullSchema, configurations: [newsConfiguration, setConfiguration])
then the news tab is shown correctly and switching to sets tab fails.
NewsModel and SetModel only differ in the class name
Import Foundation
import SwiftData
@Model
public class NewsModel{
public var name: String
init(name: String) {
self.name = name
}
}
Also the tab content differs only for referencing the respecting model and the name:
import SwiftData
struct NewsTab: View {
@Query private var news: [NewsModel]
@Environment(\.modelContext) private var modelContext
var body: some View {
ScrollView{
LazyVStack{
ForEach(news){actNews in
Text("Hello, News \(actNews.name)")
}
}
.onAppear {
let news = NewsModel(name: "News from \(Date())")
modelContext.insert(news)
try! modelContext.save()
}
}
}
}
The error message is "NSFetchRequest could not locate an NSEntityDescription for entity name 'NewsModel'" (and SetsModel respectively when change the order of the configuration)
Do I explicitly need to tell the modelContext which configuration it should use or is this done automatically?
I'm a little lost here and hope someone can help me.
Best regards,
Sven