SwiftUI ToDoList with checkboxes? - swift

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?

Related

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

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
}

SwiftUI: View Model does not update the View

I try to implement a Search Bar with Algolia, and I use the MVVM pattern.
Here's my View Model:
class AlgoliaViewModel: ObservableObject {
#Published var idList = [String]()
func searchUser(text: String){
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
var idList = [String]()
for x in hits {
idList.append(x.objectID.rawValue)
}
DispatchQueue.main.async {
self.idList = idList
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
Here is my View :
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body : some View{
VStack(alignment: .leading){
Text("Select To Chat").font(.title).foregroundColor(Color.black.opacity(0.5))
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12){
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText) })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList, id: \.self){ i in
Text(i)
}
}
}
}
}.padding()
}
}
I often use this pattern with Firebase and everything works fine, but here with Algolia the List remains empty in the NewChatView.
The print(self.idList) statement inside the View-Model shows the right idList, but it does not update the List inside the NewChatView.
You first need to create your own custom Identifiable and Hashable model to display the searchValue in a List or ForEach.
Something like this:
struct MySearchModel: Identifiable, Hashable {
let id = UUID().uuidString
let searchValue: String
}
Then use it in your AlgoliaViewModel. Set a default value of an empty array.
You can also map the hits received and convert it to your new model. No need for the extra for loop.
class AlgoliaViewModel: ObservableObject {
#Published var idList: [MySearchModel] = []
func searchUser(text: String) {
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
DispatchQueue.main.async {
self.idList = hits.map({ MySearchModel(searchValue: $0.objectID.rawValue) })
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
For the NewChatView, you can remove the ScrollView as it conflicts with the elements inside your current VStack and would hide the List with the results as well. The following changes should display all your results.
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body: some View{
VStack(alignment: .leading) {
Text("Select To Chat")
.font(.title)
.foregroundColor(Color.black.opacity(0.5))
VStack {
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText)
})
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList) { i in
Text(i.searchValue)
.foregroundColor(Color.black)
}
}
}
}.padding()
}
}

SwiftUI adding to list moves all rows down problem

I have a main list where I have 3 sections. Third section has a list of character names that can be pinned and when pinned are added to the first two sections. However, as they are pinned the rows move down. This is a serious design flaw and can result in annoyance from the user, I can only think of using a navigation link to display on another page instead. Any other way to display it on the same page without annoying the users? As seen on the images the rows have moved below so the user has to pay enough attention and not click on the incorrect row as all rows move below, instead would it possible to keep the row in same location but move the scroll view upwards, as seen on the image below.
import SwiftUI
struct TestingListView: View {
let names = ["Ashton", "Noah", "Ben", "Theo", "Ferzardo"]
#StateObject var pinned = Pinned()
var body: some View {
NavigationView {
List {
let peopleNames = getNames(names: pinned.person)
Section(header: Text("Pinned Characters")) {
if pinned.person.count == 0 {
Text("Pin your favourite characters here")
}
ForEach(pinned.person) { thePerson in
Text(thePerson.name)
}.onDelete(perform: remove)
}
Section(header: Text("Checking pins")) {
ForEach(peopleNames, id: \.self) { pNames in
Text(pNames)
}.onDelete(perform: remove)
}
Section(header: Text("Names")) {
ForEach(names, id: \.self) { name in
let isPinned = peopleNames.contains(name)
HStack {
Text(name)
Spacer()
Image(systemName: isPinned ? "pin.fill" : "pin")
.padding([.leading, .trailing], 20)
.foregroundColor(.red)
.onTapGesture {
if !isPinned {
let data = PinnedPerson(name: name, device: "Xbox")
withAnimation(.spring()) {
pinned.person.append(data)
}
}
else {
let index = peopleNames.firstIndex(of: name)!
withAnimation(.spring()) {
pinned.person.remove(at: index)
}
}
}
}
}
}//end of section
}//.listStyle(SidebarListStyle())//list end
.navigationBarTitle("Characters")
}
}
func remove(at offsets: IndexSet) {
pinned.person.remove(atOffsets: offsets)
}
func getNames(names: [PinnedPerson]) -> [String] {
var peopleNames = [String]()
let count = names.count
for i in (0..<count) {
peopleNames.append("\(names[i].name)")
}
if names.count == 0 {
peopleNames.append("Hi")
}
return peopleNames
}
}
struct TestingListView_Previews: PreviewProvider {
static var previews: some View {
TestingListView()
}
}
struct PinnedPerson: Identifiable, Codable {
var id = UUID()
let name: String
let device: String
}
class Pinned: ObservableObject {
#Published var person = [PinnedPerson]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(person) {
UserDefaults.standard.setValue(encoded, forKey: "Person")
}
}
}
init() {
if let data = UserDefaults.standard.data(forKey: "Person") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([PinnedPerson].self, from: data) {
self.person = decoded
return
}
}
self.person = []
}
}

How to properly group CoreData records by category in SwiftUI?

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.