Use Bindings with Core Data, SwiftUI - core-data

How can I use #Binding with Core Data in SwiftUI?
If I use $ in the TextField, I get the error Cannot find '$row' in scope, and if I don't use $, I get the error Cannot convert value of type 'String?' to expected argument type 'Binding<String>'.
CoreData Model:
extension User {
#nonobjc public class func fetchRequest() -> NSFetchRequest<User> {
return NSFetchRequest<User>(entityName: "User")
}
#NSManaged public var name: String?
}
extension TimetableRowData : Identifiable {
}
SwiftUI View:
import SwiftUI
struct TimetableView: View {
// Core Data
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: [
SortDescriptor(\.name)
]) var users: FetchedResults<User>
//
var body: some View {
ScrollView(.vertical, showsIndicators: true) {
ForEach(users, id: \.self) { user in
TextField("Username", text: $user.name) // <-- here I get an error
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add User") {
let newUser = User(context: moc)
newUser.name = "Default Name"
try? moc.save()
}
}
}
}
}

Related

Core Data results with #EnvironmentKey

i try to adopt answer from How to re-use the same FetchRequest, rather than the same request in several views?
i build simple app with Core data
// Count+CoreDataProperties.swift
extension Count {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Count> {
return NSFetchRequest<Count>(entityName: "Count")
}
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var number: Int16
}
// CoreDataManager.swift // all structs to fetch core data and set Environment
public struct CountEnvironmentKey: EnvironmentKey {
public static var defaultValue: [Count] = []
}
public extension EnvironmentValues {
var counts: [Count] {
get { self[CountEnvironmentKey.self] }
set { self[CountEnvironmentKey.self] = newValue }
}
}
public extension View {
func setCount(_ counts: [Count]) -> some View {
environment(\.counts, counts)
}
}
struct CountLoaderViewModifier: ViewModifier {
#FetchRequest( sortDescriptors: [])
var counts: FetchedResults<Count>
func body(content: Content) -> some View {
content
.setCount(Array(counts))
}
}
extension View {
func loadCounts() -> some View {
modifier(CountLoaderViewModifier())
}
}
// ContentView.swift
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.counts) var counts
var body: some View {
NavigationView {
List {
ForEach(counts) { item in
NavigationLink {
Text("\(item.number)")
} label: {
Text("Item at \(item.name!)")
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Count(context: viewContext)
newItem.name = "Name"
newItem.number = 1
newItem.id = UUID()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { counts[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
// MyEnvApp.swift
#main
struct MyEnvApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.loadCounts()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
in #main i getting warning
Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x60000273dfb0>
the code from sample
#ObservedObject var persistenceManager = PersistenceManager(usage: .main)
generate error also, i understand that i need to connect my core data store to environment
thanks for help in advance
.loadCounts() is applied as a modifier outside (after) the modifier that sets the managed object context in the environment. This means the environment value is not present in your modifier's body. Try moving .loadCounts() above .environment(\.managedObjectContext, ...).

swiftui how to fetch core data values from Detail to Edit views

Learning swiftui by building an app with core data; stuck in an issue of data flow from Detail to Edit of AddEdit; the flows from AddEdit to List and from List to Detail are ok. Searched but didn't find useful info online or I don't understand. Here is a simplified project for the question. It complies ok on 13.2 beta and works on simulator, with the issue of blank Edit view from Detail.
views:
struct FileList: View {
#FetchRequest(sortDescriptors: [ NSSortDescriptor(keyPath: \Item.fileName, ascending: false) ], animation: .default) var items: FetchedResults<Item>
#State private var showAdd = false
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: FileDetail(item: item)) {
Text(item.fileName ?? "").font(.headline)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing: Button(action: {
showAdd = true
}, label: { Image(systemName: "plus.circle")
})
.sheet(isPresented: $showAdd) {
FileAddEdit(items: VM())
}
)
}
}
}
struct FileList_Previews: PreviewProvider {
static let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
FileList()
}
}
struct FileDetail: View {
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
#State var showingEdit = false
#ObservedObject var item: Item
var body: some View {
VStack {
Form {
Text(self.item.fileName ?? "File Name")
Button(action: {
showingEdit.toggle()
}, label: {
title: do { Text("Edit")
}
})
.sheet(isPresented: $showingEdit) {
FileAddEdit(items: VM())
}
}
}.navigationTitle("Detail")
}
}
struct FileDetails_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let item = Item(context: moc)
return NavigationView {
FileDetail(item: item)
}
}
}
struct FileAddEdit: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var items = VM()
var body: some View {
NavigationView {
VStack {
Form {
TextField("File Name", text: $items.fileName)
Button(action: {
items.writeData(context: moc)
}, label: {
title: do { Text(items.updateFile == nil ? "Add" : "Edit")
}})
}
}
.navigationTitle("\(items.updateFile == nil ? "Add" : "Edit")")
}
}
}
struct FileAddEdit_Previews: PreviewProvider {
static var previews: some View {
FileAddEdit(items: VM())
}
}
VM:
class VM: ObservableObject {
#Published var fileName = ""
#Published var id = UUID()
#Published var isNewData = false
#Published var updateFile : Item!
init() {
}
var temporaryStorage: [String] = []
func writeData(context : NSManagedObjectContext) {
if updateFile != nil {
updateCurrentFile()
} else {
createNewFile(context: context)
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}
func DetailItem(fileItem: Item){
fileName = fileItem.fileName ?? ""
id = fileItem.id ?? UUID()
updateFile = fileItem
}
func EditItem(fileItem: Item){
fileName = fileItem.fileName ?? ""
id = fileItem.id ?? UUID()
isNewData.toggle()
updateFile = fileItem
}
private func createNewFile(context : NSManagedObjectContext) {
let newFile = Item(context: context)
newFile.fileName = fileName
newFile.id = id
}
private func updateCurrentFile() {
updateFile.fileName = fileName
updateFile.id = id
}
private func resetData() {
fileName = ""
id = UUID()
isNewData.toggle()
updateFile = nil
}
}
Much appreciated for your time and advices!
Here is a simplified version of your code Just paste this code into your project and call YourAppParent() in a body somewhere in your app as high up as possible since it creates the container.
import SwiftUI
import CoreData
//Class to hold all the Persistence methods
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
///Creates an NSManagedObject of **ANY** type
func create<T: NSManagedObject>() -> T{
T(context: context)
//For adding Defaults see the `extension` all the way at the bottom of this post
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
//Figure out the type if you want type specific changes
if obj is FileEnt{
//Make type specific changes
let name = (obj as! FileEnt).fileName
print("I'm updating FileEnt \(name ?? "no name")")
}else{
print("I'm Something else")
}
save()
}
///Creates a sample FileEnt
//Look at the preview code for the `FileEdit` `View` to see when to use.
func addSample() -> FileEnt{
let sample: FileEnt = create()
sample.fileName = "Sample"
sample.fileDate = Date.distantFuture
return sample
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
func save(){
do{
try context.save()
}catch{
print(error)
}
}
}
//Entry Point
struct YourAppParent: View{
#StateObject var coreDataPersistence: CoreDataPersistence = .init()
var body: some View{
FileListView()
//#FetchRequest needs it
.environment(\.managedObjectContext, coreDataPersistence.context)
.environmentObject(coreDataPersistence)
}
}
struct FileListView: View {
#EnvironmentObject var persistence: CoreDataPersistence
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FileEnt.fileDate, ascending: true)],
animation: .default)
private var allFiles: FetchedResults<FileEnt>
var body: some View {
NavigationView{
List{
//Has to be lazy or it will create a bunch of objects because the view gets preloaded
LazyVStack{
NavigationLink(destination: FileAdd(), label: {
Text("Add file")
Spacer()
Image(systemName: "plus")
})
}
ForEach(allFiles) { aFile in
NavigationLink(destination: FileDetailView(aFile: aFile)) {
Text(aFile.fileDate?.description ?? "no date")
}.swipeActions(edge: .trailing, allowsFullSwipe: true, content: {
Button("delete", role: .destructive, action: {
persistence.delete(aFile)
})
})
}
}
}
}
}
struct FileListView_Previews: PreviewProvider {
static var previews: some View {
YourAppParent()
// let pers = CoreDataPersistence()
// FileListView()
// #FetchRequest needs it
// .environment(\.managedObjectContext, pers.context)
// .environmentObject(pers)
}
}
struct FileDetailView: View {
#EnvironmentObject var persistence: CoreDataPersistence
#ObservedObject var aFile: FileEnt
#State var showingFileEdit: Bool = false
var body: some View{
Form {
Text(aFile.fileName ?? "")
}
Button(action: {
showingFileEdit.toggle()
}, label: {
Text("Edit")
})
.sheet(isPresented: $showingFileEdit, onDismiss: {
//Discard any changes that were not saved
persistence.resetStore()
}) {
FileEdit(aFile: aFile)
//sheet needs reinject
.environmentObject(persistence)
}
}
}
///A Bridge to FileEdit that creates the object to be edited
struct FileAdd:View{
#EnvironmentObject var persistence: CoreDataPersistence
//This will not show changes to the variables in this View
#State var newFile: FileEnt? = nil
var body: some View{
Group{
if let aFile = newFile{
FileEdit(aFile: aFile)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
newFile = persistence.create()
})
}
}
.navigationBarHidden(true)
}
}
struct FileEdit: View {
#EnvironmentObject var persistence: CoreDataPersistence
#Environment(\.dismiss) var dismiss
//This will observe changes to variables
#ObservedObject var aFile: FileEnt
var viewHasIssues: Bool{
aFile.fileDate == nil || aFile.fileName == nil
}
var body: some View{
Form {
TextField("required", text: $aFile.fileName.bound)
//DatePicker can give the impression that a date != nil
if aFile.fileDate != nil{
DatePicker("filing date", selection: $aFile.fileDate.bound)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
//Set Default
aFile.fileDate = Date()
})
}
}
Button("save", role: .none, action: {
persistence.update(aFile)
dismiss()
}).disabled(viewHasIssues)
Button("cancel", role: .destructive, action: {
persistence.resetStore()
dismiss()
})
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}
extension Optional where Wrapped == Date {
var _bound: Date? {
get {
return self
}
set {
self = newValue
}
}
public var bound: Date {
get {
return _bound ?? Date.distantPast
}
set {
_bound = newValue
}
}
}
For adding a preview that requires an object you can use this code with the new CoreDataPersistence
///How to create a preview that requires a CoreData object.
struct FileEdit_Previews: PreviewProvider {
static let pers = CoreDataPersistence()
static var previews: some View {
VStack{
FileEdit(aFile: pers.addSample()).environmentObject(pers)
}
}
}
And since the create() is now generic you can use the Entity's extension to add defaults to the variables.
extension FileEnt{
public override func awakeFromInsert() {
//Set defaults here
self.fileName = ""
self.fileDate = Date()
}
}
Below is a working example I made that extends the default Core Data SwiftUI app template to add editing of the Item's timestamp in a sheet. The sheet loads the item in the child context so edits can be made and if cancelled the edits are discarded but if saved then the changes are pushed in to the view context and it is saved. If you are unfamilar with child contexts for editing I recommend Apple's old CoreDataBooks sample project.
The main thing you need to know is when we are using a sheet to edit something we use the version that takes an item rather than a boolean. That allows you to configure the editing View correctly.
import SwiftUI
import CoreData
struct ItemEditorConfig: Identifiable {
let id = UUID()
let context: NSManagedObjectContext
let item: Item
init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
// create the scratch pad context
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = viewContext
// load the item into the scratch pad
item = context.object(with: objectID) as! Item
}
}
struct ItemEditor: View {
#ObservedObject var item: Item // this is the scratch pad item
#Environment(\.managedObjectContext) private var context
#Environment(\.dismiss) private var dismiss
let onSave: () -> Void
#State var errorMessage: String?
var body: some View {
NavigationView {
Form {
Text(item.timestamp!, formatter: itemFormatter)
if let errorMessage = errorMessage {
Text(errorMessage)
}
Button("Update Time") {
item.timestamp = Date()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// first save the scratch pad context then call the handler which will save the view context.
do {
try context.save()
errorMessage = nil
onSave()
} catch {
let nsError = error as NSError
errorMessage = "Unresolved error \(nsError), \(nsError.userInfo)"
}
}
}
}
}
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var item: Item
#State var itemEditorConfig: ItemEditorConfig?
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: edit) {
Text("Edit")
}
}
}
.sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
ItemEditor(item: config.item) {
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
itemEditorConfig = nil
}
.environment(\.managedObjectContext, si.context)
}
}
func edit() {
itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: item.objectID)
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

Why is my list of parent items growing without adding items

I have a simple core-data/swiftui test app with 2 entities. 1: Item entity with a simple text and timestamp attributes. And a one-to-one relationship to an Editor entity. 2: editor entity with a name attribute and a one-to-many relationship to Item.
In the item view I want to pick an editor from a list of stored Editors. However when I update the view (i.e. adding text to the text attribute), the list of editors to pick from grows with new and empty editor objects.
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(
destination: ItemEditView(item: item),
label: {
Text(item.text ?? "No text")
})
}
.onDelete(perform: deleteItems)
}.navigationBarItems(leading: EditButton(), trailing:
HStack {
NavigationLink(
destination: PersonEditView(editor: Editor(context: viewContext)),
label: {
Label("Add Editor", systemImage: "person")
})
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
)
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
viewContext.rollback()
}
}
}
private func addPerson() {
withAnimation {
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
struct ItemEditView: View {
#ObservedObject var item: Item
#FetchRequest private var editors: FetchedResults<Editor>
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
TextField("text", text: $item.text.bound)
Text(item.text ?? "Hello")
Text(item.editor?.name ?? "No name")
List{
ForEach(editors) { editor in
Button(editor.name ?? "no name") {
editor.addToItems(item)
}
}
}.navigationBarItems(trailing: Button(action: save, label: {Text("save")}))
}
init(item: Item) {
let personRequest: NSFetchRequest<Editor> = Editor.fetchRequest()
personRequest.fetchLimit = 10
personRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Editor.name, ascending: true)]
_editors = FetchRequest(fetchRequest: personRequest)
self.item = item
}
private func save() {
do {
try viewContext.save()
} catch {
viewContext.rollback()
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
struct PersonEditView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var editor: Editor
var body: some View {
NavigationView {
TextField("name", text: $editor.name.bound)
}.navigationBarItems(trailing: Button(action: save, label: {
Text("Save")
}))
}
private func save() {
do {
try viewContext.save()
} catch {
viewContext.rollback()
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}
Project on github
Core data model: Editor Core data model: Item
For the looks you can always filter out using an if statement. Although it doesn't seem to me that this is an ideal option. The bigger thing is that when I try to save the context, all those extra empty editors get saved as well.
Is there a way to not have empty editors added to my list and managedObjectContext?
You are creating an Editor every time the ContentView body is reloaded.
Comment out this code and it won't happen anymore. How to fix it? Take persistence code out of any View struct it doesn't belong there.
Put it into a class ViewModel or a Manager and just call methods/ pass variables with the View actions.
// NavigationLink(
// The issue is this line.
// destination: PersonEditView(editor: Editor(context: viewContext)),
// label: {
// Label("Add Editor", systemImage: "person")
// })
SwiftUI pre-loads a lot of code/Views it is a known behavior that developers have to adjust too.

SwiftUI Form only updates properties on "Enter"

I have a form where a user can enter several values to track charges.
But the values are only stored in the properties when the user hits Enter if the values is added and the user selects the next field then the value for previous entered property remains at the initial value.
It there anything that needs to be set to accept normal change of fields?
Thanks.
import SwiftUI
import CoreData
struct CreateEditChargeView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.presentationMode) var presentation
private var isNew = true
#ObservedObject var charge:Charge = Charge(entity: Charge.entity(), insertInto: nil)
var selectedVehicle: Vehicle
init(selectedVehicle: Vehicle, charge: Charge? = nil) {
self.selectedVehicle = selectedVehicle
if let charge = charge {
isNew = false
self.charge = charge
} else {
self.charge.id = UUID()
}
}
#ViewBuilder
var body: some View {
Form {
Section(header: Text("Basic")) {
TextField("KM", value: $charge.odometer,formatter: Formatter.distanceFormatter)
TextField("Price per Unit", value: $charge.pricePerUnit, formatter: Formatter.currencyFormatter)
}
}.navigationBarItems(leading: VStack{
if presentation.wrappedValue.isPresented {
Button(action: {
presentation.wrappedValue.dismiss()
}) {
Text("Cancel")
}
}
}, trailing: VStack {
if presentation.wrappedValue.isPresented {
Button(action: {
if isNew {
viewContext.insert(charge)
charge.vehicle = selectedVehicle
}
do {
try viewContext.save()
} catch {
ErrorHandler.handleError(error: error)
}
presentation.wrappedValue.dismiss()
}) {
Text("Done")
}
}
})
}
}

NavigationLink is activated automatically

I have the following code inside a CoreData SwiftUI project:
import SwiftUI
struct ContentView: View {
#FetchRequest(entity: TestObject.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \TestObject.name, ascending: true)]) var objects: FetchedResults<TestObject>
#Environment(\.managedObjectContext) var managedObjectContext
#State private var showFavorites = true
var body: some View {
NavigationView {
List {
if(showFavorites) {
Section(header: Text("Favorites").font(.headline).padding(.leading, -8)) {
ForEach(self.objects.filter({ (testObj) -> Bool in
return testObj.isFavorite
}), id: \.id) { obj in
NavigationLink(destination: DetailsView(testObject: obj)) {
Text(obj.name ?? "")
}
}
}
}
Section(header: Text("All").font(.headline).padding(.leading, -8)) {
ForEach(self.objects, id: \.id) { obj in
NavigationLink(destination: DetailsView(testObject: obj)) {
Text(obj.name ?? "")
}
}
}
}.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
.navigationBarTitle("Test", displayMode: .large)
.navigationBarItems(
trailing: Button(action: {
let newObj = TestObject(context: self.managedObjectContext)
newObj.name = "newTest"
newObj.id = UUID()
newObj.isFavorite = false
do {
try self.managedObjectContext.save()
} catch let error {
print(error)
print("error saving object")
}
}) {
Image(systemName: "plus")
}
)
}
}
}
struct DetailsView : View{
#ObservedObject var testObject : TestObject
var body : some View {
VStack {
Text(testObject.name ?? "")
}.navigationBarTitle("Detail")
.navigationBarItems(
trailing: HStack {
Button(action: {
self.testObject.isFavorite.toggle()
}) {
Image(systemName: self.testObject.isFavorite ? "heart.fill" : "heart")
}
}
)
}
}
And this entity in the xcdatamodeld file:
If i start the app for the first time, i can add new TestObjects via a tap on the plus image in the NavigationBar, which then get added to the list. If i then tap on one of the objects, i navigate to the DetailsView. On this View, tapping the NavigationBarButton with the heart should mark the object as favorite so that if the user navigates back, this is also displayed in the first part if the list, the favorites. It works so far, but i have this weird effect:
Video on Imgur
It looks like the NavigationLink for the extra element in the favorites section is automatically triggered. Any idea how i can fix this?
Thanks in advance
Try the following (cannot test your code, so just an idea)
struct DetailsView : View {
#ObservedObject var testObject : TestObject
#State private var isFavorite: Bool
init(testObject: TestObject) {
self.testObject = testObject
_isFavorite = State(initialValue: testObject.isFavorite)
}
var body : some View {
VStack {
Text(testObject.name ?? "")
}.navigationBarTitle("Detail")
.navigationBarItems(
trailing: HStack {
Button(action: {
self.isFavorite.toggle()
}) {
Image(systemName: self.isFavorite ? "heart.fill" : "heart")
}
}
)
.onDisappear {
DispatchQueue.main.async { // << this might be redundant, try w/ and w/o
self.testObject.isFavorite = self.isFavorite
}
}
}
}

Resources