Crash while dismissing a view that hasn't been edited - SwiftUI - ios

I'm building an iOS app with SwiftUI. When I click the "done" button, and the entry property is not nil, and I have not used the DatePicker TextField or TextView, I get the following runtime error in AppDelegate:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee2a83fe8)
Here is my code:
import SwiftUI
struct EditView: View {
#State var entry: Entry?
#ObservedObject var entries: Entries
#State var newDate: Date
#State var newTitle: String
#State var newBody: String
#Environment(\.presentationMode) var presentationMode
init(entries: Entries, entry: Entry?) {
UIScrollView.appearance().keyboardDismissMode = .onDrag
_entry = .init(initialValue: entry)
_entries = .init(initialValue: entries)
_newDate = .init(initialValue: entry?.date ?? Date())
_newTitle = .init(initialValue: entry?.title ?? "")
_newBody = .init(initialValue: entry?.body ?? "")
}
var body: some View {
GeometryReader { geometry in
Form {
Section {
DatePicker("Date", selection: self.$newDate, in: ...Date(), displayedComponents: .date)
.labelsHidden()
}
Section {
TextField("Title (optional)", text: self.$newTitle)
TextView(placeholder: "Entry", text: self.$newBody)
.frame(width: geometry.size.width, height: 250, alignment: .topLeading)
}
}
}
.navigationBarItems(trailing:
Button("Done") {
if let entry = self.entry {
if let index = self.entries.list.firstIndex(of: entry) {
self.entries.list[index] = Entry(date: self.newDate, title: self.newTitle, body: self.newBody)
}
} else {
self.entries.list.append(Entry(date: self.newDate, title: self.newTitle, body: self.newBody))
}
self.presentationMode.wrappedValue.dismiss()
})
}
}
import Foundation
class Entries: ObservableObject {
#Published var list = [Entry]()
}
class Entry: ObservableObject, Identifiable, Equatable {
static func == (lhs: Entry, rhs: Entry) -> Bool {
return lhs.id == rhs.id
}
let id = UUID()
#Published var date: Date
var dateString: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter.string(from: self.date)
}
#Published var title: String
#Published var body: String
init(date: Date, title: String, body: String) {
self.date = date
self.title = title
self.body = body
}
static let example = Entry(date: Date(), title: "I wrote some swift today", body: "Today I wrote some swift for an app I'm developing. It was very fun.")
When I remove the self.presentationMode.wrappedValue.dismiss() line, the problem goes away. Though, I need that line to dismiss the view. Why would this be happening, and how can I fix it? Please forgive me if my code is a complete mess. Thank you!

It looks like it tries to update during dismissing, try to postpone dismiss a bit
DispatchQueue.main.async { // defer to next event
self.presentationMode.wrappedValue.dismiss()
}

Related

how to edit an existing reminder in an array

I have an array of reminders(reminder model) in a view model and want to be able to edit existing reminders specifically through and edit swipe action and then through the reminders detail screen. I tried adding a button with a sheet to my Homeview in a list and then tried updating the edited reminder in the reminders array to a property in my view model called existingRemindData by using an update function in the reminder model. this should work but the remind var created by the foreach loop in the home view doesn't keep its value when it is called in the sheet. In the home view under the edit swipe action when I assign homevm.existingRemindData = remind.data it is equal to whatever reminder I swipe on because I did a print statement to confirm but as soon as I try to use the remind var inside of the sheet for the edit action the remind var defaults to the first item in the reminder array in the view model which is obviously not right. how would I make it so it uses the correct reminder index value when trying to update the reminder or is there another way which I could implement this functionality. any help would be great and look in the code for clarification on what I talk about.
HomeView
'''
import SwiftUI
struct HomeView: View {
#StateObject private var homeVM = HomeViewModel()
#State var percent: Int = 1
#State var showDetailEditView = false
#State var showAddView = false
#State var dropDown = false
//#State var filter = false
var body: some View {
ZStack {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
ReminderView(remind: $remind)
//.background(remind.theme.mainColor)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.swipeActions(edge: .leading) {
Button(action: {
self.showDetailEditView.toggle()
homeVM.existingRemindData = remind.data
print(homeVM.existingRemindData.title)
}) {
Label("Edit", systemImage: "pencil")
}
}
.sheet(isPresented: $showDetailEditView) {
NavigationView {
ReminderEditView(data: $homeVM.existingRemindData)
.navigationTitle(homeVM.existingRemindData.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
self.showDetailEditView.toggle()
homeVM.existingRemindData = Reminder.Data()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
self.showDetailEditView.toggle()
print("\(remind.id) \(remind.title)")
print("\(homeVM.existingRemindData.id) \(homeVM.existingRemindData.title)")
remind.update(from: homeVM.existingRemindData)
homeVM.newRemindData = Reminder.Data()
}
}
}
.background(LinearGradient(gradient: Gradient(colors: [
Color(UIColor(red: 0.376, green: 0.627, blue: 0.420, alpha: 1)),
Color(UIColor(red: 0.722, green: 0.808, blue: 0.725, alpha: 1))
]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
.swipeActions(allowsFullSwipe: true) {
Button (role: .destructive, action: {
homeVM.deleteReminder(remind: remind)
}) {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
.onAppear(
perform: {
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
})
'''
Reminder edit view
'''
import SwiftUI
extension Binding {
static func ??(lhs: Binding<Optional<Value>>, rhs: Value) -> Binding<Value> {
return Binding(get: {lhs.wrappedValue ?? rhs}, set: {lhs.wrappedValue = $0})
}
}
struct ReminderEditView: View {
#ObservedObject var editVM: EditViewModel
init(data: Binding<Reminder.Data>) {
editVM = EditViewModel(data: data)
}
var body: some View {
Form {
Section {
TextField("Title", text: $editVM.data.title)
TextField("Notes", text: $editVM.data.notes ?? "")
.frame(height: 100, alignment: .top)
}
Section {
Toggle(isOn: $editVM.data.hasDueDate, label: {
if editVM.data.hasDueDate {
VStack(alignment: .leading) {
Text("Date")
Text(editVM.data.hasDueDate ? editVM.data.formatDate(date: editVM.data.date!) : "\(editVM.data.formatDate(date: Date.now))")
.font(.caption)
.foregroundColor(.red)
}
} else {
Text("Date")
}
})
if editVM.data.hasDueDate {
DatePicker("Date", selection: $editVM.data.dueDate, in: Date()..., displayedComponents: .date)
.datePickerStyle(.graphical)
}
'''
Reminder model
'''
extension Reminder {
struct Data: Identifiable {
var title: String = ""
var notes: String?
var date: Date?
var time: Date?
var theme: Theme = .poppy
var iscomplete: Bool = false
var priority: RemindPriority = .None
let id: UUID = UUID()
var dueDate: Date {
get {
return date ?? Date()
}
set {
date = newValue
}
}
var dueTime: Date {
get {
return time ?? Date()
}
set {
time = newValue
}
}
func formatDate(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .none
return formatter.string(from: date)
}
func formatTime(time: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter.string(from: time)
}
var hasDueDate: Bool {
get {
date != nil
}
set {
if newValue == true {
date = Date()
}
else {
date = nil
hasDueTime = false
}
}
}
var hasDueTime: Bool {
get {
time != nil
}
set {
if newValue == true {
time = Date()
hasDueDate = true
}
else {
time = nil
}
}
}
}
var data: Data {
Data(title: title, notes: notes, date: date, time: time, theme: theme, iscomplete: iscomplete, priority: priority)
}
mutating func update(from data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
}
init(data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
id = data.id
}
}
'''
HomeViewModel(View model talked about)
'''
import Foundation
import SwiftUI
import Combine
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder] = Reminder.sampleReminders
#Published var newRemindData = Reminder.Data()
#Published var existingRemindData = Reminder.Data()
#Published var selectedRemind = Reminder(data: Reminder.Data())
#Published var compReminds: [Reminder] = []
private var cancellables = Set<AnyCancellable>()
/*init(reminds: [Reminder]) {
self.reminds = reminds
}*/
func newReminder() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
func deleteReminder(remind: Reminder) {
Just(remind)
.delay(for: .seconds(0.25), scheduler: RunLoop.main)
.sink {remind in
if remind.iscomplete {
self.removeRemind(remind: remind)
}
if !remind.iscomplete {
self.removeRemind(remind: remind)
}
self.reminds.removeAll { $0.id == remind.id }
}
.store(in: &cancellables)
}
func appendRemind(complete: Reminder) {
compReminds.append(complete)
}
func removeRemind(remind: Reminder) {
compReminds.removeAll() { $0.id == remind.id }
}
func remindIndex() -> Int {
return reminds.firstIndex(where: {
$0.id == existingRemindData.id
}) ?? 1
}
We don't use view model objects in SwiftUI. Change EditViewModel class to be an EditConfig struct, declare it as #State var config: EditConfig? use it as the item in sheet(item:onDismiss:content:) instead of the bool version.
Also, your date formatting is not SwiftUI compatible, you won't benefit from the labels being updated automatically when the user changes their region settings. To fix that remove the formatDate code and instead supply the formatter to Text or simply use .date. If using a formatter object make sure you aren't initing a new one every time, e.g. store one inside an #State struct or a static var. in SwiftUI we must not init objects in a View's init and body, only value types.

How to replicate the search selection on iOS's Mail app?

I have a list with a .searchable search bar on top of it. It filters a struct that has text, but also a date.
I'm trying to have the user select whether he/she wants to search for all items containing the specific text, or search for all items with a data. I thought the way that iOS Mail did it when you hit he search bar on top is a good way (I'm open to other options tho...).
It looks like this:
So, when you tap the search field, the picker, or two buttons, or a tab selector shows up. I can't quite figure which is it. Regardless, I tried with a picker, but:
I don't know where to place it
I don't know how to keep it hidden until needed, and then hide it again.
this is the basic code:
let dateFormatter = DateFormatter()
struct LibItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("myapp") public var collectables: [LibItem] = []
init() {
self.collectables = self.collectables.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.collectables = self.collectables.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
var body: some View {
NavigationView {
List(filteredItems) { collectable in
VStack(alignment: .leading) {
Spacer() Text(collectable.dateText).font(.caption).fontWeight(.medium).foregroundColor(.secondary)
Spacer()
Text(collectable.text).font(.body).padding(.leading).padding(.bottom, 1)
Spacer()
}
}
.listStyle(.plain)
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search..."
)
}
}
var filteredItems: [LibItem] {
data.collectables.filter {
searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText)
}
}
}
And I was trying to add something like, taking into account isSearching:
#Environment(\.isSearching) var isSearching
var searchBy = [0, 1] // 0 = by text, 1 = by date
#State private var selectedSearch = 0
// Yes, I'd add the correct text to it, but I wanted to have it
// working first.
Picker("Search by", selection: $selectedColor) {
ForEach(colors, id: \.self) {
Text($0)
}
}
How do I do it? How can I replicate that search UX from Mail? Or, is there any better way to let the user chose whether search text or date that appears when the user taps on the search?
isSearching works on a sub view that has a searchable modifier attached. So, something like this would work:
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
#State private var selectedItem = 0
var body: some View {
NavigationView {
VStack {
SearchableSubview(selectedItem: $selectedItem)
List(filteredItems) { collectable in
VStack(alignment: .leading) {
Spacer()
Text(collectable.dateText).font(.caption).fontWeight(.medium).foregroundColor(.secondary)
Spacer()
Text(collectable.text).font(.body).padding(.leading).padding(.bottom, 1)
Spacer()
}
}
.listStyle(.plain)
}.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search..."
)
}
}
var filteredItems: [LibItem] {
data.collectables.filter {
searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText)
}
}
}
struct SearchableSubview : View {
#Environment(\.isSearching) private var isSearching
#Binding var selectedItem : Int
var body: some View {
if isSearching {
Picker("Search by", selection: $selectedItem) {
Text("Choice 1").tag(0)
Text("Choice 2").tag(1)
}.pickerStyle(.segmented)
}
}
}

Selecting an existing item on an array or passing a new one to a .sheet

I have an array that contains the data that is displayed on a list. When the user hits "new", a sheet pops up to allow the user to enter a new item to the list.
I just added a swipe option to edit this item and I wanted to reuse the same sheet to edit the item's text. But I'm having problems understanding how to check whether a specific item was selected (by UUID?) to pass to the sheet, or it's a new item.
Code:
let dateFormatter = DateFormatter()
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State private var selectedItemId: UUID?
#State var searchText: String = ""
#State private var sheetIsShowing = false
NavigationView {
List(filteredNotes) { note in
VStack(alignment: .leading) {
//....
// not relevant code
}
.swipeActions(allowsFullSwipe: false) {
Button(action: {
selectedItemId = note.id
self.sheetIsShowing = true
} ) {
Label("Edit", systemImage: "pencil")
}
}
}
.toolbar {
// new item
Button(action: {
self.sheetIsShowing = true
}) {
Image(systemName: "square.and.pencil")
}
}
.sheet(isPresented: $sheetIsShowing) {
if self.selectedItemId == NULL { // <-- this is giving me an error
let Note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(isVisible: self.$sheetIsShowing, note: Note)
} else {
let index = data.notes.firstIndex(of: selectedItemId)
SheetView(isVisible: self.$sheetIsShowing, note: data.notes[index])
}
}
}
}
My rationale was to check whether self.selectedItemId == NULL was null or not, if not then pass that element to the sheet to be edited, if yes, the as it as a new element.
What am I doing wrong? And if there is a standard way to pass information to the sheet based on whether there is an item select or not, could you show me?
Thanks!
From this post, you can do like this in your case:
struct SheetForNewAndEdit: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
// The selected row
#State var selectedNote: NoteItem? = nil
#State private var sheetNewNote = false
// for test :
#State private var filteredNotes: [NoteItem] = [
NoteItem(id: UUID(), text: "111"),
NoteItem(id: UUID(), text: "222")];
var body: some View {
NavigationView {
List(filteredNotes) { note in
VStack(alignment: .leading) {
//....
// not relevant code
Text(note.text)
}
.swipeActions(allowsFullSwipe: false) {
Button(action: {
// the action select the note to display
selectedNote = note
} ) {
Label("Edit", systemImage: "pencil")
}
}
}
// sheet is displayed depending on selected note
.sheet(item: $selectedNote, content: {
note in
SheetView(note: note)
})
// moved tool bar one level (inside navigation view)
.toolbar {
// Toolbar item to have toolbar
ToolbarItemGroup(placement: .navigationBarTrailing) {
ZStack {
Button(action: {
// change bool value
self.sheetNewNote.toggle()
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
.sheet(isPresented: $sheetNewNote) {
let Note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(note: Note)
}
}
}
}
Note : SheetView does not need any more a boolean, but you can add one if you orefer
Swift uses nil, not null, so the compiler is complaining when you are comparing selected items to null. However, you will have another issue. Your selectedItemId is optional, so you can't just use it in your else clause to make your note. You are better off using an if let to unwrap it. Change it to:
.sheet(isPresented: $sheetIsShowing) {
if let selectedItemId = selectedItemId,
let index = data.notes.firstIndex(where: { $0.id == selectedItemId }) {
SheetView(isVisible: self.$sheetIsShowing, note: data.notes[index])
} else {
let note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(isVisible: self.$sheetIsShowing, note: note)
}
}
edit:
I realized that you were attempting to use two optionals without unwrapping them, so I changed this to an if let to make sure both are safely unwrapped.

SwiftUI Core Data Binding TextFields in DetailView

I have a SwiftUI app with SwiftUI App lifecycle that includes a master-detail type
list driven from CoreData. I have the standard list in ContentView and NavigationLinks
to the DetailView. I pass a Core Data entity object to the Detailview.
My struggle is setting-up bindings to TextFields in the DetailView for data entry
and for editing. I tried to create an initializer which I could not make work. I have
only been able to make it work with the following. Assigning the initial values
inside the body does not seem like the best way to do this, though it does work.
Since the Core Data entities are ObservableObjects I thought I should be able to
directly access and update bound variables, but I could not find any way to reference
a binding to Core Data in a ForEach loop.
Is there a way to do this that is more appropriate than my code below?
Simplified Example:
struct DetailView: View {
var thing: Thing
var count: Int
#State var localName: String = ""
#State private var localComment: String = ""
#State private var localDate: Date = Date()
//this does not work - cannot assign String? to State<String>
// init(t: Thing) {
// self._localName = t.name
// self._localComment = t.comment
// self._localDate = Date()
// }
var body: some View {
//this is the question - is this safe?
DispatchQueue.main.async {
self.localName = self.thing.name ?? "no name"
self.localComment = self.thing.comment ?? "No Comment"
self.localDate = self.thing.date ?? Date()
}
return VStack {
Text("\(thing.count)")
.font(.title)
Text(thing.name ?? "no what?")
TextField("name", text: $localName)
Text(thing.comment ?? "no comment?")
TextField("comment", text: $localComment)
Text("\(thing.date ?? Date())")
//TextField("date", text: $localDate)
}.padding()
}
}
And for completeness, the ContentView:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Thing.date, ascending: false)])
private var things : FetchedResults<Thing>
#State private var count: Int = 0
#State private var coverDeletedDetail = false
var body: some View {
NavigationView {
List {
ForEach(things) { thing in
NavigationLink(destination: DetailView(thing: thing, count: self.count + 1)) {
HStack {
Image(systemName: "gear")
.resizable()
.frame(width: 40, height: 40)
.onTapGesture(count: 1, perform: {
updateThing(thing)
})
Text(thing.name ?? "untitled")
Text("\(thing.count)")
}
}
}
.onDelete(perform: deleteThings)
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}
.navigationTitle("Thing List")
.navigationBarItems(trailing: Button("Add Task") {
addThing()
})
}
}
private func updateThing(_ thing: FetchedResults<Thing>.Element) {
withAnimation {
thing.name = "Updated Name"
thing.comment = "Updated Comment"
saveContext()
}
}
private func deleteThings(offsets: IndexSet) {
withAnimation {
offsets.map { things[$0] }.forEach(viewContext.delete)
saveContext()
self.coverDeletedDetail = true
}
}
private func addThing() {
withAnimation {
let newThing = Thing(context: viewContext)
newThing.name = "New Thing"
newThing.comment = "New Comment"
newThing.date = Date()
newThing.count = Int64(self.count + 1)
self.count = self.count + 1
saveContext()
}
}
func saveContext() {
do {
try viewContext.save()
} catch {
print(error)
}
}
}
And Core Data:
extension Thing {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Thing> {
return NSFetchRequest<Thing>(entityName: "Thing")
}
#NSManaged public var comment: String?
#NSManaged public var count: Int64
#NSManaged public var date: Date?
#NSManaged public var name: String?
}
extension Thing : Identifiable {
}
Any guidance would be appreciated. Xcode 12.2 iOS 14.2
You already mentioned it. CoreData works great with SwiftUI.
Just make your Thing as ObservableObject
#ObservedObject var thing: Thing
and then you can pass values from thing as Binding. This works in ForEach aswell
TextField("name", text: $thing.localName)
For others - note that I had to use the Binding extension above since NSManagedObjects are optionals. Thus as davidev stated:
TextField("name", text: Binding($thing.name, "no name"))
And ObservedObject, not Observable

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

Resources