Editing a specific core data object in modal view - core-data

Progressing in my Core Data learning, I'm now stuck on the following :
I display a list of core data objects in ContentView.
If the user wants to edit one of them, he long-touches the item in the list which pops up a context sheet with and edit button. So far so good.
The edit Button summons a modal view on which the editing will take place.
In the Edit view, I start by fetching the correct item through its UUID property that I had stored in UserDefaults.
I am able to display the item's name, but I run into an issue with its date (the item has an "eventDate" Date property.
The app builds, I run it on my device, but it crashes as soon as I try to edit an item. An error is thrown in my EditView code when I instantiate the value to be displayed by a picker to the event's date :
Here's what happens inside the edit button :
Button(action: {
self.modalViewCaller = 1 // To tell the sheet which view to display
UserDefaults.standard.set(item.identNumber?.uuidString, forKey: kactiveEventUUID)
self.settingsModalIsPresented = true
})
{ Text("Edit entry")
Image(systemName: "globe")
}
And the way I fetch the event in EditView :
#Environment(\.managedObjectContext) var managedObjectContext
// We get the event to be edited through its UUID :
#FetchRequest(entity: Takeoffs.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Takeoffs.eventDate, ascending: false)],
predicate: NSPredicate(format: "identNumber == %#", UUID(uuidString: UserDefaults.standard.string(forKey: kactiveEventUUID)!)! as CVarArg)) var fetchedEvent: FetchedResults<Takeoffs>
// UserDefaults.standard.object(forKey: kactiveEventUUID) as! CVarArg)
#State var selectedDate = Date()
init() { // This sets "selectedDate" to the event's value for the date picker
_selectedDate = State(initialValue: fetchedEvent.first?.eventDate ?? Date()) // The underscore is used here
}
The project is available here if anyone has the courage : https://github.com/Esowes/RecentExp
Thanks for any help...
[Edit :] Trying the .didAppear solution suggested below, but can't seem to find a view that accepts .didAppear in my body :
var body: some View {
NavigationView {
Form {
HStack {
Text("Airport : ")
TextField(String(fetchedEvent.first?.airportName ?? ""), text: $airportNameTextfield)
.disabled(airportNameTextfield.count > 2) // To limit the textField to 3 chars (IATA code)
Button(action: {
self.airportNameTextfield = ""
}) {
Image(systemName: "clear")
}
} // END of Hstack
Picker("", selection: $typeSelectorIndex) {
ForEach(0 ..< types.count) { index in
Text(self.types[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
// Text("Selected type is: \(types[typeSelectorIndex])")
VStack {
Button(action: {
self.selectedDate = Date()
}) {
Text("Today")
}
DatePicker("",selection: $selectedDate, displayedComponents: .date)
.padding(30)
.labelsHidden()
}
} // END of Form
.navigationBarItems(
leading:
Button("Done") {
self.saveEdits()
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
} // END of Button "Done"
)
.navigationBarTitle("Event edition")
} // END of Navigation View
} // END of some View

The fetchedEvent is not available yet at View.init call moment, so instead use it in .didAppear, like below
// ... remove that init at all
var body: some View {
Text("Any internal view here")
.onAppear {
// assigned fetched event data, here it is available
self.selectedDate = self.fetchedEvent.first?.eventDate ?? Date()
}
}

Related

How to select an item from a search file and place in textfield in another file

Using SwiftUI - Xcode 14.2 - iOS 16.0
I have tried different search tutorials to create a search file for my project but am unable to find out how to select the item in the search file and place that selected item in a textfield in another file. I have searched this site for other posts, i tried searching through Google, YouTube, etc...
In File 1, I have a textfield that that has a prompt 'start typing' and when selected, it directs you to the Search file to select the item you want, so it can be placed in place of the prompt.
File 1 (where the textfield is needed to paste the selected item):
VStack {
NavigationLink(destination: NameSearch()) {
TextField("Name", text: .constant(""), prompt: Text(" Start typing ")
.foregroundColor(.blue))
.multilineTextAlignment(.leading)
.padding()
}
}
Once I click on the 'start typing' prompt, it navigates to NameSearch.swift file, as seen below.
NameSearch.swift:
import SwiftUI
struct NameSearch: View {
let name = [
"Jane", "George", "Sam", "Henry", "Sally", "Liz", "John"
]
#State private var searchText = ""
var body: some View {
NavigationStack {
VStack {
// Search view
SearchBarView(searchText: $searchText)
List {
// Filtered list of names
ForEach(name.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self) {
searchText in Text(searchText)
}
}
.navigationBarTitle(Text("Search Name"))
.resignKeyboardOnDragGesture()
}
}
}
}
struct NameSearch_Previews: PreviewProvider {
static var previews: some View {
Group {
NameSearch()
.environment(\.colorScheme, .light)
NameSearch()
.environment(\.colorScheme, .dark)
}
}
}
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
modifier(ResignKeyboardOnDragGesture())
}
}
struct SearchBarView: View {
#Binding var searchText: String
#State private var showCancelButton: Bool = false
var onCommit: () ->Void = {print("onCommit")}
var body: some View {
HStack {
HStack {
Image(systemName: "magnifyingglass")
// Search text field
ZStack (alignment: .leading) {
if searchText.isEmpty { // Separate text for placeholder to give it the proper color
Text("Search")
}
TextField("", text: $searchText, onEditingChanged: { isEditing in
self.showCancelButton = true
}, onCommit: onCommit).foregroundColor(.primary)
}
// Clear button
Button(action: {
self.searchText = ""
}) {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary) // For magnifying glass and placeholder test
.background(Color(.tertiarySystemFill))
.cornerRadius(10.0)
if showCancelButton {
// Cancel button
Button("Cancel") {
UIApplication.shared.endEditing(true) // this must be placed before the other commands here
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
.padding(.horizontal)
.navigationBarHidden(showCancelButton)
}
}
Question 1: How do I hide all the names from showing in the list so that I just see the search bar and the cancel button and an empty list?
Question 2: Once I type the name I am looking for, it should pop up and I want to select name - how can I do this?
once I type the name in search bar, it appears in the empty list
I select that name
it then takes me back to File 1
replaces the 'start typing' prompt with the name i just selected in the Search file.
Question 3: I have noticed in the Search file, I am getting a warning with the following code. How can I resolve it?
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
The warning that appears is:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead
Firstly, thank you for providing a working example of your code.
As you're building for iOS 15+, you should probably be using the .searchable modifier rather than rolling your own.
The 2021 WWDC video introducing this feature is here https://developer.apple.com/wwdc21/10176
Some new features from 2022 here: https://developer.apple.com/wwdc22/10052

FetchRequest and reset in SwiftUI View?

I tried around with reset() of the context and found a strange behavior. I use the property wrapper for the fetch request in the SwiftUI View. When resetting the context why are there still the objects in the list? Does it only reset all objects which aren't saved? But what is the difference to rollback()? While testing I found a strange behavior which could have to do with the Discussion part of the Docs but how should it be handled with the property wrapper, or is it better to use rollback()?
And after clicking the reset button the list doesn't get updated, also there is one persons name setted to nil, which is strange too.
Link to Video: https://imgur.com/a/tUCPHUl
From Doc:
Summary
Returns the context to its base state. Declaration
func reset()
Discussion
All the receiver's managed objects are “forgotten.” If you use this
method, you should ensure that you also discard references to any
managed objects fetched using the receiver, since they will be invalid
afterwards.
My Code:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)]) var persons: FetchedResults<Person>
func saveContext(){
do{
try context.save()
}catch{}
}
var body: some View {
VStack{
Text("\(persons.count)")
if persons.isEmpty{
Text("No persons defined")
}else{
ScrollView{
LazyVStack{
ForEach(persons){p in
Text(p.name ?? "NO NAME").background(Color.green)
}
}
}
}
Spacer()
Button("Add Person without save"){
let p = Person(context: context)
p.id = UUID()
p.name = "Test Without save \(persons.count)"
}
Button("Add Person with save"){
let p = Person(context: context)
p.id = UUID()
p.name = "Test with save \(persons.count)"
saveContext()
}
Button("reset"){
context.reset()
}
Button("rollback"){
context.rollback()
}
Button("delete All"){
for p in persons{
context.delete(p)
}
saveContext()
}
Button("save Context"){
saveContext()
}
}
}
}

Saving Item in CoreData Initiates Navigation

In my ContentView I have a FetchRequest<Project>. I navigate to ProjectView using a NavigationLink. From ProjectView I navigate to AddItemView using another NavigationLink. In AddItemView when I add an Item to the Project and call container.viewContext.save() the AddItemView automatically dismisses back to the ContentView.
My guess is that saving to CoreData updates the FetchRequest<Project> list which in turn updates the views, but I am not sure.
How can I save a new Item to the Project in CoreData and only navigate back to ProjectView and not ContentView?
To reproduce:
Create new Single View App and check Core Data and Host in CloudKit
In the .xcdatamodel delete the default Entity and replace it with an Entity called Project which has attributes date: Date and title: String and an Entity called Item which has an attribute name: String. Give the Project a relationship called items (to type Item) and choose “to many” on the right. Give the Item a relationship called project that is the inverse of items.
Replace the code in Persistence.swift with this:
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataBug")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 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.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Copy ContentView
// ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
let projects: FetchRequest<Project>
init() {
projects = FetchRequest<Project>(entity: Project.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Project.date, ascending: false)
])
}
var body: some View {
NavigationView {
List {
ForEach(projects.wrappedValue) { project in
NavigationLink(destination: ProjectView(project: project)) {
Text(project.title ?? "Title")
}
}
}
.navigationTitle("Projects")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
Button {
withAnimation {
let project = Project(context: moc)
let now = Date()
project.date = now
project.title = now.description
try? moc.save()
}
} label: {
Label("Add Project", systemImage: "plus")
}
}
}
}
}
}
Create ProjectView.swift and copy this:
// ProjectView.swift
import SwiftUI
struct ProjectView: View {
#ObservedObject var project: Project
var items: [Item] {
project.items?.allObjects as? [Item] ?? []
}
var body: some View {
List {
ForEach(items) { item in
Text(item.name ?? "")
}
}
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
NavigationLink(destination: AddItemView(project: project)) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
Create AddItemView.swift and copy this:
import SwiftUI
// AddItemView.swift
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var moc
let project: Project
#State private var selectedName: String = ""
var body: some View {
TextField("Type name here", text: $selectedName)
.navigationTitle("Add Item")
.navigationBarItems(trailing: Button("Add") {
let ingestion = Item(context: moc)
ingestion.project = project
ingestion.name = selectedName
try? moc.save()
presentationMode.wrappedValue.dismiss()
})
}
}
Run the app. Click the plus on the top right. Click the project that just slid in. In the ProjectView click the plus on the top right again. Type a name in the TextField and click add on the top right. When the AddItemView is dismissed it probably went back to ContentView. If not add another item to the project.

NavigationLinks are being grouped

I have a restaurant menu app that is grouping menu items inside of sections of the menu with NavigationLinks on each menu item which are intended to display a more detailed description of the item. All the menu items in a section are being grouped together as if they were just a single link and triggering the error "Fatal error: UIKitNavigationBridge: multiple active destinations: file SwiftUI". In other words, it is trying to display the detail for all the items within that section when you click on any individual item.
I'm doing this with a section view that displays the various sections and in turn, each section displays the items within that section.
It appears to be a bug in SwiftUI, but since I'm relatively new to SwiftUI, I thought I'd seek more seasoned advice.
import SwiftUI
struct MenuSectionView: View
{
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var env: GlobalEnvironment
var group: Group
var items: [MenuItem]
init(group: Group)
{
self.group = group
items = getMenuItems(businessid: group.businessid!, groupid: group.groupid)
}
var body: some View
{
VStack
{
ForEach (items, id: \.itemid)
{
itemx in
if group.groupid == itemx.groupid
{
MenuItemView(item: itemx)
}
}
}
}
}
import SwiftUI
import CoreData
struct MenuItemView: View
{
#Environment(\.managedObjectContext) var context
#EnvironmentObject var env: GlobalEnvironment
var item: MenuItem
init(item: MenuItem)
{
self.item = item
}
var body: some View
{
return VStack
{
NavigationLink(destination: DetailView(item: item))
{
VStack
{
HStack
{
if let image = item.image
{
Image(uiImage: UIImage(data: image)!).resizable().frame(width: 40, height: 40).cornerRadius(5)
} else
{
Image(item.name!).resizable().frame(width: 40, height: 40).cornerRadius(5)
}
Text(item.name!)
}
Text(item.desc!)
}
}
}
}
}
}
Apparently the VStack in the above example was the cause of the error/bug. I eliminated it and now the links work correctly. It is still a bug since in a more complex iteration, the VStack is needed. I found the same thing happens with Buttons within stacks.

SwiftUI TextField CoreData - Changing an attribute's data

I'm trying to use TextField to change the data of an attribute of CoreData, and everything I've come up with hasn't been successful. There is a similar question (listed below), and I'm going to post the code from the correct answer to that to explain it.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
// Here I would like to do this
// TextField("PlaceHolder", $itemStore.items[idx].name)
// That doesn't work... also tried
// TextField("PlaceHolder", $name) - where name is a #State String
// How can you then automaticlly assign the new value of #State name
// To $itemStore.items[idx].name?
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}
Original Question:
SwiftUI #Binding doesn't refresh View
I now have it working.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
// Added new #State variable
#State var name = ""
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
TextField("Placeholder", text: $name) {
// When the enter key is tapped, this runs.
self.itemStore.items[self.idx].name = self.name
}
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}

Resources