I tried to use the value of AppStore in CoreData's predicate.
struct MyView: View {
#AppStorage("valueFilter") private var valueFilter = ""
#Environment(\.managedObjectContext) var moc
#FetchRequest(
entity: Value.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "value_filter == %#", valueFilter) // report error in this line.
) var values: FetchedResults<Value>
...
}
But it report Cannot use instance member 'valueFilter' within property initializer; property initializers run before 'self' is available in predicate line.
Related
I just cannot figure out how to pass core data values of an item from ForEach list in NavigationLink to Detail view. Here is the code that got error: "Cannot convert value of type 'FetchedResults.Element' (aka 'FileEnt') to expected argument type 'FileViewModel'"
struct FileList: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: FileEnt.entity(), sortDescriptors: [NSSortDescriptor(key: "fileName", ascending: false)]) var results: FetchedResults<FileEnt>
#ObservedObject var fileData : FileViewModel
var body: some View {
NavigationView {
List {
ForEach(results) { aFile in
NavigationLink(destination: FileDetails(fileData: aFile), label: {
// ** error on red-underscored aFile above.
Text(aFile.fileName ?? "")
})
}
}.navigationBarTitle("Files")
}
}
}
FileViewModel is for Add, Edit and Detail views. Here is its simplified version for the question:
class FileViewModel: ObservableObject {
#Published var fileName = ""
init(){
}
func DetailItem(fileItem: FileEnt){
fileName = fileItem.fileName ?? ""
}
}
FileDetails:
struct FileDetails: View {
#ObservedObject var fileData : FileViewModel
#Environment(\.managedObjectContext) var context
#State var isEdit = false
var body: some View {
VStack {
Form {
HStack {
Text("File Name:")
Spacer()
Text(fileData.fileName)
....
Modified the post for easy understanding. Thanks for any advice.
G'day everyone,
I'm trying to work out how CoreData relationships can work with UI elements like pickers.
At the moment I have a 3 view app (based on the Xcode boilerplate code) which displays a list of parent entities, which have children which have children. I want a picker to select which grandchild a child entity should refer to.
At the moment I have two funny side effects:
When I run the app as a preview (so there is pre-populated data... this sample code will break without the data in place),
the selected grandchild in the picker is the grandchild of the first
child, irrespective of which child you're dropped into in the first
view.
When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
When I select a child and "save" that, the value in the child summary does not change, until I click another child at which point the value changes before the transition to the modal view.
I am clearly missing something in my understanding of the sequence of events when presenting modals in SwiftUI... can any what shed any light on what I've done wrong?
Here's a video to make this more clear:
https://github.com/andrewjdavison/Test31/blob/main/Test31%20-%20first%20click%20issue.mov?raw=true
Git repository of the sample is https://github.com/andrewjdavison/Test31.git, but in summary:
Data Model:
View Source:
import SwiftUI
import CoreData
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#Binding var licence: Licence
#Binding var showModal: Bool
#State var selectedElement: Element
#FetchRequest private var elements: FetchedResults<Element>
init(currentLicence: Binding<Licence>, showModal: Binding<Bool>, context: NSManagedObjectContext) {
self._licence = currentLicence
self._showModal = showModal
let fetchRequest: NSFetchRequest<Element> = Element.fetchRequest()
fetchRequest.sortDescriptors = []
self._elements = FetchRequest(fetchRequest: fetchRequest)
_selectedElement = State(initialValue: currentLicence.wrappedValue.licenced!)
}
func save() {
licence.licenced = selectedElement
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $selectedElement, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
}
}
Text("Selected: \(selectedElement.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
struct RegisterView : View {
#Environment(\.managedObjectContext) private var viewContext
#State var showModal: Bool = false
var currentRegister: Register
#State var currentLicence: Licence
init(currentRegister: Register) {
currentLicence = Array(currentRegister.licencedUsers! as! Set<Licence>)[0]
self.currentRegister = currentRegister
}
var body: some View {
VStack {
List {
ForEach (Array(currentRegister.licencedUsers! as! Set<Licence>), id: \.self) { licence in
Button(action: {currentLicence = licence; showModal = true}) {
HStack {
Text("\(licence.leasee!) : ")
Text("\(licence.licenced!.desc!)")
}
}
}
}
}
.sheet(isPresented: $showModal) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Register.id, ascending: true)],
animation: .default)
private var registers: FetchedResults<Register>
var body: some View {
NavigationView {
List {
ForEach(registers) { register in
NavigationLink(destination: RegisterView(currentRegister: register)) {
Text("Register id \(register.id!)")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
[1]: https://i.stack.imgur.com/AfaNb.png
I didn't really understand this
• selected grandchild in the picker is the grandchild of the first child, irrespective of which child you're dropped into in the first view.
• When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
Could you attach a video that represents a problem?
But I can give you a solution to the preview problem and the second one.
Preview
If you use preview with Core Data, you need to use a viewContextcreated with MockData and pass it to your View. Here I provide a generic code, that can be modified for each of your views:
In your Persistance struct (CoreData Manager) declare a variable preview with your preview Items:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// Here you create your Mock Data
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Make sure it has inMemory: Bool in its init, as it is responsible for separating real viewContext and previewContext:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
Create Mock Item from your viewContext and pass it to preview:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItems = try! context.fetch(request)
YourView(item: fetchedItems)
}
}
If you use #FetchRequest and #FetchedResults it makes it easier, as they will do creating and fetching objects for you. Just implement a preview like this:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
YourView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Here is Persistence struct created by Xcode at the moment of the project initialization:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Second problem
Core Data objects are built with classes, so their type is a reference. When you change a property is a class it doesn't notifiy the view struct to redraw with a new value. (exception is classes, that are created to notify about changes.)
You need to explicitly tell your RegisterView struct to redraw itself after you dismiss your LicenceView. You can do it by creating one more variable in your RegisterView - #State var id = UUID(). Then attach an .id(id) modifier at the end of your VStack
VStack {
//your code
}.id(id)
Finally, create a function viewDismissed which will change the id property in your struct:
func viewDismissed() {
id = UUID()
}
Now, attach this function to your sheet with an optional parameter onDismiss
.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
OK. Huge vote of thanks to Lorem for getting me to the answer. Thanks too for Roma, but it does turn out that his solution, whilst it worked to resolve one of my key problems, does introduce inefficiencies - and didn't resolve the second one.
If others are hitting the same issue I'll leave the Github repo up, but the crux of it all was that #State shouldn't be used when you're sharing CoreData objects around. #ObservedObject is the way to go here.
So the resolution to the problems I encountered were:
Use #ObservedObject instead of #State for passing around the CoreData objects
Make sure that the picker has a tag defined. The documentation I head read implied that this gets generated automatically if you use ".self" as the id for the objects in ForEach, but it seems this is not always reliable. so adding ".tag(element as Element?)" to my picker helped here.
Note: It needed to be an optional type because CoreData makes all the attribute types optional.
Those two alone fixed the problems.
The revised "LicenceView" struct is here, but the whole solution is in the repo.
Cheers!
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var licence: Licence
#Binding var showModal: Bool
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Element.desc, ascending: true)],
animation: .default)
private var elements: FetchedResults<Element>
func save() {
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $licence.licenced, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
.tag(element as Element?)
}
}
Text("Selected: \(licence.licenced!.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
I am trying to compose a dynamic filter in my Codre Data filters like this (using SwiftUI):
#State var NumberOfRooms: Int
init(NumberOfRooms: Int) {
self.NumberOfRooms = NumberOfRooms
}
#FetchRequest(entity: Listing.entity(),sortDescriptors:[NSSortDescriptor(keyPath: \Listing.publishdate, ascending: false),],predicate: NSPredicate(format: "category == %# AND rooms ", "House", "\($NumberOfRooms)"))
In the view i want to modify the value of the variable like this:
Text("2")
.onTapGesture {
self.NumberOfRooms = 2
}
I get the error at NSPredicate:
Cannot use instance member '$NumberOfRooms' within property initializer; property initializers run before 'self' is available
Basically i want to dynamically compose the predicates.
Any help is deeply appreciated.
Initialize your request inside the init like this.
var fetchRequest: FetchRequest<Listing>
#State var NumberOfRooms: Int = 10
init(NumberOfRooms: Int) {
fetchRequest = FetchRequest<BlogIdea>(entity: Listing.entity(),sortDescriptors:[],predicate: NSPredicate(format: "category == %# AND rooms ", "House", "\(NumberOfRooms)"))
self.NumberOfRooms = NumberOfRooms
}
I cannot figure out how to tie in the #State var to a picker so that #FetchRequest will update.
This code compiles, but changing the picker selection does nothing to fetchRequest, because it's not calling the init. All kinds of other variants have failed mostly.
How do I accomplish this?
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var selectedSkill: Skill? = nil
#State private var pickerSelection: Int
#FetchRequest var skills: FetchedResults<Skill>
init(pickerSelection: Int) {
self._pickerSelection = State(initialValue: pickerSelection)
self._skills = FetchRequest(
entity: Skill.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Skill.value, ascending: true)],
predicate: NSPredicate(format: "apparatus == %d", pickerSelection)
)
}
There are a few ways to go about this here is mine. Not sure of your intended use of Skill but I think you can figure out how to make it work for you.
I would make apparatus an enum
enum Apparatus: Int, CaseIterable, CustomStringConvertible, Identifiable {
case vault, unevenBars, balanceBeam, all
var id: String { self.description }
var description: String {
switch self {
case .vault: return "Vault"
case .unevenBars: return "Uneven Bars"
case .balanceBeam: return "Balance Beam"
case .all: return "All"
}
}
}
extend on Item
extension Item {
var unwrappedApparatus: Apparatus {
Apparatus(rawValue: Int(apparatus)) ?? Apparatus.vault
}
var unwrappedName: String {
name ?? "Unknown"
}
}
Set your #State in ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedApparatus = Apparatus.vault
var body: some View {
List {
Picker("Apparatus", selection: $selectedApparatus) {
Text("Vault").tag(Apparatus.vault)
Text("Uneven Bars").tag(Apparatus.unevenBars)
Text("Balance Beam").tag(Apparatus.balanceBeam)
Text("All").tag(Apparatus.all)
}
Text(selectedApparatus.description)
ItemsView(selectedApparatus: $selectedApparatus) // <-- View changing on selectedApparatus
}
}
}
Now break out the View you want to change based on the selectedApparatus to its own View
struct ItemsView: View {
#FetchRequest private var items: FetchedResults<Item>
init(selectedApparatus: Binding<Apparatus>) {
self._items = FetchRequest(entity: Item.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)],
predicate: selectedApparatus.wrappedValue == .all ? nil : NSPredicate(format: "apparatus == %d", selectedApparatus.wrappedValue.rawValue))
}
var body: some View {
ForEach(items) { item in
Text(item.unwrappedName)
}
}
}
edit: so far i think i've narrowed the problem done to the fetchresult in listview.
when the fetchrequest(predicate) changes, it doesn't refetch.
i'm building a todo app with swiftUI and Core Data.
in the listView i have a button that'll change the predicate for the fetchrequest, but i can't get it to work.
code in the listView
struct HomeListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ItemEntity.loadItems()) var items: FetchedResults<ItemEntity>
Text("someText")
.onTapGesture {
ItemEntity.predicateType.next() //next() is a method defined in core data entity class
}
code in the core data entity class
extension ItemEntity {
static var sortType = sort.ascending
static var predicateType = predicate.all
static func loadItems() -> NSFetchRequest<ItemEntity> {
var request: NSFetchRequest<ItemEntity> = ItemEntity.fetchRequest() as! NSFetchRequest<ItemEntity>
var predicate: NSPredicate {
switch predicateType {
case .all:
return NSPredicate(format: "type = %i", 0)
case .imur:
return NSPredicate(format: "type = %i", 0)
case .unimur:
return NSPredicate(format: "type = %i", 1)
case .imunur:
return NSPredicate(format: "type = %i", 2)
case .unimunur:
return NSPredicate(format: "type = %i", 3)
case .entry:
return NSPredicate(format: "type = %i", 4)
}
request.sortDescriptors = [sortDescriptor]
request.predicate = predicate
return request
}
enum predicate {
case all
case imur
case unimur
case imunur
case unimunur
case entry
mutating func next() {
switch self {
case .all:
self = .imur
case .imur:
self = .unimur
case .unimur:
self = .imunur
case .imunur:
self = .unimunur
case .unimunur:
self = .entry
case .entry:
self = .all
}
}
}
}
the idea is when user tap on the button on list view, it call the next() method and set predicateType property in ItemEntity class to another value, then the predicate property in loadItems() will update the fetchrequest, then the listview will reload.
i know there is something wrong with this approach, but i can't figure out how to fix it.
Thanks for helping!
You need to change some state so that the view re-renders.
Try adding a #State var, change it from the button action and use it in the view:
struct HomeListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ItemEntity.loadItems()) var items: FetchedResults<ItemEntity>
#State private var refresh: Bool = false // add a state
var body: some View {
Text("someText")
.onTapGesture {
ItemEntity.predicateType.next()
refresh.toggle() // change the state
}
.background(toggle ? Color.clear : Color.clear) // use the state
}
}