How to properly group CoreData records by category in SwiftUI? - swift

I am using CoreData. I have:
1 Entity: Todo
3 attributes: category (String), date (Date), title (String).
Module: Current Product Module
Codegen: Class Definition
I would like to build a simple ToDo app that looks like this:
To do items
Category
1. 3/26/20 To do item 1
2. 3/26/20 To do item 2
Category
1. 3/26/20 To do item 3
2. 3/27/20 To do item 4
I know there are similar questions, but I didn't find an answer on how to set everything using CoreData and SwiftUI.
I have most of the code done. Including adding items, saving to CoreData, deleting items.
ContentView.swift
Here I display the To do list. I have added the comments to problematic parts.
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#State private var date = Date()
#FetchRequest(
entity: Todo.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Todo.date, ascending: true)
]
) var todos: FetchedResults<Todo>
#State private var show_modal: Bool = false
// let dictionary = Dictionary(grouping: Todo) { $0.category }
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
var body: some View {
NavigationView {
List {
// Here I should sort by category
ForEach(self.todos, id: \.title) { todo in
// Here I should write category name instead of static text
Section(header: Text("Category")) {
ForEach(Array(self.todos.enumerated()), id: \.element) {(i, todo) in
NavigationLink(destination: TodoDetailsView(todo: todo)) {
HStack {
Text("\(i+1). ")
Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
Text(todo.title ?? "")
}
}
}
}
}
}
.navigationBarTitle(Text("To do items"))
.navigationBarItems(
trailing:
Button(action: {
self.show_modal = true
}) {
Text("Add")
}.sheet(isPresented: self.$show_modal) {
TodoAddView().environment(\.managedObjectContext, self.moc)
}
)
.listStyle(GroupedListStyle())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
TodoAddView.swift
Here I add new items and save them to CoreData. This works OK.
import SwiftUI
struct TodoAddView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var moc
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
#State private var showDatePicker = false
#State private var title = ""
#State private var category = ""
#State private var date : Date = Date()
var body: some View {
NavigationView {
VStack {
HStack {
Button(action: {
self.showDatePicker.toggle()
}) {
Text("\(date, formatter: Self.dateFormat)")
}
Spacer()
}
if self.showDatePicker {
DatePicker(
selection: $date,
displayedComponents: .date,
label: { Text("Date") }
)
.labelsHidden()
}
TextField("title", text: $title)
TextField("category", text: $category)
Spacer()
}
.padding()
.navigationBarTitle(Text("Add to do item"))
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
},
trailing:
Button(action: {
let todo = Todo(context: self.moc)
todo.date = self.date
todo.title = self.title
todo.category = self.category
do {
try self.moc.save()
}catch{
print(error)
}
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
}
)
}
}
}
struct TodoAddView_Previews: PreviewProvider {
static var previews: some View {
TodoAddView()
}
}
TodoDetailsView.swift
Here I display details of each item and can delete an item from there. This works good too.
import SwiftUI
struct TodoDetailsView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var todo: Todo
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
#State private var showDatePicker = false
#State private var newDate : Date = Date()
#State private var newTitle = ""
#State private var newCategory = ""
var body: some View {
ScrollView {
VStack {
HStack {
Button(action: {
self.showDatePicker.toggle()
}) {
Text("\(newDate, formatter: Self.dateFormat)")
}
Spacer()
}
if self.showDatePicker {
DatePicker(
selection: $newDate,
displayedComponents: .date,
label: { Text("Date") }
)
.labelsHidden()
}
TextField("title", text: $newTitle, onCommit: {
self.todo.title = self.newTitle
try? self.moc.save()
}
)
TextField("category", text: $newCategory, onCommit: {
self.todo.category = self.newCategory
try? self.moc.save()
}
)
Spacer()
}
.padding()
.navigationBarTitle(Text("Details"))
.navigationBarItems(
trailing:
Button(action: {
self.moc.delete(self.todo)
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}catch{
print(error)
}
}) {
Text("Delete")
.foregroundColor(.red)
}
)
}
.onAppear {
self.newDate = self.todo.date ?? Date()
self.newTitle = self.todo.title ?? ""
self.newCategory = self.todo.category ?? ""
}
.onDisappear {
self.todo.date = self.newDate
self.todo.title = self.newTitle
self.todo.category = self.newCategory
try? self.moc.save()
}
}
}
struct TodoDetailsView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let todo = Todo.init(context: context)
todo.date = Date()
todo.title = "to do item"
return TodoDetailsView(todo: todo).environment(\.managedObjectContext, context)
}
}
The thing I don't know is how to properly group the items by, for example, category.
How to do it using Core Data? I assume I need to change Codegen of my Entity and add an extension to group the records. For example something like this:
let dictionary = Dictionary(grouping: Todo) { $0.category }
But should it be Manual/None or Category/Extension? And where and which code to use? I know how to create a Subclass.
And later I could probably reference to this dictionary in ForEach in my ContentView. But don't know how exactly.
If my assumptions are wrong, please correct me. Thanks in advance.

Related

SwiftUI ToDoList with checkboxes?

I want to write a ToDoList in swiftUI with core data. Everything works so far but I want to have a checkbox next to each item it Signify whether it is completed or not.
I have added a property isChecked:boolean in core data but I don't know how to properly read it from the database. How to use a Toggle() in my case?
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ToDoListItem.getAllToDoListItems())
var items: FetchedResults<ToDoListItem>
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section (header: Text("NewItem")){
HStack {
TextField("Enter new Item.",text: $text)
Button(action: {
if !text.isEmpty{
let newItem = ToDoListItem(context: context)
newItem.name = text
newItem.createdAt = Date()
// current date as created
newItem.isChecked = false
do {
try context.save()
} catch {
print(error)
}
// to clear the textField from the previous entry
text = ""
}
}, label: {
Text("Save")
})
}// end of vstack
}
Section {
ForEach(items){ toDoListItem in
VStack(alignment: .leading){
// to have a checkbox
Button {
toDoListItem.isChecked.toggle()
} label: {
Label(toDoListItem.name!, systemImage: toDoListItem.isChecked ? "checkbox.square" : "square")
}
if let name = toDoListItem.name {
// Toggle(isOn: toDoListItem.isChecked)
Text(name)
.font(.headline)
}
//Text(toDoListItem.name!)
//.font(.headline)
if let createdAt = toDoListItem.createdAt {
//Text("\(toDoListItem.createdAt!)")
Text("\(createdAt)")
}
}
}.onDelete(perform: { indexSet in
guard let index = indexSet.first else {
return
}
let itemToDelete = items[index]
context.delete(itemToDelete)
do {
try context.save()
}
catch {
print(error)
}
})
}
}
.navigationTitle("To Do List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ToDoListItem.swift
class ToDoListItem: NSManagedObject,Identifiable {
#NSManaged var name:String?
#NSManaged var createdAt:Date?
#NSManaged var isChecked:Bool
// mapped to the entry properties in database
}
extension ToDoListItem {
static func getAllToDoListItems() -> NSFetchRequest<ToDoListItem>{
let request:NSFetchRequest<ToDoListItem> = ToDoListItem.fetchRequest() as!
NSFetchRequest<ToDoListItem>
// cast as todolist item
let sort = NSSortDescriptor(key: "createdAt", ascending: true)
// above order of sorting
request.sortDescriptors = [sort]
return request
}
}
Should isChecked be an optional as well?

Value in SwiftUI not being passed

I am trying to pass that a values to let my item know what group it should be added to in Core Data.
Here are some pictures of my App.
Please note that itemsInGroup fetches all of the items that should be in the group.
After adding breakpoints in my app, the value of the group where the Item entity is being add is equal to nil. This should have a value (which is set when the add item button is pressed).
Thank you for your help in advance.
Main part of the code
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Group.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Group>
#State var activeSheet: ActiveSheet?
#State private var selectedGroup: Group? = nil
var body: some View {
NavigationView {
List {
ForEach(items) { group in
// Text("Item at \(item.timestamp!, formatter: itemFormatter)")
Text(group.title ?? "New group")
ForEach(group.itemsInGroup) { item in
Text(item.title ?? "New Item")
}
Button(action: {
selectedGroup = group
activeSheet = .second
}, label: {
Text("Add Item")
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
Button(action: {
activeSheet = .first
}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(item: $activeSheet) { item in
switch(item) {
case .first: AddGroupName()
case .second: AddItemView(group: selectedGroup)
}
}
}
private func addItem() {
withAnimation {
let newItem = Group(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)
}
}
Where I add a new item
struct AddItemView: View {
#Environment(\.managedObjectContext) private var viewContext
let group: Group?
#State var title = ""
var body: some View {
VStack {
Form {
TextField("Title", text: $title)
Button(action: {
let item = Item(context: viewContext)
item.group = group
item.title = title
try? viewContext.save()
}, label: {
Text("Save")
})
}
}
}
}
Where I add a new group
struct AddGroupName: View {
#Environment(\.managedObjectContext) private var viewContext
#State var title = ""
var body: some View {
VStack {
Form {
TextField("Title", text: $title)
Button(action: {
let item = Group(context: viewContext)
item.title = title
item.timestamp = Date()
try? viewContext.save()
}, label: {
Text("Save")
})
}
}
}
}
My Core Data Model
Why is the group value not being passed and saved correctly? It should be saved in the selectedGroup variable in the main part of the code.
When I try and add an item and save it to the core data database I get this error "Illegal attempt to establish a relationship 'group' between objects in different contexts"
Please note that I have tried setting selectedGroup equal to a value other than nil, but then this initial value is used when trying to add an item.
You should declare your group property using the #Binding property wrapper in AddItemView
#Binding var group: Group
This way the #State property selectedGroup will be updated when the AddItemView is dismissed

Model to search notes by date

How to model notes, so I can search them ,after, by dates? like on the picture.
Firstly, I thought about dictionary with key of type Date; but it didn't work to me.
So I've tried the following String format:
"04-28-2020" - US calendar
"28-04-2020" - Other calendar
One of the issues - if the telephone changes date style(region dependent), keys remain in dictionary, but aren't accessed any more...
My intuition that it should be the dictionary, but what are the sustainable way to make such a model?
Can it be an array of notes [Note], without keys?
struct Note: Codable, Identifiable {
let id = UUID()
var date: Date
var title: String
var description: String
var dateString: String {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .medium
return df.string(from: date)
}
// method returns "key" to be used in dictionary
static func dateKey(for date: Date) -> String {
let df = DateFormatter()
df.dateStyle = .short
return df.string(from: date)
}
}
class NotesOrganizer: ObservableObject {
// #Published var notes = [Note]() - tried initially
#Published var notesDictionary = [String : [Note]]()
}
struct ContentView: View {
#State private var title = ""
#State private var description = ""
#ObservedObject var notesOrganizer = NotesOrganizer()
var body: some View {
NavigationView {
VStack {
Button("Save note") {
let key = Note.dateKey(for: Date())
notesOrganizer.notesDictionary[key]?.append(Note(date: Date(), title: title, description: description))
?? (notesOrganizer.notesDictionary[key] = [Note(date: Date(), title: title, description: description)])
print("Date Key: \(Note.dateKey(for: Date()))")
}
NavigationLink(destination: DetailView(notesOrganizer: notesOrganizer))
{
Text("Log history ->")
.foregroundColor(.purple)
.underline()
}
}
.navigationBarHidden(true)
}
}
init() {
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
UINavigationBar.appearance().shadowImage = UIImage()
}
}
DetailView()
extension Date {
var tomorrow: Date? {
return Calendar.current.date(byAdding: .day, value: 1, to: self)
}
var yesterday: Date? {
return Calendar.current.date(byAdding: .day, value: -1, to: self)
}
}
struct DetailView: View {
#ObservedObject var notesOrganizer: NotesOrganizer
#State private var dayLabel: String = Note.dateKey(for: Date())
#State private var chosenDate = Date()
var body: some View {
VStack {
HStack {
Button(action: {
chosenDate = chosenDate.yesterday!
dayLabel = Note.dateKey(for: chosenDate)
}, label: {
Image(systemName: "chevron.left")
})
Text(dayLabel)
Spacer()
Button(action: {
chosenDate = chosenDate.tomorrow!
dayLabel = Note.dateKey(for: chosenDate)
}, label: {
Image(systemName: "chevron.right")
})
}
.padding([.horizontal,.bottom])
List{
ForEach(notesOrganizer.notesDictionary[day] ?? []) { note in
HStack {
VStack(alignment:.leading) {
Text(note.title)
Text(note.description)
}.multilineTextAlignment(.leading)
Spacer()
Text(note.dateString)
}
}.onDelete(perform: removeItems)
}
.navigationBarTitle("", displayMode: .inline)
Spacer()
}

Listing CoreData object through Relationship

I had this working without CoreData relationships (multiple fetches), but it occurred to me that I should probably have relationships between these entities implemented, so that I can just fetch from a single entity to get all attributes.
When I fetch accountNames from the Accounts entity directly for my AccountsList.swift (to create accounts) - it works just fine, but when I try to call them through the relationship (originAccounts), it doesn't show anything in the list. Same issue for the Categories picker.
I have 3 CoreData entities, and two Pickers (for category and account)
Expenses
expenseAccount:String
expenseCategory:String
expenseCost:Double
expenseDate:Date
expenseId:UUID
expenseIsMonthly:Bool
expenseName:String
Categories
categoryName:String
Accounts
accountName:String
Expenses has a many to one relationship with both Accounts and Categories
import SwiftUI
import CoreData
struct ExpenseDetail: View {
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
#FetchRequest(fetchRequest: Expenses.expensesList)
var results: FetchedResults<Expenses>
var logToEdit: Expenses?
#State var name: String = ""
#State var amount: String = ""
#State var category: String?
#State var date: Date = Date()
#State var account: String?
#State var isMonthly: Bool = false
var currencyFormatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
return f
}()
var body: some View {
NavigationView {
Form{
TextField("Expense Name", text: $name)
Section{
HStack{
TextField("$\(amount)", text: $amount)
.keyboardType(.decimalPad)
.textFieldStyle(PlainTextFieldStyle())
.disableAutocorrection(true).multilineTextAlignment(.leading)
}
DatePicker(selection: $date, displayedComponents: .date) {
Text("Date")
}.onAppear{self.hideKeyboard()}
Picker(selection: $category, label: Text("Category")) {
ForEach(results) { (log: Expenses) in
Text(log.originCategories?.categoryName ?? "No Category").tag(log.originCategories?.categoryName)
}
}
Picker(selection: $account, label: Text("Account")) {
ForEach(results) { (log: Expenses) in
Text(log.originAccounts?.accountName ?? "No Account").tag(log.originAccounts?.accountName)
}
}
Toggle(isOn: $isMonthly) {
Text("Monthly Expense")
}.toggleStyle(CheckboxToggleStyle())
}
Section{
Button(action: {
onSaveTapped()
}) {
HStack {
Spacer()
Text("Save")
Spacer()
}
}
}
Section{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Spacer()
Text("Cancel").foregroundColor(.red)
Spacer()
}
}
}
}.navigationBarTitle("Add Expense")
}
}
private func onSaveTapped() {
let expenseLog: Expenses
if let logToEdit = self.logToEdit {
expenseLog = logToEdit
} else {
expenseLog = Expenses(context: self.context)
expenseLog.expenseId = UUID()
}
expenseLog.expenseName = self.name
expenseLog.originCategories?.categoryName = self.category
expenseLog.expenseCost = Double(self.amount) ?? 0
expenseLog.expenseDate = self.date
expenseLog.originAccounts?.accountName = self.account
print("\(self.account ?? "NoAccountValue")")
expenseLog.expenseIsMonthly = self.isMonthly
do {
try context.save()
} catch let error as NSError {
print(error.localizedDescription)
}
self.presentationMode.wrappedValue.dismiss()
}
}
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ExpenseDetail_Previews: PreviewProvider {
static var previews: some View {
ExpenseDetail()
}
}
struct CheckboxToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
return HStack {
configuration.label
Spacer()
Image(systemName: configuration.isOn ? "checkmark.square" : "square")
.resizable()
.frame(width: 22, height: 22)
.onTapGesture { configuration.isOn.toggle() }
}
}
}
expensesList fetch details, if needed
static var expensesList: NSFetchRequest<Expenses> {
let request: NSFetchRequest<Expenses> = Expenses.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "expenseName", ascending: true)]
return request
}

Using Picker with Core Data

I have seen a number of responses about similar issues on here, but the picker for $category in the code below doesn't seem to work. When I select the picker, I see the list of categoryNames, but when I choose one, it doesn't get populated into $category.
I have 2 CoreData entities:
Expenses
expenseAccount:String
expenseCategory:String
expenseCost:Double
expenseDate:Date
expenseId:UUID
expenseIsMonthly:Bool
expenseName:String
Categories
categoryName:String
import SwiftUI
import CoreData
struct ExpenseDetail: View {
#FetchRequest(
entity: Categories.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Categories.categoryName, ascending: true)
]
)
private var result: FetchedResults<Categories>
var logToEdit: Expenses?
#Environment(\.managedObjectContext) var context
#State var name: String = ""
#State var amount: String = ""
#State var category: String = ""
#State var date: Date = Date()
#State var account: String = ""
#State var isMonthly: Bool = false
var currencyFormatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
return f
}()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form{
TextField("Expense Name", text: $name)
Section{
HStack{
TextField("$\(amount)", text: $amount)
.keyboardType(.decimalPad)
.textFieldStyle(PlainTextFieldStyle())
.disableAutocorrection(true).multilineTextAlignment(.leading)
}
DatePicker(selection: $date, displayedComponents: .date) {
Text("Date")
}.onAppear{self.hideKeyboard()}
Picker(selection: $category, label: Text("Category")) {
ForEach(result) { (log: Categories) in
Text(log.categoryName ?? "No Category").tag(log.categoryName)
}
}
Picker(selection: $account, label: Text("Account")) {
ForEach(result) { (log: Categories) in
self.Print("\(log.categoryName ?? "")")
Button(action: {
// TODO: Implement Edit
}) {
Text(log.categoryName!.capitalized).tag(self.category)
}
}
}
Toggle(isOn: $isMonthly) {
Text("Monthly Expense")
}.toggleStyle(CheckboxToggleStyle())
}
Section{
Button(action: {
onSaveTapped()
}) {
HStack {
Spacer()
Text("Save")
Spacer()
}
}
}
Section{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Spacer()
Text("Cancel").foregroundColor(.red)
Spacer()
}
}
}
}.navigationBarTitle("Add Expense")
}
}
private func onSaveTapped() {
let expenseLog: Expenses
if let logToEdit = self.logToEdit {
expenseLog = logToEdit
} else {
expenseLog = Expenses(context: self.context)
expenseLog.expenseId = UUID()
}
expenseLog.expenseName = self.name
expenseLog.expenseCategory = self.category
print("\(expenseLog.expenseName ?? "") category Picker: \(self.category)")
print("\(expenseLog.expenseName ?? "") ExpenseCategory: \(expenseLog.expenseCategory!)")
expenseLog.expenseCost = Double(self.amount) ?? 0
print("\(expenseLog.expenseName ?? "") Amount: \(self.amount)")
print("\(expenseLog.expenseName ?? "")ExpenseCost: \(expenseLog.expenseCost)")
expenseLog.expenseDate = self.date
expenseLog.expenseAccount = self.account
expenseLog.expenseIsMonthly = self.isMonthly
do {
try context.save()
} catch let error as NSError {
print(error.localizedDescription)
}
self.presentationMode.wrappedValue.dismiss()
}
}
The type of the selection value is String while Category.categoryName is String?. Notice the added ?, this means it is an Optional.
It is the default type for CoreData, but you can remove the Optional value inside the model:
I would ask myself does a Category without a name make sense before doing this change. If it does, you will probably have to use another identifier for the selection.