How to fill TextField with data from Core Data and update changes? - ios

I am trying to learn how to Save, Edit and Delete data using Core Data. So far, with the help of this great community, I have managed to Save and Delete, but I don't know how to Edit and Update currently saved data.
Here is a simple example of an app I am working on. It is a list with items from Core Data. I am adding new list entries on a modal (AddItemView) and deleting them on EditItemView.
I would like to edit and update data as well on the AddItemView view.
I managed to pass data to hint of TextField, but what I wanted is:
pass the current data to text of TextField and make it editable
Save/update the data after tapping
Core Data has 1 Entity: ToDoItem. It has 1 Attribute: title (String). Codegen: Class Definition, Module: Current Product Module.
I have added some additional comments in the code.
ContentView
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: ToDoItem.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \ToDoItem.title, ascending: true)
]
) var toDoItems: FetchedResults<ToDoItem>
#State private var show_modal: Bool = false
var body: some View {
NavigationView {
List{
ForEach(toDoItems, id: \.self) {todoItem in
NavigationLink(destination: EditItemView(todoItem: todoItem)) {
Text(todoItem.title ?? "")
.font(.headline)
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing:
Button(action: {
self.show_modal = true
}) {
Text("Add")
}.sheet(isPresented: self.$show_modal) {
AddItemView().environment(\.managedObjectContext, self.managedObjectContext)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
AddItemView
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State private var title = ""
var body: some View {
NavigationView {
ScrollView {
TextField("to do item...", text: $title)
.font(Font.system(size: 30))
Spacer()
}
.padding()
.navigationBarTitle(Text("Add Item"))
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
},
trailing:
Button(action: {
let toDoItem = ToDoItem(context: self.managedObjectContext)
toDoItem.title = self.title
do {
try self.managedObjectContext.save()
}catch{
print(error)
}
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
}
)
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
EditItemView
import SwiftUI
struct EditItemView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var todoItem: ToDoItem
//I am only using this newTitle variable, because I don't know how to properly bind TextField to todoItem.title
#State private var newTitle = ""
var body: some View {
ScrollView {
TextField(todoItem.title != nil ? "\(todoItem.title!)" : "", text: $newTitle)
//TextField("to do...", text: (todoItem.title != nil ? "\(todoItem.title!)" : ""))
//ERROR
//I need something like the above, but this gives me an error: Cannot convert value of type 'String' to expected argument type 'Binding<String>'
}
.padding()
.navigationBarTitle(Text("Edit item"))
.navigationBarItems(
trailing:
Button(action: {
print("Delete")
self.managedObjectContext.delete(self.todoItem)
do {
try self.managedObjectContext.save()
self.presentationMode.wrappedValue.dismiss()
}catch{
print(error)
}
}) {
Text("Delete")
.foregroundColor(.red)
}
)
}
}
struct EditItemView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Test data
let todoItem = ToDoItem.init(context: context)
todoItem.title = "Title"
return EditItemView(todoItem: todoItem).environment(\.managedObjectContext, context)
}
}

I would do it in the following way
TextField("_here_is_label_name_", text: $newTitle, onCommit: {
self.todoItem.title = self.newTitle
try? self.managedObjectContext.save()
})
.onAppear {
self.newTitle = self.todoItem.title != nil ? "\(self.todoItem.title!)" : ""
}
.onDisappear {
self.todoItem.title = self.newTitle
try? self.managedObjectContext.save()
}
Update: added .onDisappear modifier; duplicated code can be extracted in dedicated private function to have good design.

Related

How to setup NavigationLink inside SwiftUI list

I am attempting to set up a SwiftUI weather app. when the user searches for a city name in the textfield then taps the search button, a NavigationLink list item should appear in the list. Then, the user should be able to click the navigation link and re-direct to a detail view. My goal is to have the searched navigation links to populate a list. However, my search cities are not populating in the list, and I'm not sure why. In ContentView, I setup a list with a ForEach function that passes in cityNameList, which is an instance of the WeatherViewModel. My expectation is that Text(city.title) should display as a NavigationLink list item. How should I configure the ContentView or ViewModel to populate the the list with NavigationLink list items? See My code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityWeather, id: \.id) { city in
NavigationLink(destination: DetailView(detail: viewModel)) {
HStack {
Text(city.cityWeather.name)
.font(.system(size: 32))
}
}
}
}
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error)
}
}
task.resume()
}
}
Model
import Foundation
struct WeatherModel: Identifiable, Codable {
var id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DetailView
import SwiftUI
struct DetailView: View {
var detail: WeatherViewModel
var body: some View {
VStack(spacing: 20) {
Text(detail.cityWeather.name)
.font(.system(size: 32))
Text("\(detail.cityWeather.main.temp)")
.font(.system(size: 44))
Text(detail.cityWeather.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherViewModel.init())
}
}
try something like this example code, works well for me:
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
}
}
}
}
Spacer()
}.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name).font(.system(size: 32))
Text("\(detail.main.temp)").font(.system(size: 44))
Text(detail.firstWeatherInfo()).font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}

SwiftUI manipulate items from a struct from a view

I'd like the ability to edit and put into a new view the 'expenses' the user adds. I've been having problems accessing the data after a new expense has been added. I am able to delete the items and add them up but I'd like to click on the 'expenses' and see and edit the content in them Image of the view
//Content View
import SwiftUI
struct ExpenseItem: Identifiable, Codable {
let id = UUID()
let name: String
let type: String
let amount: Int
}
class Expenses: ObservableObject {
#Published var items = [ExpenseItem]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try?
encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try?
decoder.decode([ExpenseItem].self, from: items) {
self.items = decoded
return
}
}
}
// Computed property that calculates the total amount
var total: Int {
self.items.reduce(0) { result, item -> Int in
result + item.amount
}
}
}
struct ContentView: View {
#ObservedObject var expenses = Expenses()
#State private var showingAddExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in
HStack {
VStack {
Text(item.name)
.font(.headline)
Text(item.type)
}
Spacer()
Text("$\(item.amount)")
}
}
.onDelete(perform: removeItems)
// View that shows the total amount of the expenses
HStack {
Text("Total")
Spacer()
Text("\(expenses.total)")
}
}
.navigationBarTitle("iExpense")
.navigationBarItems(trailing: Button(action: {
self.showingAddExpense = true
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddExpense) {
AddView(expenses: self.expenses)
}
}
}
func removeItems(at offsets: IndexSet) {
expenses.items.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//AddExpense
import SwiftUI
struct AddView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var expenses: Expenses
#State private var name = ""
#State private var type = "Personal"
#State private var amount = ""
static let types = ["Business", "Personal"]
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
Picker("Type", selection: $type) {
ForEach(Self.types, id: \.self) {
Text($0)
}
}
TextField("Amount", text: $amount)
.keyboardType(.numberPad)
}
.navigationBarTitle("Add new expense")
.navigationBarItems(trailing: Button("Save") {
if let actualAmount = Int(self.amount) {
let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
self.expenses.items.append(item)
self.presentationMode
.wrappedValue.dismiss()
}
})
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView(expenses: Expenses())
}
}
Remove #observedObject in AddView.
A view cannot change an ObservableObject. ObservableObject is used for being notified when a value is changed.
When you pass the expenses class to AddView, you are giving it a reference. Therefore, AddView can change the expenses, and consequently update ContentView.

Core Data Object still saved on dismiss/cancel in presentationMode SwiftUI

When I'm trying to dismiss/cancel an Add Object Modal, it is creating an empty object instead of just cancelling.
I've tried deleteObject, context.rollback(), and a bunch of other random things. Would love some help and able to answer any questions.
I realize that this isn't an issue by putting the Cancel button in a NavigationBarItem but would like to be able to understand how to make an separate "cancel (or dismiss)" button.
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Game.gameName, ascending: true)]) var games: FetchedResults<Game>
#State private var showingAddGame = false
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.games, id: \.self) { games in
NavigationLink(destination: GameGoalsDetail(game: games)) {
VStack(alignment: .leading) {
Text(games.gameName ?? "Unknown Game")
Text(games.gameDescription ?? "Unknown Game Description")
}
}
}
.onDelete(perform: self.removeGames)
}
.navigationBarItems(leading:
HStack {
Button(action: {
self.showingAddGame.toggle()
}) {
Text("Add Game")
.padding(.top, 50)
.foregroundColor(Color.yellow)
}.sheet(isPresented: self.$showingAddGame) {
AddGameView().environment(\.managedObjectContext, self.moc)
}
Image("Game Goals App Logo")
.resizable()
.frame(width: 100, height: 100)
.padding(.leading, (geometry.size.width / 2.0) + -160)
.padding(.bottom, -50)
}, trailing:
EditButton()
.padding(.top, 50)
.foregroundColor(Color.yellow)
)
}
}
}
func removeGames(at offsets: IndexSet) {
for index in offsets {
let game = games[index]
moc.delete(game)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newGame = Game(context: context)
newGame.gameName = "Apex Legends"
newGame.gameDescription = "Maybe this will work"
return ContentView().environment(\.managedObjectContext, context)
}
}
AddGameView.swift
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
HStack {
Button("Add Game") {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
}
}
}
}
struct AddGameView_Previews: PreviewProvider {
static var previews: some View {
AddGameView()
}
}
I've searched all over so if there is something out there that I've missed as far as a stackoverflow post, please link it as I'd like to not only fix this but understand why.
Your Cancel button is not creating an empty object. The problem is that the whole row in your form that has Add and Cancel buttons is interactive and triggers actions of your both buttons.
I have found an answer here: https://stackoverflow.com/a/59402642/12315994
To keep your current layout you need to simply add one line to each of your buttons:
.buttonStyle(BorderlessButtonStyle())
After this, only taping on each button will trigger actions. Form's row with the buttons will not be clickable.
There are 2 other solutions. Both are to move your buttons out of Form.
Solution 1
is to move buttons to NavigationBarItems like this:
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
}
}
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
,
trailing:
Button(action: {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}) {
Text("Add Game")
}
)
}
}
}
Solution 2
Is to move buttons out of Form and move them to the bottom of the screen. Like this:
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
VStack {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
}
HStack {
Button(action: {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}) {
Text("Add Game")
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
}
}
}
}
Both options are better than your current layout from the UX point of view, because buttons are now in more standard locations. Especially version 1 is a more standard way of presenting buttons like this in iOS.

How to delete Core Data entry from Details View (Edit Item View) in SwiftUI?

I have made a very simple app in SwiftUI. ToDo List using Core Data. I can add to do items and store them with Core Data. Items are presented on a list in ContentView. Tapping each item brings us to EditItemView. I have managed to show correct data for each entry. From this view I would like to delete this particular entry I see in EditItemView. It should work similar to deleting lists in Reminders app on iOS. Delete button should delete this particular entry and take us back to ContentView. But... nothing happens. I am not getting any errors, but nothing is deleted as well.
Core Data
In Core Data I have 1 entity: ToDoItem (Module: Current Product Module, Codegen: Class Definition)
Attributes:
createdAt : Date (with default value for today)
title: String (with default value = Empty String)
Here is the code I have so far:
ContentView
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: ToDoItem.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \ToDoItem.createdAt, ascending: true),
NSSortDescriptor(keyPath: \ToDoItem.title, ascending: true)
]
) var toDoItems: FetchedResults<ToDoItem>
#State private var show_modal: Bool = false
var body: some View {
NavigationView {
List{
ForEach(toDoItems, id: \.self) {todoItem in
NavigationLink(destination: EditItemView(createdAt: todoItem.createdAt!, title: todoItem.title!)) {
ToDoItemView(title: todoItem.title!, createdAt: todoItem.createdAt!)
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing:
Button(action: {
self.show_modal = true
}) {
Text("Add")
}.sheet(isPresented: self.$show_modal) {
AddItemView().environment(\.managedObjectContext, self.managedObjectContext)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
AddItemView
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
#State private var createdAt : Date = Date()
#State private var showDatePicker = false
#State private var title = ""
var body: some View {
NavigationView {
ScrollView {
HStack {
Button(action: {
self.showDatePicker.toggle()
}) {
Text("\(createdAt, formatter: Self.dateFormat)")
}
Spacer()
}
if self.showDatePicker {
DatePicker(
selection: $createdAt,
displayedComponents: .date,
label: { Text("Date") }
)
.labelsHidden()
}
TextField("to do item", text: $title)
.font(Font.system(size: 30))
Spacer()
}
.padding()
.navigationBarTitle(Text("Add transaction"))
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
},
trailing:
Button(action: {
let toDoItem = ToDoItem(context: self.managedObjectContext)
toDoItem.createdAt = self.createdAt
toDoItem.title = self.title
do {
try self.managedObjectContext.save()
}catch{
print(error)
}
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
}
)
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
EditItemView
import SwiftUI
struct EditItemView: View {
#Environment(\.managedObjectContext) var managedObjectContext
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
var createdAt : Date
var title: String = ""
#State private var newCreatedAt : Date = Date()
#State private var showDatePicker = false
#State private var newTitle = ""
var body: some View {
ScrollView {
HStack {
Button(action: {
self.showDatePicker.toggle()
}) {
Text("\(createdAt, formatter: Self.dateFormat)")
}
Spacer()
}
if self.showDatePicker {
DatePicker(
selection: $newCreatedAt,
displayedComponents: .date,
label: { Text("Date") }
)
.labelsHidden()
}
TextField(title, text: $newTitle)
.font(Font.system(size: 30))
}
.padding()
.navigationBarTitle(Text("Edit transaction"))
.navigationBarItems(
trailing:
Button(action: {
print("Delete")
let deleteToDoItem = ToDoItem(context: self.managedObjectContext)
self.managedObjectContext.delete(deleteToDoItem)
do {
try self.managedObjectContext.save()
}catch{
print(error)
}
// let deleteToDoItem = self.toDoItems[indexSet.first!]
// self.managedObjectContext.delete(deleteToDoItem)
//
// do {
// try self.managedObjectContext.save()
// }catch{
// print(error)
// }
}) {
Text("Delete")
.foregroundColor(.red)
}
)
}
}
struct EditItemView_Previews: PreviewProvider {
static var previews: some View {
EditItemView(
createdAt: Date(),
title: "to do item"
)
}
}
ToDoItemView
import SwiftUI
struct ToDoItemView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
var title:String = ""
var createdAt:Date = Date()
var body: some View {
HStack{
VStack(alignment: .leading){
Text(title)
.font(.headline)
Text("\(createdAt, formatter: Self.dateFormat)")
.font(.caption)
}
}
}
}
struct ToDoItemView_Previews: PreviewProvider {
static var previews: some View {
ToDoItemView(title: "To do item", createdAt: Date())
}
}
P.s. I am aware that I can add .onDelete in list view. But I want to make it deliberately harder for the users to delete an item. This is why I want to move Delete button to details view.
Just add a property to EditItemView
var todoItem: ToDoItem
Also add the environment object to dismiss the view
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
Edit the delete button action
self.managedObjectContext.delete(self.todoItem)
do {
try self.managedObjectContext.save()
self.presentationMode.wrappedValue.dismiss()
}catch{
print(error)
}
Last on ContentView init the EditView init
ForEach(toDoItems, id: \.self) { todoItem in
EditItemView(todoItem: todoItem)
EDIT:
In EditItemView add:
var todoItem: ToDoItem
And change this line:
Text("\(createdAt, formatter: Self.dateFormat)")
To:
Text(todoItem.createdAt != nil ? "\(todoItem.createdAt!, formatter: Self.dateFormat)" : "")
And this line:
TextField(title, text: $newTitle)
To this:
TextField(todoItem.title != nil ? "\(todoItem.title!)" : "", text: $newTitle)
Thanks to Asperi for helping with this solution here: Error: Argument type 'Date?' does not conform to expected type 'ReferenceConvertible'
don't know if its too late, here is my solution:
1.) We name the view from which you call (or, more precisely, instantiate) the Detail View "Caller View".
Define in the Caller View a state property to save the reference to the core data entity which has to be deleted:
#State var entityToDelete: EntityType? = nil
2.) Define in the Detail View the appropriate binding property to the state property above.
#Binding var entityToDelete: EntityType?
3.) Parameterize the call (instatiation) of Detail View from the Caller View with the new property:
CallerView {
...
DetailView(..., $entityToDelete)
...
}
4.) I gas, in the Detail View you present values of some entity and you have an option to delete it (or something similar). In the Detail View set the value of the entityToDelete property to the entity which has to be deleted. It would be probably optimal to dismiss the detail view after klick to "delete button", it depends on your application semantics:
entityToDelete = presentedEntity
self.presentationMode.wrappedValue.dismiss() // depends on your app logic
5.) Delete entityToDelete in the Caller View. A good place to do that is the .onAppear - closure. If you to that in the Caller View directly, you can have a Warning "View state modification during its actualization....":
CallerView {
} .onAppear (perform: deleteItem)
...
func deleteItem ()->Void {
if entityToDelete != nil {
managedObjectContext.delete(entityToDelete!)
try? managedObjectContext.save()
entityToDelete = nil
}
}
Best,
Dragan

FetchedResult views do not update after navigating away from view & back

SwiftUI FetchedResult views fail to update when you navigate away from them and return.
I have a simple todo list app I've created as an example. This app consists of 2 entities:
A TodoList, which can contain many TodoItem(s)
A TodoItem, which belongs to one TodoList
First, here are my core data models:
For the entities, I am using Class Definition in CodeGen.
There are only 4 small views I am using in this example.
TodoListView:
struct TodoListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: TodoList.entity(),
sortDescriptors: []
) var todoLists: FetchedResults<TodoList>
#State var todoListAdd: Bool = false
var body: some View {
NavigationView {
List {
ForEach(todoLists, id: \.self) { todoList in
NavigationLink(destination: TodoItemView(todoList: todoList), label: {
Text(todoList.title ?? "")
})
}
}
.navigationBarTitle("Todo Lists")
.navigationBarItems(trailing:
Button(action: {
self.todoListAdd.toggle()
}, label: {
Text("Add")
})
.sheet(isPresented: $todoListAdd, content: {
TodoListAdd().environment(\.managedObjectContext, self.managedObjectContext)
})
)
}
}
}
This simply fetches all TodoList(s) and spits them out in a list. There is a button in the navigation bar which allows for adding new todo lists.
TodoListAdd:
struct TodoListAdd: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State var todoListTitle: String = ""
var body: some View {
NavigationView {
Form {
TextField("Title", text: $todoListTitle)
Button(action: {
self.saveTodoList()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
}
.navigationBarTitle("Add Todo List")
}
.navigationViewStyle(StackNavigationViewStyle())
}
func saveTodoList() {
let todoList = TodoList(context: managedObjectContext)
todoList.title = todoListTitle
do { try managedObjectContext.save() }
catch { print(error) }
}
}
This simply saves a new todo list and then dismisses the modal.
TodoItemView:
struct TodoItemView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var todoList: TodoList
#FetchRequest var todoItems: FetchedResults<TodoItem>
#State var todoItemAdd: Bool = false
init(todoList: TodoList) {
self.todoList = todoList
self._todoItems = FetchRequest(
entity: TodoItem.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "todoList == %#", todoList)
)
}
var body: some View {
List {
ForEach(todoItems, id: \.self) { todoItem in
Button(action: {
self.checkTodoItem(todoItem: todoItem)
}, label: {
HStack {
Image(systemName: todoItem.checked ? "checkmark.circle" : "circle")
Text(todoItem.title ?? "")
}
})
}
}
.navigationBarTitle(todoList.title ?? "")
.navigationBarItems(trailing:
Button(action: {
self.todoItemAdd.toggle()
}, label: {
Text("Add")
})
.sheet(isPresented: $todoItemAdd, content: {
TodoItemAdd(todoList: self.todoList).environment(\.managedObjectContext, self.managedObjectContext)
})
)
}
func checkTodoItem(todoItem: TodoItem) {
todoItem.checked = !todoItem.checked
do { try managedObjectContext.save() }
catch { print(error) }
}
}
This view fetches all of the TodoItem(s) that belong to the TodoList that was tapped. This is where the problem is occurring. I'm not sure if it is because of my use of init() here, but there is a bug. When you first enter this view, you can tap a todo item in order to "check" it and the changes show up in the view immediately. However, when you navigate to a different TodoItemView for a different TodoList and back, the views no longer update when tapped. The checkmark image does not show up, and you need to leave that view and then re-enter it in order for said changes to actually appear.
TodoItemAdd:
struct TodoItemAdd: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
var todoList: TodoList
#State var todoItemTitle: String = ""
var body: some View {
NavigationView {
Form {
TextField("Title", text: $todoItemTitle)
Button(action: {
self.saveTodoItem()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
}
.navigationBarTitle("Add Todo Item")
}
.navigationViewStyle(StackNavigationViewStyle())
}
func saveTodoItem() {
let todoItem = TodoItem(context: managedObjectContext)
todoItem.title = todoItemTitle
todoItem.todoList = todoList
do { try managedObjectContext.save() }
catch { print(error) }
}
}
This simply allows the user to add a new todo item.
As I mentioned above, the views stop updating automatically when you leave and re-enter the TodoItemView. Here is a recording of this behaviour:
https://i.imgur.com/q3ceNb1.mp4
What exactly am I doing wrong here? If I'm not supposed to use init() because views in navigation links are initialized before they even appear, then what is the proper implementation?
Found the solution after hours of googling various different phrases of the issue: https://stackoverflow.com/a/58381982/10688806
You must use a "lazy view".
Code:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Usage:
NavigationLink(destination: LazyView(TodoItemView(todoList: todoList)), label: {
Text(todoList.title ?? "")
})

Resources