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.
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 ?? "")
}
}
I have a view class showing list of items coming from ViewModel class, in picker. Initial state of this picker is first element from the array of objects of the viewModel class.
On selection of item from picker, I want to do different actions in that view - 1. send the object info to different screen on button click. 2. display information with respected to selected object from picker.
import SwiftUI
import Combine
struct SetConfiguration: View {
#ObservedObject var profileListVM : ProfilesListViewModel = ProfilesListViewModel()
#State private var selectedConfiguration = 0 ///show "Add" as initial state
var body: some View {
HStack {
Text("Configuration:")
Picker(selection: $selectedConfiguration.onChange(connectToConfiguration), label: EmptyView()) {
ForEach(profileListVM.profiles, id: \.self) {
choice in
Text(choice.name).tag(choice)
}
}
Text (“Selcted item is: \(self. selectedconfiguration.name)”)
Button(action: {
}) {
Text("Edit")
}.sheet(isPresented: $showEditConfig) {
EditConfigurationView()
// TODO pass selectedConfiguration as profile object
}
}
}
viewModel class:
class ProfilesListViewModel: ObservableObject {
#Published var profiles: [ProfileViewModel] = [ProfileViewModel]()
static var addNewProfile = ProfileViewModel(name: "Add Configuration")
init() {
fetchAllProfiles()
}
func fetchAllProfiles() {
profiles.append(ProfilesListViewModel.addNewProfile) ///Add is first object
self.profiles = CoreDataManager.shared.getConfigurations().map(ProfileViewModel.init) /// fetch all profile objects
}
}
I believe this is the context for your question. Here is the working example:
// MARK: MOCKS FOR MODELS
struct ProfileViewModel: Hashable {
let id = UUID()
let name: String
}
class CoreDataManager {
static let shared = CoreDataManager()
func getConfigurations() -> [ProfileViewModel] {
return [ProfileViewModel(name: "first"), ProfileViewModel(name: "second"), ProfileViewModel(name: "third")]
}
}
// MARK: changed class because it's not even working because of lack of code
class ProfilesListViewModel: ObservableObject {
#Published var profiles: [ProfileViewModel] = [ProfileViewModel]()
static var addNewProfile = ProfileViewModel(name: "Add Configuration")
init() {
fetchAllProfiles()
}
func fetchAllProfiles() {
print("fetched")
profiles.append(ProfilesListViewModel.addNewProfile) ///Add is first object
self.profiles = CoreDataManager.shared.getConfigurations()
}
}
// MARK: the solution
struct SetConfiguration: View {
#ObservedObject var profileListVM: ProfilesListViewModel = ProfilesListViewModel()
#State private var selectedConfiguration = 0 ///show "Add" as initial state
#State private var choosedConfiguration = 0
var body: some View {
VStack {
HStack {
Picker(selection: $selectedConfiguration.onChange(selectNewConfig), label: Text("Configuration")) {
ForEach(0 ..< self.profileListVM.profiles.count) { choice in
Text(self.profileListVM.profiles[choice].name).tag(choice)
}
}
}
Text("Selected item is: \(choosedConfiguration)")
}
}
func selectNewConfig(_ newValue: Int) {
print(newValue)
withAnimation {
choosedConfiguration = newValue
}
}
}
Tips
To avoid misunderstandings in the future:
you should add all the working code and links, or simplify it to be clear what you want to achieve. Not every swift developer know about extension Binding, so they will just say: onChange will not ever work and they will be right;
format your code;
add some examples of your models or remove/simplify them.
I believe, you don't need choosedConfiguration, you can do some experiments with this.
I have a standard SwiftUI list setup, powered by Core Data FetchRequest.
struct SomeView: View {
var container: Container
var myObjects: FetchRequest<MyObject>
init(container: Container) {
let predicate : NSPredicate = NSPredicate(format: "container = %#", container)
self.container = container
self.myObjects = FetchRequest<MyObject>(entity: MyObject.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)], predicate: predicate)
}
var body: some View {
VStack(spacing: 0.0) {
List(myObjects.wrappedValue, id: \.uniqueIdentifier) { myObject in
rowView(for: myObject, from: self.myObjects.wrappedValue)
}
}
}
}
Everything works well when items are added and deleted. RowView returns a view that presents different content based on various properties of myObject.
Problem: when I modify a particular myObject elsewhere in the app (change one of its properties), and save the associated Core Data ManagedObjectContext, the List row representing that item is not updated/refreshed in the UI.
Possibly a cause for this is that I am updating my Core Data object by setting a property, that in turn sets another property. Maybe the associated signaling doesn’t reach the right place, and I should emit more notifications here.
Code in MyObject. ObjectType is an enum, typeValue is int32 backing this, that actually gets stored in CD database.
var type: ObjectType {
get {
return ObjectType(rawValue: typeValue)!
}
set {
self.typeValue = newValue.rawValue
}
}
How do I cause a list row to update when the backing Core Data object is modified and saved elsewhere in the app?
I finally figured this out on my own. The fix was not in the list, but further down the stack, in RowView.
RowView code was such:
struct RowView: View {
var myObject: MyObject
// Other code to render body etc
}
When doing this, the RowView works as expected, but it treats myObject as immutable. Any changes to myObject don’t cause a view redraw.
The one-keyword fix is to add #ObservedObject to the declaration:
struct RowView: View {
#ObservedObject var myObject: MyObject
}
It now works as expected, and any updates to MyObject cause a redraw.