Cannot find 'entity' in scope SwiftUI - swift

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)
}

Related

How do I instantly load core data in a view after closing my popup?

The point of this app is to use core data to permanently add types of fruit to a list. I have two views: ContentView and SecondScreen. SecondScreen is a pop-up sheet. When I input a fruit and press 'save' in SecondScreen, I want to immediately update the list in ContentView to reflect the type of fruit that has just been added to core data as well as the other fruits which have previously been added to core data. My problem is that when I hit the 'save' button in SecondScreen, the new fruit is not immediately added to the list in ContentView. Instead, I have to restart the app to see the new fruit in the list.
Here is the class for my core data:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my ContentView struct:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { entity in
Text(entity.name ?? "NO NAME")
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh)
})
}
}
}
Here is my SecondScreen struct:
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#StateObject var vm = CoreDataViewModel()
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
To try and solve this issue, I've created a #State boolean variable called 'refresh' in ContentView and bound it with the 'refresh' variable in SecondScreen. This variable is toggled when the user hits the 'save' button on SecondScreen, and I was thinking that maybe this would change the #State variable in ContentView and trigger ContentView to reload, but it doesn't work.
In your second screen , change
#StateObject var vm = CoreDataViewModel()
to
#ObservedObject var vm: CoreDataViewModel
then provide for the instances that compiler will ask for
hope it helps
You need to use #FetchRequest instead of #StateObject and NSFetchRequest. #FetchRequest will call body to update the Views when the fetch result changes.

How do I get a textfield to display attributes of a core data entity?

I have a list of fruits. the struct FruitRowView provides the layout for the view of each row. In this FruitRowView, there's a TextField which I want to display the name of each fruit. I am having trouble doing this. The reason why I want to use a TextField to display the name of each fruit rather than a Text is so that users can easily edit the name of the fruit right from that TextField. In this case, fruits are the Core Data entity and the fruit name is an attribute of this entity.
Here is my core data class:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my contentView:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(vm: vm, fruit: fruit)
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh, vm: vm)
})
}
}
}
Here is my popup screen (used to create a new fruit)
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: CoreDataViewModel
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
Here is my FruitRowView:
struct FruitRowView: View {
//instance of core data model
#ObservedObject var vm: CoreDataViewModel
var fruit: FruitEntity
#State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
So the error that I'm getting is: 'Cannot use instance member 'fruit' within property initializer; property initializers run before 'self' is available'. This error occurs in the FruitRowView when I try to assign fruitName to fruit.name. I assume that there's an easy workaround for this but I haven't been able to figure it out.
Since the fruitEntities in the view model is a published property, you don't need a state variable in the row. You need the binding for the row view, and you should pass it in the content view.
You don't need to pass the view model to the row view as well.
struct FruitRowView: View {
// No need to pass view model to child, only pass the data
// #ObservedObject var vm: CoreDataViewModel
#Binding var fruitName: String
// You don't need this.
// #State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
struct ContentView: View {
...
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(fruitName: $fruit.name)
}
}
...
}

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)
}

Detail View of a CoreData list item in SwiftUI

I'm trying to pass the details of a CoreData list item on to a separate detail view...
My Item Model
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntites: [MemoryEntity] = []
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)")
}
}
}
My content View
#StateObject var vm = CoreDataViewModel()
List {
ForEach(vm.savedEntites) { entity in
Text(entity.name ?? "No Name")
.font(.system(size: 25))
HStack {
Button(action: {showingDetailScreen.toggle()}) {
Label("View Details", systemImage: "pencil")
}.foregroundColor(Color(.red))
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(isVisible: self.$showingDetailScreen, entity: entity)
}
}
}
Detail View, this is where i want details of the item to be shown. For now i just want the "name" to be displayed on this view.
struct DetailItemView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isVisible: Bool
var entity: MemoryEntity
var body: some View {
Text("\(entity.name)")
HStack {
Button("Exit") {
self.isVisible = false
}
}
.frame(width: 300, height: 150)
}
}
...But i'm getting this error in DetailItemView right next to Text("\(entity.name)"):
No exact matches in call to instance method 'appendInterpolation'
First of all creating a string from a string with String Interpolation is redundant.
Secondly your code already includes the answer (right after ForEach). As name is an optional you have to unwrap it.
Text(entity.name ?? "No Name")
in DetailItemView do what you did in ContentView use:
Text(entity.name ?? "No Name")

How to make view update when CoreData entity changes

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))
}
}