I want to create a custom Cell for my app with swiftUI and i can't put it in textView. I dont want to use ForEach.
In my core data i have 1 entity: FacturationCoreData.
In this entity i have 3 attributes: Article, Price, Quantity.
When I use Text(facture.price), I cant find price in attribute in my core data and i have 3 errors:
1.
Initializer 'init(_:)' requires that 'Binding' conform to
'StringProtocol'
Referencing subscript 'subscript(dynamicMember:)' requires wrapper
'Binding<FetchRequest.Configuration>'
Value of type 'FetchedResults' has no dynamic
member 'price' using key path from root type
'FetchRequest.Configuration'
4.Insert '$'
struct FactureRow: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: FacturationCoreData.entity(), sortDescriptors: []) private var factures: FetchedResults<FacturationCoreData>
var body: some View {
VStack(alignment: .leading) {
Text(factures.price)
}
}
}
struct FactureRow_Previews: PreviewProvider {
static var previews: some View {
FactureRow().previewLayout(.sizeThatFits)
}
}
Related
In a parent view, I have this:
LongPressEditableText(contents: "\(workout.name ?? "")", context: workout, keyPath: \WorkoutEntity.name)
referencing a string field of a WorkoutEntity in CoreData.
The LongPressEditableText is to be a component which is usually just a Text(), but when long pressed, becomes a TextField with the same contents, editable. On submit it should update the UI (it does this fine), but it should also save the new value to the appropriate spot in CoreData.
struct LongPressEditableText: View {
#State var contents: String
#Environment(\.managedObjectContext) private var viewContext
var context: NSObject
var keyPath: KeyPath<NSObject, String?>
#State var inEditMode: Bool = false
var body: some View {
if inEditMode {
TextField("test", text: $contents)
.onSubmit {
context[keyPath: keyPath] = contents
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
inEditMode.toggle()
}
} else {
Text(contents)
.onLongPressGesture {
inEditMode.toggle()
}
}
}
}
At the moment, I get two errors. In my parent view Cannot convert value of type 'KeyPath<WorkoutEntity, String?>' to expected argument type 'KeyPath<NSObject, String?>' and in the LongPressEditableText view Cannot assign through subscript: key path is read-only
I can solve the first by forcing KeyPath but that's not a solution as I want the editable field to work with a number of different entities with string fields, so I'd like it to be generic. The second I am stumped about, this is as close as I've been able to get to success.
"Generics isn’t my primary concern...", yes it is because it is a very helpful solution here that tells the compiler and runtime what type of object is used in the text field.
First of all since this is Core Data we shouldn't use NSObject but instead NSManagedObject so lets make the view generic with a type that inherits from NSManagedObject and then use the generic type inside for the properties.
struct LongPressEditableText<ManagedObject: NSManagedObject>: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var contents: String = ""
#State var object: ManagedObject
var keyPath: ReferenceWritableKeyPath <ManagedObject, String?>
Notice that the property object (context in your code) is declared to be of the generic type and that the keyPath is also defined to hold the same type. I have also changed from KeyPath to ReferenceWritableKeyPath since the generic type is a class and we want to use the key path to update the object.
And to use the field here is an example, since the view is generic the compiler can deduct that the generic type is Item and also check that it has a property text
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
VStack {
LongPressEditableText(object: item, keyPath: \.text)
}
.padding()
}
}
I have a SwiftUI view where a Core Data NSManagedObject is passed in (without #FetchRequest) and where the subview needs to show the one-to-many relationship from the first entity. In this case, a Person is passed in, and I want to show the Tags associated with that Person.
struct CJMapsCalloutView: View {
#ObservedObject var personAddress: PersonAddress
var body: some View {
VStack(alignment: .leading, spacing: 8.0) {
if let personTags = personAddress.person?.getSortedListOfAssignedTags() {
ContactTagsList(personTags: personTags)
}
}
}
The ContactTagsList
struct ContactTagsList: View {
#State var personTags: [Tag]
var body: some View {
HStack(spacing: 0.0) {
ForEach(personTags) { (tag: Tag) in
ContactTag(personTag: tag)
}
.padding(.leading, 0).padding(.trailing, 2)
}
}
When using #State, it displays the tags list just fine, and even changes to individual Tags are reflected automatically in the view as well. But any changes to the 'relationship', whether adding or removing tags from the Person, don't cause the view to update.
I tried using #ObservedObject instead of #State for the var personTags, but that gives errors at compile time:
Property type '[Tag]' does not match that of the 'wrappedValue'
property of its wrapper type 'ObservedObject'
I'm new to SwiftUI, so I'm not sure what the issue is with using #ObservedObject. And if I can't use that, what's the best way to handle this situation to get automatic updates to the relationship?
Trying to implement core data in SwiftUI, I've run into a wall.
Following many tutorials, I wrote the following project, presumably exactly as instructed but the app won't build.
I'm stuck at a very early stage, so I'm hoping someone can help here.
I created 2 entities in my XCDtatamodel, each with a string property called "airportName"
I simply try to display a list of one of the entities :
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Takeoffs.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Takeoffs.eventDate, ascending: false)]) var fetchedTakeoffs: FetchedResults<Takeoffs>
var body: some View {
List {
ForEach (fetchedTakeoffs, id: \.self) { item in
Text(item.airportName) // THIS IS WHERE I GET THE ERROR
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
But Xcode tells me that "Value of type 'NSManagedObject' has no member 'airportName'"
It's like my XCDatamodel is not connected to the app.
I created the project by checking the SwiftUI, Use CoreData checkboxes.
The whole code can be found here :
https://github.com/Esowes/RecentExp
Thanks for any pointers.
I am able to build it. But not sure it would run as you expected it to. Please go through. I think properties of the takeOff items are optional you were getting error.
List {
ForEach (fetchedTakeoffs, id: \.self) { item in
Text(item.airportName ?? "")
}
}
Is it possible to use a core data record in a predicate inside the #FetchRequest property wrapper in SwiftUI?
I have a list of Project and a list of Tasks. I want to tap on a project and navigate to a list of related tasks for that project. I can't seem to find a way to pass in the parent project in a way that SwiftUI can see before the #FetcheRequest is initialized.
I tried placing the parent project in an EnvironmentObject. This is called when I navigate from the ProjectListView to the TaskListView.
TaskListView()
.environment(\.managedObjectContext, self.managedObjectContext)
.environmentObject(self.projectToEdit)
Then in the TaskListView I added tried this:
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var parentProject: Project
#FetchRequest(
entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.name, ascending: true)
],
predicate: NSPredicate(format: String(format: "%#%#", "taskProject", " == %#"), parentProject)
) var tasks: FetchedResults<Task>
I get the following error on the line with the predicate.
Cannot use instance member 'parentProject' within property initializer; property initializers run before 'self' is available
So is there a way to write a predicate in some way that can use the parent project? Passing the project to the task view does not seem like it's going to work. How else would I go about using a record in a predicate like this?
The FetchRequest can be dynamically created in the init method. That way you can vary predicate and sort conditions. Here is some sample code to achieve that.
// sample Project class
class Project:NSManagedObject {
var id : String
var name : String
}
// sample Task class
class Task:NSManagedObject {
var id : String
var prjId : String
var name : String
}
// Task List View
struct TaskListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
private var tasksRequest: FetchRequest<Task>
private var tasks: FetchedResults<Task> { tasksRequest.wrappedValue }
private var project:Project
// init Task with Project
init(_ project:Project) {
self.project = project
// create FetchRequest
self.tasksRequest = FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(key: "name", ascending:true)],
predicate: NSPredicate(format: "prjId == %#", project.id))
}
var body: some View {
VStack {
Section(header: Text("Tasks under \(project.name):")) {
// access the fetched objects
ForEach(tasks, id:\.id) { task in
Text("\(task.name)")
}
}
}
}
}
Then the call to TaskListView() would look like:
// call to TaskListView
TaskListView(self.projectToEdit)
.environment(\.managedObjectContext, self.managedObjectContext)
I'm trying to make an edit form that can take a value as a #Binding, edit it, and commit the change. In this case, I'm populating a list with Core Data records using the #FetchRequest property wrapper. I want to tap on a row to navigate from the List to a Detail view, then on the Detail view I want to navigate to the Edit view.
I tried doing this without the #Binding and the code will compile but when I make an edit, it is not reflected on the previous screens. It seems like I need to use #Binding but I can't figure out a way to get a NSManagedObject instance inside of a List or ForEach, and pass it to a view that can use it as a #Binding.
List View
struct TimelineListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
// The Timeline class has an `allTimelinesFetchRequest` function that can be used here
#FetchRequest(fetchRequest: Timeline.allTimelinesFetchRequest()) var timelines: FetchedResults<Timeline>
#State var openAddModalSheet = false
var body: some View {
return NavigationView {
VStack {
List {
Section(header:
Text("Lists")
) {
ForEach(self.timelines) { timeline in
// ✳️ How to I use the timeline in this list as a #Binding?
NavigationLink(destination: TimelineDetailView(timeline: $timeline)) {
TimelineCell(timeline: timeline)
}
}
}
.font(.headline)
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Lists"), displayMode: .inline)
}
} // End Body
}
Detail View
struct TimelineDetailView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Binding var timeline: Timeline
var body: some View {
List {
Section {
NavigationLink(destination: TimelineEditView(timeline: $timeline)) {
TimelineCell(timeline: timeline)
}
}
Section {
Text("Event data here")
Text("Another event here")
Text("A third event here")
}
}.listStyle(GroupedListStyle())
}
}
Edit View
struct TimelineEditView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var newDataValue = ""
#Binding var timeline: Timeline
var body: some View {
return VStack {
TextField("Data to edit", text: self.$newDataValue)
.shadow(color: .secondary, radius: 1, x: 0, y: 0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear {
self.newDataValue = self.timeline.name ?? ""
}.padding()
Spacer()
}
.navigationBarItems(
leading:
Button(action: ({
// Dismiss the modal sheet
self.newDataValue = ""
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Cancel")
},
trailing: Button(action: ({
self.timeline.name = self.newDataValue
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
// Dismiss the modal sheet
self.newDataValue = ""
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Done")
}
)
}
}
I should mention, the only reason I'm even trying to do this is because the modal .sheet() stuff is super buggy.
To implement creation and editing functionality with Core Data it is best to use nested managed object contexts. If we inject a child managed object context, derived from the main view context, as well as the managed object being created or edited that is associated with a child context, we get a safe space where we can make changes and discard them if needed without altering the context that drives our UI.
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = viewContext
let childItem = childContext.object(with: objectID) as! Item
return ItemHost(item: childItem)
.environment(\.managedObjectContext, childContext)
Once we are done with our changes, we just need to save the child context and the changes will be pushed up to the main view context and can be saved right away or later, depending on your architecture. If we are unhappy with our changes we can discard them by calling rollback() on our child context.
childContext.rollback()
Regarding the question of binding managed objects with SwiftUI views, once we have our child object injected into our edit form, we can bind its properties directly to SwiftUI controls. This is possible since NSManagedObject class conforms to ObservableObject protocol. All we have to do is mark a property that holds a reference to our child object with #ObservedObject and we get its bindings. The only complication here is that there are often type mismatches. For example, managed objects store strings as String?, but TextField expects String. To go around that we can use Binding’s initializer init?(_ base: Binding<Value?>).
We can now use our bindings, provided that the name attribute has a default empty string set in the managed object model, or else this will crash.
TextField("Title", text: Binding($item.title)!)
This way we can cleanly implement the SwiftUI philosophy of no shared state. I have provided a sample project for further reference.
#Binding only works with structs.
But CoreData result are Objects (NSManagedObject adopting ObservableObject). You need to use #ObservedObject to register for changes.