How to make view update when CoreData entity changes - swift

Update
Based on #Tushar Sharma's suggestion, I removed #ObservedObject tag from the place variable in both PlaceDetail.swift and EditPlaceForm.swift. This made it so that the place property was an argument for the view rather than a state object. I then created separate state variables in EditPlaceForm.swift to hold the values for the text inputs. I added an onAppear to the view that sets the state values using the provided place object. This works great, and resolves the issue of the back button disappearing. It introduced a new problem though: PlaceDetail doesn't update until after it's been closed and reopened. I then did some testing to see if updating the place variable within PlaceDetail itself caused an update. To do this I just added a button that changed the name of the place and saved it to the view context when pressed. The view still did not update until it was reloaded. Therefore, my question has switched to how to make the view update when place is updated, without using #ObservedObject in EditPlaceForm.swift, which caused the back button to disappear.
Updated Pieces of Code
PlaceDetail.swift
Original:
#ObservedObject var place: Place
Updated:
var place: Place
EditPlaceForm.swift
Updated:
//
// EditPlaceForm.swift
//
import SwiftUI
import CoreLocation
struct EditPlaceForm: View {
#Environment(\.presentationMode) private var presentationMode
#Environment(\.managedObjectContext) private var viewContext
var place: Place
#State private var name = ""
#State private var street = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
#State private var country = ""
#State private var showAlert = false
#State private var alertText = ""
var body: some View {
NavigationView {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $name)
}
Section (header: Text("Address")) {
VStack {
TextField("Street", text: $street)
TextField("City", text: $city)
HStack {
TextField("State", text: $state)
Divider()
TextField("ZIP", text: $zip)
}
TextField("Country", text: $country)
}
}
Section(header: Text("Save")) {
Button (action: {
save()
}, label: {
Text("Save")
})
}
}
.navigationTitle("Edit Place")
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Error"), message: Text(alertText))
})
}
.onAppear {
loadPlaceData()
}
}
func loadPlaceData() {
name = place.name
street = place.street
city = place.city
state = place.state
zip = place.zip
country = place.country
}
func save() {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString("\(street), \(city) \(state), \(zip), \(country)", completionHandler: {
placemarks, error in
if error != nil {
print("Error forward geocoding")
print(error!.localizedDescription)
alertText = "Encountered geocoding error."
showAlert = true
return
}
if placemarks == nil {
print("No placemarks found")
alertText = "Location could not be found."
showAlert = true
return
}
let placemark = placemarks![0]
let testVars = [
placemark.thoroughfare,
placemark.locality,
placemark.administrativeArea,
placemark.country,
placemark.postalCode
]
for testVar in testVars {
if testVar == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
}
if placemark.location == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
viewContext.performAndWait {
place.street = placemark.subThoroughfare! + " " + placemark.thoroughfare!
place.city = placemark.locality!
place.state = placemark.administrativeArea!
place.country = placemark.country!
place.zip = placemark.postalCode!
place.latitude = placemark.location!.coordinate.latitude
place.longitude = placemark.location!.coordinate.latitude
do {
try viewContext.save()
presentationMode.wrappedValue.dismiss()
} catch {
print("ERROR: FAILED TO SAVE PLACE DETAILS")
alertText = "Failed to save."
showAlert = true
}
}
})
}
}
struct EditPlaceForm_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
EditPlaceForm(place: Place.getDefault(context: viewContext))
}
}
Original Post
Setup
My app has a main page which is a navigation view. On that page, there are navigation links. One of those links leads to the "Places" page which contains a list. The list items on that page are navigation links to detail pages for CoreData entities called Place. In the detail pages I have an edit button that opens an editing form as a sheet.
Intended Behavior
You open the details view from the "Places" view, using the navigation link. You then click the edit button and change some of the information. When you click save, the editing form closes and the changes are reflected in the details view. You can then click the back button to go back to the "Places" view.
Current Behavior
You open the details view from the "Places" view, using the navigation link. You then click the edit button and change some of the information. When you save, the editing form closes and the changes are reflected in the details view, but the back button has disappeared and you are stuck in the details view.
Video: https://drive.google.com/file/d/1lzj-hOCb7hgMutQQwPl0ShR7l-c9sDib/view?usp=sharing
Code
"Places" view:
//
// PlacesList.swift
//
import SwiftUI
struct PlacesList: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Place.entity(), sortDescriptors: [
NSSortDescriptor(key: "name", ascending: true)
])
private var places: FetchedResults<Place>
#State private var showAddPlaceForm = false
var body: some View {
List {
ForEach (places) { place in
NavigationLink (destination: PlaceDetail(place: place)) {
PlaceRow(place: place)
}
}
.onDelete(perform: deleteRow)
}
.listStyle(PlainListStyle())
.navigationTitle(Text("Places"))
.navigationBarItems(trailing:
HStack {
Button(action: {
showAddPlaceForm = true
}, label: {
Image(systemName: "plus.circle")
.imageScale(.large)
})
.sheet(isPresented: $showAddPlaceForm) {
NewPlaceForm()
}
}
)
}
func deleteRow(at offsets: IndexSet) {
for offset in offsets {
viewContext.performAndWait {
viewContext.delete(places[offset])
try? viewContext.save()
}
}
}
}
struct PlacesList_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
NavigationView {
PlacesList()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
}
PlaceRow.swift
//
// PlaceRow.swift
//
import SwiftUI
struct PlaceRow: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
var body: some View {
VStack (alignment: .leading) {
Text(place.name)
.font(.headline)
Group {
Text(place.street)
Text(place.city) + Text(", ") + Text(place.state)
}
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
struct PlaceRow_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
List {
PlaceRow(place: Place.getDefault(context: viewContext))
}
}
}
PlaceDetail.swift
//
// PlaceDetail.swift
//
import SwiftUI
import CoreLocation
struct PlaceDetail: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
#State private var showEditForm = false
var body: some View {
List {
VStack (alignment: .leading) {
Text("Address")
.font(.headline)
Group {
Text(place.street)
Text(place.cityStateZIPString)
Text(place.country)
}
.font(.subheadline)
.foregroundColor(.secondary)
}
VStack {
MapView(place: place)
.frame(height: 300)
}
if place.encodedAddress != nil {
Link(destination:
URL(string: "http://maps.apple.com/?daddr=\(place.encodedAddress!)")!,
label: {
HStack {
Text("Get Directions with Apple Maps")
Spacer()
Image(systemName: "chevron.forward.circle")
.imageScale(.large)
}
.foregroundColor(.blue)
})
Link(destination:
URL(string: "https://www.google.com/maps/search/?api=1&destination=\(place.encodedAddress!)")!,
label: {
HStack {
Text("Get Directions with Google Maps")
Spacer()
Image(systemName: "chevron.forward.circle")
.imageScale(.large)
}
.foregroundColor(.blue)
})
}
}
.toolbar {
Button(action: {
showEditForm = true
}, label: {
Image(systemName: "pencil.circle")
})
}
.navigationTitle(place.name)
.sheet(isPresented: $showEditForm, content: {
EditPlaceForm(place: place)
})
}
}
struct PlaceDetail_Previews: PreviewProvider {
static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
NavigationView {
PlaceDetail(place: Place.getDefault(context: viewContext))
.environment(\.managedObjectContext, viewContext)
}
}
}
EditPlaceForm.swift
//
// EditPlaceForm.swift
//
import SwiftUI
import CoreLocation
struct EditPlaceForm: View {
#Environment(\.presentationMode) private var presentationMode
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
#State private var showAlert = false
#State private var alertText = ""
var body: some View {
NavigationView {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $place.name)
}
Section (header: Text("Address")) {
VStack {
TextField("Street", text: $place.street)
TextField("City", text: $place.city)
HStack {
TextField("State", text: $place.state)
TextField("ZIP", text: $place.zip)
}
TextField("Country", text: $place.country)
}
}
Section(header: Text("Save")) {
Button (action: {
save()
}, label: {
Text("Save")
})
}
}
.navigationTitle("Edit Place")
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Error"), message: Text(alertText))
})
}
}
func save() {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(place.address, completionHandler: {
placemarks, error in
if error != nil {
print("Error forward geocoding")
print(error!.localizedDescription)
alertText = "Encountered geocoding error."
showAlert = true
return
}
if placemarks == nil {
print("No placemarks found")
alertText = "Location could not be found."
showAlert = true
return
}
let placemark = placemarks![0]
let testVars = [
placemark.thoroughfare,
placemark.locality,
placemark.administrativeArea,
placemark.country,
placemark.postalCode
]
for testVar in testVars {
if testVar == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
}
if placemark.location == nil {
print("Not enough information to complete place")
alertText = "Not enough information."
showAlert = true
return
}
viewContext.performAndWait {
place.street = placemark.subThoroughfare! + " " + placemark.thoroughfare!
place.city = placemark.locality!
place.state = placemark.administrativeArea!
place.country = placemark.country!
place.zip = placemark.postalCode!
place.latitude = placemark.location!.coordinate.latitude
place.longitude = placemark.location!.coordinate.latitude
do {
try viewContext.save()
presentationMode.wrappedValue.dismiss()
} catch {
print("ERROR: FAILED TO SAVE PLACE DETAILS")
}
}
})
}
}
struct EditPlaceForm_Previews: PreviewProvider {
private static var viewContext = PersistenceController.preview.container.viewContext
static var previews: some View {
EditPlaceForm(place: Place.getDefault(context: viewContext))
}
}

Related

FetchedResults not initialized before first use? SwiftUI

kinda new to SwiftUI and CoreData.
My problem is that when CoreData already has data saved, the FetchedResults is not initialized when try to use data in sheet.
When I click on one of the buttons to open .viewApp sheet (which sets currentApplication to the application clicked on), it gives me the default values, implying that applications is not initialized.
After clicking another button, it works as expected.
My question is how can I initilize the FetchResults? Is it not initialized automatically?
Here is my code:
import SwiftUI
enum ActiveSheet: Identifiable {
case newApp, viewApp
var id: Int {
hashValue
}
}
struct ApplicationView : View {
let title: String
let company: String
let date: String
let notes: String
var body : some View {
Text(company)
Text(title)
Text(date)
VStack(alignment: .leading){
Text("Notes")
Text(notes).padding()
}
}
}
ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var applications:FetchedResults<Application>
#State var showAddScreen = false
#State var showApplicationView = false
#State var currentApplication: Application? = nil
#State var activeSheet: ActiveSheet?
var body: some View {
NavigationView {
List {
ForEach(applications.indices, id:\.self) { i in
Button(action: {
currentApplication = applications[i]
activeSheet = .viewApp
}, label: {
HStack{
VStack{
Text(applications[i].company ?? "Error")
Text(applications[i].title ?? "Error")
}
Spacer()
Text(applications[i].date ?? "Error")
}
})
}.onDelete(perform: deleteApplications)
}
.navigationTitle("Applications")
.toolbar {
Button("Add") {
activeSheet = .newApp
}
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .newApp:
AddScreenView(moc: _moc, activeSheet: $activeSheet)
case .viewApp:
ApplicationView(title: currentApplication?.title ?? "Job Title", company: currentApplication?.company ?? "Company Name", date: currentApplication?.date ?? "Date of Submission", notes: currentApplication?.notes ?? "Notes")
}
}
}
func deleteApplications(at offsets: IndexSet) {
for offset in offsets {
let application = applications[offset]
moc.delete(application)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
UPDATE:
I changed it to an if else in the ContentView and this has fixed the problem. However, it would be nice to know when the sheet is initialized prior to the fetchedreults.

why data are passing back but SwiftUi not updating Text

I get to pass back data via closure, so new name is passed, but my UI is not updating. The new name of the user is printed when I go back to original view, but the text above the button is not getting that new value.
In my mind, updating startingUser should be enough to update the ContentView.
my ContentView:
#State private var startingUser: UserData?
var body: some View {
VStack {
Text(startingUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser?.name)")
}
}
}
my EditView:
struct DetailView: View {
#Environment(\.dismiss) var dismiss
var user: UserData
var callBackClosure: (UserData) -> Void
#State private var name: String
var body: some View {
NavigationView {
Form {
TextField("your name", text: $name)
}
.navigationTitle("edit view")
.toolbar {
Button("dismiss") {
var newData = self.user
newData.name = name
newData.id = UUID()
callBackClosure(newData)
dismiss()
}
}
}
}
init(user: UserData, callBackClosure: #escaping (UserData) -> Void ) {
self.user = user
self.callBackClosure = callBackClosure
_name = State(initialValue: user.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(user: UserData.example) { _ in}
}
}
my model
struct UserData: Identifiable, Codable, Equatable {
var id = UUID()
var name: String
static let example = UserData(name: "Luke")
static func == (lhs: UserData, rhs: UserData) -> Bool {
lhs.id == rhs.id
}
}
update
using these changes solves the matter, but my question remains valid, cannot understand the right reason why old code not working, on other projects, where sheet and text depends on the same #state var it is working.
adding
#State private var show = false
adding
.onTapGesture {
startingUser = UserData(name: "Start User")
show = true
}
changing
.sheet(isPresented: $show) {
DetailView(user: startingUser ?? UserData.example) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser!.name)")
}
}
The reason Text is not showing you the updated user name that you are passing in the closure is, your startingUser property will be set to nil when you dismiss the sheet because you have bind that property with sheet. Now after calling callBackClosure(newData) you are calling dismiss() to dismiss the sheet. To overcome this issue you can try something like this.
struct ContentView: View {
#State private var startingUser: UserData?
#State private var updatedUser: UserData?
var body: some View {
VStack {
Text(updatedUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newUser in
updatedUser = newUser
print("✅ \(updatedUser?.name ?? "no name")")
}
}
}
}
I would suggest you to read the Apple documentation of sheet(item:onDismiss:content:) and check the example from the Discussion section to get more understanding.

SwiftUI Modifying state during view update, this will cause undefined behavior - Proper use explanation

Im learning swift and this error/warning is driving me crazy because I cant see what call Im making that causing it... The Xcode warning only shows up in my #main struct
Modifying state during view update, this will cause undefined behavior.
I thought it might be in the ListView, but I realized the warning only shows after the "Submit Post" button is it.
Im looking for a fix, but more importantly and explanation as to why this is happening and the proper usage moving forward.
import SwiftUI
import Firebase
#main
struct SocialcademyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
PostsList()
}
}
}
struct PostsList: View {
#StateObject var viewModel = PostsViewModel()
#State private var searchText = ""
#State private var showNewPostForm = false
var body: some View {
NavigationView {
List(viewModel.posts) { post in
if searchText.isEmpty || post.contains(searchText) {
PostRow(post: post)
}
}
.searchable(text: $searchText)
.navigationTitle("Posts")
.toolbar {
Button {
showNewPostForm = true
} label: {
Label("New Post", systemImage: "square.and.pencil")
}
}
.sheet(isPresented: $showNewPostForm) {
NewPostView(creationAction: viewModel.makeCreationAction())
}
}
}
}
struct NewPostView: View {
typealias CreationAction = (Post) async throws -> Void
let creationAction: CreationAction
#State private var post = Post(title: "", content: "", authorName: "")
#State private var state = FormState.idle
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $post.title)
TextField("Author Name", text: $post.authorName)
}
Section {
TextField("Content", text: $post.content)
.multilineTextAlignment(.leading)
}
Button(action: createPost, label: {
if state == .working {
ProgressView() } else {
Text("Submit Post")
}
})
.font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.listRowBackground(Color.accentColor)
}
}
.navigationTitle("New Post")
.disabled(state == .working)
.alert("Cannot Create Post", isPresented: $state.isError, actions: {}) {
Text("Sorry, something went wrong")
}
.onSubmit {
createPost()
}
}
private func createPost() {
print("[NewPostForm] creating a new post")
Task {
state = .working
do {
try await creationAction(post)
dismiss()
} catch {
state = .error
print("[NewPostForm] Cannot create post: \(error)")
}
}
}
}
private extension NewPostView {
enum FormState {
case idle, working, error
var isError: Bool {
get {
self == .error
}
set {
guard !newValue else { return }
self = .idle
}
}
}
}
#MainActor
class PostsViewModel: ObservableObject {
#Published var posts = [Post.testPost]
func makeCreationAction() -> NewPostView.CreationAction {
return { [weak self] post in
try await PostsRepository.create(post)
self?.posts.insert(post, at: 0)
}
}
}

SwiftUI: ViewModel making app crash when loading a view

I want to rename an item in a ForEach list. When i try to load the EditListView for a selected list the entire app crashes.
This is a SwiftUI macOS app and the items are saved using CoreData.
The crash happens as soon as you click on "Edit List" for any of the list items.
It doesn't crash if i remove this view model var listVM: MyListViewModel from the EditListViewModel.
Here's the EditListView
struct EditListView: View {
let name: String
#Binding var isVisible: Bool
var list: MyListViewModel
#ObservedObject var editListVM: EditListViewModel
init(name: String,list: MyListViewModel, isVisible: Binding<Bool> ) {
self.list = list
editListVM = EditListViewModel(listVM: list)
_isVisible = isVisible
self.name = name
}
var body: some View {
VStack {
Text(name)
Button(action: {
editListItemVM.update()
}) {
Text("Update List Name")
}
Button(action: {
self.isVisible = false
}) {
Text("Cancel")
}
}......
EditListViewModel
class EditListViewModel: ObservableObject {
var listVM: MyListViewModel
#Published var name: String = ""
init(listVM: MyListViewModel) {
self.listVM = listVM
name = listVM.name
}
func update(){
....}
}
MyListViewModel
struct MyListViewModel: Identifiable {
private let myList: MyList
init(myList: MyList) {
self.myList = myList
}
var id: NSManagedObjectID {
myList.objectID
}
var name: String {
myList.name ?? ""
}
}
MyList Model
#objc(MyList)
public class MyList: NSManagedObject, BaseModel {
static var all: NSFetchRequest<MyList> {
let request: NSFetchRequest<MyList> = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
extension MyList {
#nonobjc public class func fetchRequest() -> NSFetchRequest<MyList> {
return NSFetchRequest<MyList>(entityName: "MyList")
}
#NSManaged public var name: String?
}
extension MyList : Identifiable {
}
Here's the Main View
struct MyListsView: View {
#StateObject var vm: MyListsViewModel
#State private var showPopover: Bool = false
init(vm: MyListsViewModel) {
_vm = StateObject(wrappedValue: vm)
}
List {
Text("My Lists")
ForEach(vm.myLists) { myList in
NavigationLink {
MyListItemsHeaderView(name: myList.name)
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: MyListViewModel(myList: MyList()), isVisible: $showPopover)
}
}
}.contextMenu {
Button {
showPopover = true
// Show the EditListView
} label: {
Label("Edit List", systemImage: "pen.circle")
}......
First get rid of your view model objects we don't use those in SwiftUI. We use the View struct and the property wrappers like #FetchRequest make the struct behave like an object. It looks like this:
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
ItemView(item: item)
I recommend looking at Xcode's app template with core data checked.
Then for editing you can use .sheet(item: like this:
struct ItemEditor: View {
#ObservedObject var item: Item // this is the scratch pad item
#Environment(\.managedObjectContext) private var context
#Environment(\.dismiss) private var dismiss // causes body to run
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 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 EditItemButton: View {
let itemObjectID: NSManagedObjectID
#Environment(\.managedObjectContext) private var viewContext
#State var itemEditorConfig: ItemEditorConfig?
var body: some View {
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 // dismiss the sheet
}
.environment(\.managedObjectContext, config.context)
}
}
func edit() {
itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: itemObjectID)
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct ItemView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditItemButton(itemObjectID: item.objectID)
}
}
}
}
the params for EditListView in the main view were incorrect.
Fixed it with the following params:
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: myList, isVisible: $showPopover)
}

Cannot find 'entity' in scope SwiftUI

I'm trying to display the detail of an item from a list on to a sheet. I'm getting this error Cannot find 'entity' in scope even though it's declared in the detail item struct.
This error doesn't appear if the sheet is inside of the list BUT this causes only the first item detail to be shown even if you select any of the other items below the first.
This is a macOS app.
#StateObject var vm = CoreDataViewModel()
List {
ForEach(vm.savedEntites) { entity in
Text(entity.name ?? "No Name")
.font(.system(size: 25))
HStack {
Button(action: {vm.deleteMemory(entity: entity)}) {
Label("Delete",systemImage: "trash")
}.foregroundColor(Color(.red))
Button(action: {showingDetailScreen.toggle()}) {
Label("Details", systemImage: "pencil")
}.foregroundColor(Color(.red))
}
}// list ends here
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(entity: entity,isVisible: self.$showingDetailScreen)
}
}
Detail Item View Struct
struct DetailItemView: View {
var entity: MemoryEntity
#Environment(\.presentationMode) var presentationMode
#Binding var isVisible: Bool
var body: some View {
Text(entity.name ?? "No Name")
HStack {
Button("Exit") {
self.isVisible = false
}
}
.frame(width: 300, height: 150)
}
}
ViewModel
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntites: [MemoryEntity] = []
#Published var selectedEntity: String = ""
init() {
container = NSPersistentContainer(name: "MemoryContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error loading CoreData. \(error)")
}
}
FetchMemories()
}
func FetchMemories() {
let request = NSFetchRequest<MemoryEntity>(entityName: "MemoryEntity")
do {
savedEntites = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
}
The scope of entity is inside ForEach brackets,
ForEach(vm.savedEntites) { entity in
} //End scope entity var
If you want to show a present outside ForEach, as a suggestion you could declare in your viewModel a selectedEntity as #Published
List {
ForEach(vm.savedEntites) { entity in
Button(action: {
vm.selectedEntity = entity
showingDetailScreen.toggle()
}) {
}
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(entity: vm.selectedEntity,isVisible: self.$showingDetailScreen)
}