SwiftUI: Change ForEach source or fetch request using Picker (Segmented) - ios

I'm new to SwiftUI and coding in general, so sorry if this has being covered before. I did search about it but I couldn't find it clear enough.
I'm trying to make an app for storing notes and tasks. (I'm not planning to put it on the store, I'm too newbie for that, but I do want to learn Swift and working on an actual app is more useful to me than reading about it.) I have an entity called "Base" with 8 attributes and automatic Codegen Class Definition selected. I prefer to keep it that way, no manual please.
I have three fetch requests. One gets me all data from Core Data. The other two filter one attribute called campoEstado. For now in campoEstado I only store one of two possible values, as strings: "Activas", and "Pospuestas". In the future I may add more so I can't use a boolean for this.
I get a List working with one fetch request. But I can't change that source when the app is running.
I made a Picker with .pickerStyle(SegmentedPickerStyle()) that shows me: Todo, Activas, Pospuestas. When the user selects one of this tabs in the picker the list should change to:
Tab 1: All tasks
Tab 2: A filtered list containing only tasks with campoEstado = "Activas" (calls the fetch request filtroActivas)
Tab 3: A filtered list containing only tasks with campoEstado = "Pospuestas" (calls the fetch request filtroPospuestas)
How it should look:
My code in ContentView:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Base.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Base.campoNota, ascending: true),
NSSortDescriptor(keyPath: \Base.campoFechaCreacion, ascending: true)
], predicate: NSPredicate(format: "campoEstado == %#", "Activas")
) var filtroActivas: FetchedResults<Base>
#FetchRequest(entity: Base.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Base.campoNota, ascending: true),
NSSortDescriptor(keyPath: \Base.campoFechaCreacion, ascending: true)
], predicate: NSPredicate(format: "campoEstado == %#", "Pospuestas")
) var filtroPospuestas: FetchedResults<Base>
#FetchRequest(entity: Base.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Base.campoNota, ascending: true),
NSSortDescriptor(keyPath: \Base.campoFechaCreacion, ascending: true)
]
) var bases: FetchedResults<Base>
var body: some View {
NavigationView {
List {
Picker("Solapas", selection: $selectorIndex) {
//This should connect to ForEach source, currently "bases"
}
.pickerStyle(SegmentedPickerStyle())
ForEach(bases, id: \.self) { lista in
NavigationLink(destination: VistaEditar(base: lista)) {
Rectangle()
.foregroundColor(.gray)
.frame(width: 7, height: 50)
VStack(alignment: .leading) {
Text(lista.campoNota ?? "Unknown title")
.font(.headline)
Text(lista.campoEstado ?? "Activas")
}
}
}
I have two problems:
I don't know how to connect the selected tab in the picker with the source of ForEach inside List
I made the list work for one fetch request, the one that brings me every record in Core Data, but I don't know how to change the ForEach source when the app is running. I have tried an array of names of variables for every one of the fetch requests variables names (bases, filtroActivas and filtroPospuestas) putting them in [] but that didn't work.
I know this isn't elegant, I just need it to work first and then go for efficiency and elegance. I'm sure there is some stupid thing I'm not seeing but it's been a week and I'm getting desperate.
I hope I was clear, if you need more information please ask.
If anyone can help me I would very much appreciate it. Thanks in advance!

I worry I may get downvoted for this because it's not an entirely complete answer. But I'm out of time and I wanted to see if I could get you in the right direction.
What I believe you're looking for is an Array of Arrays. A FetchedResult will return an optional array, which you'll want to ensure isn't nil. I used compactMap in my example to generate non-nil array's of my sample optional arrays which are intended to simulate what you are getting back from your #FetchRequest properties. You should be able to load this code into a new project in Xcode to see the results. Again it's not a complete answer but will hopefully help you along in your project. I hope this helps, keep working at it, you'll get it!
struct ContentView: View {
// Sample data to mimic optional FetchedResults coming from CoreData.
// These are your #FetchRequest properties
let fetchedResultsBases: [String?] = ["Red", "Green", "Blue"]
let fetchedResultsFiltroPospuestas: [String?] = ["1", "2", "3"]
let fetchedResultsFiltroActivas: [String?] = ["☺️", "πŸ™ƒ", "😎"]
// Options for your picker
let types = ["Bases", "Pospuestas", "Activas"]
#State private var groups = [[String]]()
#State private var selection = 0
var body: some View {
NavigationView {
VStack {
Picker("Solapas", selection: $selection) {
ForEach(0..<types.count, id: \.self) {
Text("\(self.types[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
List {
ForEach(0..<groups.count, id: \.self) { group in
Text("\(self.groups[self.selection][group])")
}
}
}
}
.onAppear(perform: {
self.loadFetchedResults()
})
} // This is the end of the View body
func loadFetchedResults() {
// This is where you'll create your array of fetchedResults, then add it to the #State property.
var fetchedResults = [[String]]()
// Use compact map to remove any nil values.
let bases = fetchedResultsBases.compactMap { $0 }
let pospuestas = fetchedResultsFiltroPospuestas.compactMap { $0 }
let filtroActivas = fetchedResultsFiltroActivas.compactMap { $0 }
fetchedResults.append(bases)
fetchedResults.append(pospuestas)
fetchedResults.append(filtroActivas)
groups = fetchedResults
}
} // This is the end of the ContentView struct

Related

Core Data / SwiftUI - How to get from a backgroundContext back to the mainContext

I want to use concurrency to perform some expensive core data fetch request in background.
To do so I created a backgroundContext with .newBackgroundContext() and automaticallyMergesChangesFromParent = true
I'm not able to work in the main context with the data I fetched in the backgroundContext. Otherwise the app crashes.
Although the two contexts get synced it seems like I'm bound to the backgroundContext as long as I work with that fetched data, is that right?
And If so, is there any reason why not to perform everything across the whole app in the backgroundContext? That would prevent from accidentally switch to the mainContext.
Or is there any convenient way to get to the mainContext after fetching and processing the data in the backgroundContext?
Here is a small example:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
let bgContext = PersistenceController.shared.container.newBackgroundContext()
#State var items: [Item] = []
var body: some View {
List {
ForEach(items) { item in
Text(date, formatter: itemFormatter)
}
}
.task {
items = await fetchItems(context: bgContext)
}
}
func fetchItems(context: NSManagedObjectContext) async -> [Item] {
do{
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
return try bgContext.fetch(request)
} catch {
return[]
}
}
}
Right now, every further action needs to get done in the backgroundContext, including save and delete functions.
Based on mlhals comment, I found a quite simple Solution.
I perform all expensive fetch requests and calculations in the background.
For the visible elements I use a #FetchRequest property wrapper.
Since it is updating automatically it updates changes made in the background context instantly. Further user actions like deleting etc. I can then proceed in the "main" context.

How to use constant variable in SwiftUI Core Data #FetchRequest?

How can I use #FetchRequest in SwiftUI with a fetch request based on a variable being passed in from a parent view, while also ensuring that the view updates based on Core Data changes?
I have this issue where using #FetchRequest in SwiftUI while passing in a variable to the fetch request doesn't work. It results in the following error:
Cannot use instance member 'name' within property initializer; property initializers run before 'self' is available
struct DetailView: View {
#FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default)
private var data: FetchedResults<Report>
let name: String
var body: some View {
Text("\(data.first?.summary.text ?? "")")
.navigationTitle(name)
.onAppear {
ReportAPI.shared.fetch(for: name) // Make network request to get Report data then save to Core Data
}
}
}
extension Report {
#nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<Report> {
let fetchRequest = NSFetchRequest<Report>(entityName: "Report")
fetchRequest.fetchLimit = 1
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Report.createdAt, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "details.name == %#", name)
return fetchRequest
}
}
This view is pushed using a NavigationLink from a previous view. Like: NavigationLink(destination: DetailView(name: item.name)).
I've looked at this answer and I don't think that it will work for my use case since normally the #FetchRequest property wrapper automatically listens for data changes and updates automatically. Since I'm doing an async call to make a network request and update Core Data, I need the data variable to update dynamically once that is complete.
I've also looked at this answer but I think it has some of the same problems as mentioned above (although I'm not sure about this). I'm also getting an error using that code on the self.data = FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default) line. So I'm not sure how to test my theory here.
Cannot assign to property: 'data' is a get-only property
struct DetailView: View {
#FetchRequest
private var data: FetchedResults<Report>
let name: String
var body: some View {
Text("\(data.first?.summary.text ?? "")")
.navigationTitle(name)
.onAppear {
ReportAPI.shared.fetch(for: name) // Make network request to get Report data then save to Core Data
}
}
init(name: String) {
self.name = name
self.data = FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default)
}
}
Therefore, I don't think that question is the same as mine, since I need to be sure that the view updates dynamically based on Core Data changes (ie. when the fetch method saves the new data to Core Data.

SwiftUI CoreData Filtered List Deletion Fails Unpredictably

I'm struggling with a SwiftUI app with SwiftUI life cycle where I have created a fairly standard Core Data list and have added a search field to filter the list. Two views - one with a predicate and one without. The unfiltered list generation works as expected including swipe to delete. The filtered list appropriately displays the filtered list, but on swipe to delete I get completely unpredictable results. Sometimes the item disappears, sometimes it pops back on the list, sometimes the wrong item is deleted. I cannot discern any pattern.
Here's the view:
struct MyFilteredListView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.horizontalSizeClass) var sizeClass
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \InvItem.name, ascending: true)],
animation: .default) private var invItems: FetchedResults<InvItem>
var fetchRequest: FetchRequest<InvItem>
init(filter: String) {
//there are actually a bunch of string fields included - just listed two here
fetchRequest = FetchRequest<InvItem>(entity: InvItem.entity(), sortDescriptors: [], predicate: NSPredicate(format: "category1 CONTAINS[c] %# || name CONTAINS[c] %# ", filter, filter))
}
var body: some View {
let sc = (sizeClass == .compact)
return List {
ForEach(fetchRequest.wrappedValue, id: \.self) { item in
NavigationLink(destination: InvItemDetailView(invItem: item)) {
InvItemRowView(invItem: item)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: sc ? 100 : 200)
.padding(.leading, 10)
}//link
}
.onDelete(perform: deleteInvItems)
}//list
}
private func deleteInvItems(offsets: IndexSet) {
withAnimation {
offsets.map { invItems[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this - raise an alert
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
Core Data is pretty standard:
class InvItem: NSManagedObject, Identifiable {}
extension InvItem {
#NSManaged var id: UUID
#NSManaged var category1: String?
#NSManaged var name: String
//bunch more attributes
public var wrappedCategory1: String {
category1 ?? "No category1"
}
//other wrapped items
}//extension inv item
extension InvItem {
static func getAllInvItems() -> NSFetchRequest<InvItem> {
let request: NSFetchRequest<InvItem> = InvItem.fetchRequest() as! NSFetchRequest<InvItem>
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [sortDescriptor]
return request
}
}//extension
I'm guessing there is something about the offsets behavior in deleteInvItems(offsets: IndexSet) that I don't understand, but
I have not been able to find anything on this. This same code works as expected in the unfiltered list.
Any guidance would be appreciated. Xcode Version 12.2 beta (12B5018i) iOS 14
First Edit:
I figured out the pattern. Apparently the IndexSet refers to the entire entity, not the filtered items. Say for example that the unfiltered list has 10 items and the filtered list has 3 items. When I delete the third item in the filtered list the result is the deletion of the third item in the unfiltered list, so unless those two are the same, the third item reappears on the filtered list and the third item on the unfiltered list is deleted from Core Data. If I then delete the third item on the filtered list again, the fourth item on the original list is deleted (since the third is already gone).
So the question becomes - how do I get a reference to the object I want
to delete - IndexSet does not work.
It is likely because your ForEach is using a wrappedValue which may or may not be updated.
https://developer.apple.com/documentation/swiftui/binding/wrappedvalue
I suggest you look at the code that is provided when you create a new project (you are using some of it) so you can adjust and have more accurate data.
ForEach(items) { item in
Also, when deleting you are mixing the offset from fetchRequest and items in invItems stick with only one.
If you want to be able to dynamically filter the list I have had better luck using a FetchedResultsController wrapped in an ObservedObject
https://www.youtube.com/watch?v=-U-4Zon6dbE

SwiftUI Struct for Dictionary entry

I have decided after several years of development to restart my project using SwiftUI to future proof as much as I can.
In my current project I have my data in several .CSV's which I then process into dictionaries and then create a list of entries on screen using an Array of keys which are generated programmatically from user input.
All examples I've seen for SwiftUI use JSON. However the structure of these files are identical to an Array of Dictionaries. My question is; is it possible to create a Struct of a dictionary entry to pass in a forEach watching an Array of Keys (data inside the dictionary will never change and I am not looking to iterate or watch the dictionary).
My main goal is to reuse as much as possible but am willing to change what I have to get full benefit of SwiftUI. Obviously if I change the way I store my data almost everything will be useless. If there's a real benefit to converting my data to JSON or even start using something like CoreData I will.
If I'm understanding correctly, you are looking to
Take some user input
Transform that into keys that correspond to your data dictionary
Extract the data for the matching keys into some struct
Display a list of those structs using SwiftUI
Here is a simple implementation of those steps.
import SwiftUI
// A dictionary containing your data
let data: [String: Int] = [
"apples": 5,
"bananas": 3,
"cherries": 12
]
// A struct representing a match from your data
struct Item {
var name: String
var quantity: Int
}
// A view that displays the contents of your struct
struct RowView: View {
var item: Item
var body: some View {
Text("\(item.quantity) \(item.name)")
}
}
struct ContentView: View {
#State private var searchText: String = ""
func items(matching search: String) -> [Item] {
// 2 - split the user input into individual keys
let split = search.split(separator: " ", omittingEmptySubsequences: true).map { $0.lowercased() }
// 3 - turn any matching keys/values in your dictionary to a view model
return split.compactMap { name in
guard let quantity = data[name] else { return nil }
return Item(name: name, quantity: quantity)
}
}
var body: some View {
VStack {
// 1 - get user input
TextField("Search", text: $searchText)
.padding()
// 4 - display the matching values using ForEach (note that the id: \.name is important)
List {
ForEach(items(matching: searchText), id: \.name) { item in
RowView(item: item)
}
}
}
}
}
You'll see that as you type in the text field, if you enter any of the strings "apples", "bananas", or "cherries", a corresponding row will pop into your list.
Depending on the size of your list, and what kind of validation you are performing on your users search queries, you might need to be a little more careful about doing the filtering/searching in an efficient way (e.g. using Combine to only split and search after the user stops typing).

FetchedResults won't trigger and SwiftUI update but the context saves it successfully

I am trying to update an attribute in my Core Data through an NSManagedObject. As soon as I update it, I save the context and it gets saved successfully.
Problem
After the context saves it, the UI (SwiftUI) won't update it with the new value. If I add a completely new Children into Core Data (insert) the UI gets updated.
What I tried:
Asperi approach - I can print out the correct new value in .onReceive but the UI doesn't update
Using self.objectWillChange.send() before context.save() - didn't work either
Changed the Int16 to String, because I was speculating that somehow Int16 is not observable? Didn't work either
Is this a SwiftUI bug?
As-is State
//only updating the data
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchReq = NSFetchRequest<Card>(entityName: "Card") //filter distributorID; should return only one Card
fetchReq.resultType = .managedObjectResultType
fetchReq.predicate = NSPredicate(format: "id == %#", params["id"]!) //I get an array with cards
var cards :[Card] = []
cards = try context.fetch(fetchReq)
//some other functions
cards[0].counter = "3" //
//...
self.objectWillChange.send() //doesn't do anything?
try context.save()
//sucessfully, no error. Data is there if I restart the app and "force" a reload of the UI
//Core Data "Card"
extension Card :Identifiable{
#nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
return NSFetchRequest<Card>(entityName: "Card")
}
//...
#NSManaged public var id: String
//....
}
//Main SwiftUI - data is correctly displayed on the card
#FetchRequest(entity: Card.entity(),
sortDescriptors: [],
predicate: nil)
var cards: FetchedResults<Card>
List {
ForEach(cards){ card in
CardView(value: Int(card.counter)!, maximum: Int(card.maxValue)!,
headline: card.desc, mstatement: card.id)
}
If first code block is a part of ObservableObject, then it does not look that third block, view one, depends on it, and if there is no changes in dependencies, view is not updated.
Try this approach.
But if there are dependencies, which are just not provided then change order of save and publisher as
try context.save()
self.objectWillChange.send()

Resources