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
}
}
}
}
Related
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)
}
}
In the view whose code you see below I have a button that calls the searchView function. It displays an alert and in it we enter the number by which we are looking for an object and this object is returned by the getSong function. Up to this point everything is working fine. The problem I have encountered is that it now calls view and passes this found object, but the view does not refresh. I must have made some mistake that I can't locate or that I have forgotten. I would appreciate some help with this.
DetailView:
import CoreData
import SwiftUI
struct DetailView: View {
#State var song : Song
#State var isSelected: Bool
var body: some View {
VStack{
Text(song.content ?? "No content")
.padding()
Spacer()
}
.navigationBarTitle("\(song.number). \(song.title ?? "No title")", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
HStack{
Button(action: {
song.favorite.toggle()
isSelected=song.favorite
}) {
Image(systemName: "heart.fill")
.foregroundColor(isSelected ? .red : .blue)
}
Button(action: {
searchView()
}) {
Image(systemName: "magnifyingglass")
}
}
}
}
}
func searchView(){
let search = UIAlertController(title: "Go to the song", message: "Enter the number of the song you want to jump to.", preferredStyle: .alert)
search.addTextField {(number) in
number.placeholder = "Song number"
}
let go = UIAlertAction(title: "Go", style: .default) { (_) in
let songFound = PersistenceController.shared.getSong(number: (search.textFields![0].text)!)
DetailView(song: songFound!, isSelected: songFound!.favorite)
}
let cancel = UIAlertAction(title: "Cancel", style: .destructive) { (_) in
print("Cancel")
}
search.addAction(cancel)
search.addAction(go)
UIApplication.shared.windows.first?.rootViewController?.present(search, animated: true, completion: {
})
}
}
struct SongDetail_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let song = Song(context: moc)
song.number=0
song.title="Title"
song.content="Content"
song.author="Author"
song.favorite=false
return NavigationView {
DetailView(song: song, isSelected: song.favorite)
}
}
}
PersistenceController:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error \(error.localizedDescription)")
}
}
}
func save(completion: #escaping (Error?) -> () = {_ in}) {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
completion(nil)
} catch {
completion(error)
}
}
}
func delete(_ object: NSManagedObject, completion: #escaping (Error?) -> () = {_ in}) {
let context = container.viewContext
context.delete(object)
save(completion: completion)
}
func getSong(number: String) -> Song? {
guard let songNumber = Int64(number) else { return nil }
let context = container.viewContext
let request = NSFetchRequest<Song>(entityName: "Song")
request.predicate = NSPredicate(format: "number == %ld", songNumber)
do {
return try context.fetch(request).first
} catch {
print(error)
return nil
}
}
}
I am trying to transfer CDListModel to my Today View but I can not do it properly. So that I can see how many reminders I have totally today in list.reminders.count. Right now I am getting 0
struct Today: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var list: CDListModel
#State var isTodayTapped = false
var body: some View {
Button(action: {
self.isTodayTapped.toggle()
}) {
ZStack{
RoundedRectangle(cornerRadius: 15)
.foregroundColor(.white)
HStack(alignment: .top, spacing: 100) {
Text(String(list.reminders!.count))
}
}
}
.fullScreenCover(isPresented: $isTodayTapped) {
TodayView()
}
}
}
MainPageView
struct MainPageView: View {
#Environment(\.managedObjectContext) private var viewContext
var list: CDListModel
var body: some View {
ZStack {
NavigationView {
ZStack {
VStack{
HStack(spacing: 20){
Today(list: list) //What should I send here?
.environment(\.managedObjectContext, viewContext)
}
}
}
}
}
}
}
ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedList = ListModel(color: "", text: "", reminders: [])
var body: some View {
MainPageView(selectedList: $selectedList, list: CDListModel())
.environment(\.managedObjectContext, viewContext)
}
}
You do not need to transfer data from another view to Today view. Because the data you need is already inside the core data so you can reach to data using
#FetchRequest( entity: CDReminder.entity(),
sortDescriptors:
[NSSortDescriptor(keyPath: \CDReminder.date, ascending: true)]
)var reminder: FetchedResults<CDReminder>
inside the Today view without modifying any of the other views.
Change Today as below
struct Today: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest( entity: CDReminder.entity(),
sortDescriptors:
[NSSortDescriptor(keyPath: \CDReminder.date, ascending: true)]
)var reminder: FetchedResults<CDReminder>
#State var isTodayTapped = false
var body: some View {
Button(action: {
self.isTodayTapped.toggle()
}) {
ZStack{
RoundedRectangle(cornerRadius: 15)
.foregroundColor(.white)
HStack(alignment: .top, spacing: 100) {
Text(String(list.reminders!.count))
}
}
}
.fullScreenCover(isPresented: $isTodayTapped) {
TodayView()
}
}
}
MainPage as below
struct MainPageView: View {
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
ZStack {
NavigationView {
ZStack {
VStack{
HStack(spacing: 20){
Today()
.environment(\.managedObjectContext, viewContext)
}
}
}
}
}
}
}
ContentView as below
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedList = ListModel(color: "", text: "", reminders: [])
var body: some View {
MainPageView(selectedList: $selectedList)
.environment(\.managedObjectContext, viewContext)
}
}
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.
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")
}
}
})
}
}