Coredata dynamic filters (predicate) in SwiftUI - filter

I found many tutorials pointing out how to create a dynamic filter in Swiftui and Coredata, but none solved my problem, which is to create an entity extension like:
extension Food {
var myFetchRequest: NSFetchRequest<Food> {
let request: NSFetchRequest<Food> = Food.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
request.predicate = NSPredicate(format: "name CONTAINS[c] %#", searchText)
}
}
because obviously I get the following error:
I would like to get the following result:
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjContext
#FetchRequest(fetchRequest: Food.myFetchRequest)
var food: FetchedResults<Food>
#State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(food) { food in
NavigationLink(destination: EditFoodView(food: food)) {
HStack {
VStack(alignment: .leading, spacing: 6) {
Text(food.name!)
.bold()
Text("\(Int(food.calories))") + Text(" calories").foregroundColor(.red)
}
Spacer()
Text(calcTimeSince(date: food.date!))
.foregroundColor(.gray)
.italic()
}
}
}
.onDelete(perform: deleteFood)
}
.listStyle(.plain)
}
.navigationTitle("iCalories")
.searchable(text: $searchText)
}
// Deletes food at the current offset
private func deleteFood(offsets: IndexSet) {
withAnimation {
offsets.map { food[$0] }
.forEach(managedObjContext.delete)
// Saves to our database
DataController().save(context: managedObjContext)
}
}
}
How do I have to change extension Food { to make the code work?

Related

How to make 'Published<[String]>.Publisher' conform to 'RandomAccessCollection'

I am trying to make a view that updates based on if the user toggles the favorite button or not. I want the entire view to reconstruct in order to display an array of values whenever that array of values is changed. Inside the view, a for each loop should display every value in the array.
The view that I want to update every time savedArray is changed is FavView. But when I try to use a foreach loop to display every value is savedArray(which I created as a #Published so the view would reconstruct), it gives me the error Generic struct 'ForEach' requires that 'Published<[String]>.Publisher' conform to 'RandomAccessCollection'. I am confused because I thought that String arrays were able to be used in for each loops. Is this not true? How do I loop through a #Published array? Thank you!
This is my code for the savedArray(in ViewModel) and the FavView I want to display it in with the for each.
struct ContentView: View {
#StateObject private var statNavManager = StatsNavigationManager()
#State private var saved: [String] = []
var body: some View {
TabView {
StatsView(saved: $saved)
.tabItem {
Label("Home", systemImage: "house")
}
FavView(saved: $saved)
.tabItem {
Label("Saved", systemImage: "bookmark")
}
}
.environmentObject(statNavManager)
}
}
final class ViewModel: ObservableObject {
#Published var items = [Item]()
#Published var showingFavs = true
#Published var savedItems: Set<String> = []
#Published var savedArray: [String]
// Filter saved items
var filteredItems: [String] {
//return self.items
return savedArray
}
var db = Database()
init() {
self.savedItems = db.load()
self.items = db.returnList()//the items
self.savedArray = Array(db.load())
print("savedarray", savedArray)
print("important!", self.savedItems, self.items)
}
func contains(_ item: Item) -> Bool {
savedItems.contains(item.id)
}
// Toggle saved items
func toggleFav(item: Item) {
print("Toggled!", item)
if contains(item) {
savedItems.remove(item.id)
if let index = savedArray.firstIndex(of: item.id) {
savedArray.remove(at: index)
}
} else {
savedItems.insert(item.id)
savedArray.append(item.id)
}
db.save(items: savedItems)
}
}
struct FavView: View {
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
List {
var x = print("testing",vm.savedArray)//this only prints once at the start
ForEach($vm.savedArray, id: \.self) { string in
let item = vm.db.returnItem(input: string.wrappedValue)
HStack {
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.subheadline)
}
Spacer()
Image(systemName: vm.contains(item) ? "bookmark.fill" : "bookmark")
.foregroundColor(.blue)
.onTapGesture {
vm.toggleFav(item: item)
}
}
}
}
.cornerRadius(10)
}
}
}
in ForEach, you are using $ symbol to access savedArray you have to use the vm itself
struct FavView: View {
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
List {
ForEach($vm.savedArray, id: \.self) { string in //< here $vm.savedArray not vm.$savedArray
let item = vm.db.returnItem(input: string)
HStack {
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.subheadline)
}
Spacer()
Image(systemName: vm.contains(item) ? "bookmark.fill" : "bookmark")
.foregroundColor(.blue)
.onTapGesture {
vm.toggleFav(item: item)
}
}
}
}
.cornerRadius(10)
}
}
}
this should work.

Sending an NSManagedObjectID to a struct / view

I'm complete new to swift, swiftui and coredata. I have good programming experience in other languages, but swift is its own world. :-)
Important information: it's for macOS not iOS!!
My problem: I want to edit a Dataset in an separate view displayed in a sheet. I followed this example (SwiftUI update view on core data object change), but when trying to run, my NSManagedObjectID is allway nil.
The ContentView (shortened)
import SwiftUI
import CoreData
struct ContentView: View {
#State public var selectedBookId: NSManagedObjectID?
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Books.title, ascending: true)],
animation: .default)
private var books: FetchedResults<Books>
#State private var showingEditScreen = false
var body: some View {
NavigationView {
List {
ForEach(books, id: \.self) { book in
HStack {
NavigationLink {
HStack {
Button {
// here store objectID to var
selectedBookId = book.objectID
showingEditScreen.toggle()
} label: {
Label("", systemImage: "pencil")
}
}
.padding(10.0)
} label: {
Text(book.title!)
}
}
}.onDelete(perform: deleteBooks)
}
.toolbar {
ToolbarItem(placement: .automatic) {
// here goes blabla
}
}
Text("Bitte zuerst ein Buch auswählen!")
}
.sheet(isPresented: $showingEditScreen) {
// Run EditBookView an send bookId
EditBookView(bookId: selectedBookId).environment(\.managedObjectContext, self.viewContext)
}
}
}
My EditView looks like this
import SwiftUI
struct EditBookView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.dismiss) var dismiss
var bookId: NSManagedObjectID! // This is allways nil!!
var book: Books {
moc.object(with: bookId) as! Books
}
#State private var title = ""
#State private var review = ""
var body: some View {
Form {
Text("Edit Book").font(.title)
Spacer()
Section {
TextField("Buchname", text: $title)
TextEditor(text: $review)
} header: {
Text("Schreibe eine Zusammenfassung")
}
Spacer()
Section {
HStack {
Button("Save") {
// add the book
// here code for update
try? moc.save()
dismiss()
}
Button("Cancel") {
print(bookId) // shows "nil"
dismiss()
}
}
}
Spacer()
}
.onAppear {
self.title = self.book.title ?? ""
self.review = self.book.review ?? ""
}
.padding(10.0)
}
}
First: thanks for all the good hints. In the end, I could solve the problem using
#ObservedObject var aBook: Books
at the beginning of my EditView.
The button itself has the following code
Button {
showingEditScreen.toggle()
} label: {
Label("", systemImage: "pencil")
}.sheet(isPresented: $showingEditScreen) {
EditBookView(aBook: book).environment(\.managedObjectContext, self.viewContext)
}
This way, I can send the whole book object of a single book item to the edit view and I can use it.

SwiftUI - Picker .onChange and didSet

I'm trying to achieve the behavior in the attached GIF:
Sorry for the High Speed, I had to compress it dramatically to be able to upload it here. The App is "Documents" from Readdle if you want to have a look on your own.
Anyways: I'm exactly trying to achieve this behavior (sorting and filtering, including the dynamic arrow up down icon).
I tried the following approach, however I'm not able to achieve this "ontap" expierience. On Change only triggers when I change the value but when I want to sort an existing value ascending and descending it's not working (which is obvious because it's not changing). I already played around with "didSet" but this also did not work.
Do you have an idea how this can be accomplished?
Below is my code:
import SwiftUI
struct ContentView: View {
#State var selection = 0
#State var sortByAsc = true
#State var filterColumn = "A"
//Test to set case via picker but picter doesnt execute didSet
#State var myFilterTest: MyFilters = .alphabetical {
didSet {
switch myFilterTest {
case .creationDate:
sortByAsc.toggle()
print("c")
case .rating:
sortByAsc.toggle()
print("b")
case .alphabetical:
sortByAsc.toggle()
print("a")
}
}
}
var body: some View {
NavigationView {
Text("Hello, World!")
.padding()
.navigationTitle("SwiftUI")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu(content: {
Picker("My Picker", selection: $selection) {
Label("Title", systemImage: sortByAsc ? "arrow.down" : "arrow.up")
.tag(0)
Label("Rating", systemImage: sortByAsc ? "arrow.down" : "arrow.up")
.tag(1)
.onTapGesture {
print("tap")
}
}
.onChange(of: selection) { tag in
print("Selected Tag: \(tag)")
sortByAsc.toggle()
if(tag == 0) {
filterColumn = "Title"
}
if(tag == 1) {
filterColumn = "Rating"
}
}
}, label: {
Image(systemName: "ellipsis.circle")
})
}
}
}
}
}
enum MyFilters: CaseIterable {
case alphabetical
case rating
case creationDate
}
Solved It. Here's the Code:
struct PickerView: View {
#State private var pickerIndex = 0
#State private var previousPickerIndex = 0
#State var sortByAsc = true
var body: some View {
let pickerSelection = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
if(pickerIndex == previousPickerIndex) {
sortByAsc.toggle()
}
previousPickerIndex = pickerIndex
})
NavigationView {
Text("Hello, World!")
.padding()
.navigationTitle("SwiftUI")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu(content: {
Picker("My Picker", selection: pickerSelection) {
ForEach(0..<4, id: \.self) { index in
Label("Title \(index)", systemImage: getSortingImage(menuItem: index))
.tag(index)
}
}
}, label: {
Image(systemName: "ellipsis.circle")
})
}
}
}
}
func getSortingImage(menuItem: Int) -> String {
if(menuItem == pickerIndex) {
if(sortByAsc) {
return "arrow.down"}
else {
return "arrow.up"
}
}
else {
return ""
}
}
}

Deleting an item from a list based on the element UUID

I feel a bit embarrassed for asking this, but after more than a day trying I'm stuck. I've had a few changes on the code based on replies to other issues. The latest code essentially selects the items on a list based on the UUID.
This has caused my delete function to stop working since I was working with passing an Int as the selected element to be deleted. I was originally implementing things like this.
Code follows, I'm still trying to figure out my way around SwiftUI, but question is, how can I now delete items on a list (and on the array behind it) based on a UUID as opposed to the usual selected item.
In case it makes a difference, this is for macOS Big Sur.
Code:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
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 AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
ToolbarItem(placement: .automatic) {
Button(action: {
// Delete here????
}) {
Image(systemName: "trash")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
The original removeNote I had is the following:
func removeNote() {
if let selection = self.selectedItem,
let selectionIndex = data.notes.firstIndex(of: selection) {
print("delete item: \(selectionIndex)")
data.notes.remove(at: selectionIndex)
}
}
could you try this:
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID // <--- here
var text: String
var date = Date()
var dateText: String = ""
var tags: [String] = []
}
func removeNote() {
if let selection = selectedNoteId,
let selectionIndex = data.notes.firstIndex(where: { $0.id == selection }) {
print("delete item: \(selectionIndex)")
data.notes.remove(at: selectionIndex)
}
}

Work out time 1.5 hours ahead using current time and display this in the list

I have the following code someone presses on the Table and its displays the current time which is the arrival time of a customer.
I want to display the time they must leave by, next to the current time this will always be 1.5 hours ahead I can not work out how to do this. everything I have tried comes back with an error.
Still new to Xcode
any help would be great
import SwiftUI
struct TimeListView: View {
#State var tableOne = false
#State var tableTwo = false
#State var tableThree = false
var body: some View {
// Title
VStack {
Text("Arrival Times")
.font(.title)
.fontWeight(.bold)
// List View
List {
// Table 1
HStack {
Button(action: {
self.tableOne.toggle()
}, label: {
Text("Table 1 -")
})
if tableOne {
Text(getCurrentTime())
}
}
// Table 2
HStack {
Button(action: {
self.tableTwo.toggle()
}, label: {
Text("Table 2 -")
})
if tableTwo {
Text(getCurrentTime())
}
}
// Table 3
HStack {
Button(action: {
self.tableThree.toggle()
}, label: {
Text("Table 3 -")
})
if tableThree {
Text(getCurrentTime())
}
}
}
}
}
}
struct TimeListView_Previews: PreviewProvider {
static var previews: some View {
TimeListView()
}
}
// Get Current Time Function
func getCurrentTime() -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_UK_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter.string(from: Date())
you need make a date and add 1.5 hour to it, also you forgot create 3 deferent State for them.
import SwiftUI
struct ContentView: View {
var body: some View {
TimeListView()
}
}
struct TimeListView: View {
#State private var table1: Bool = false
#State private var table2: Bool = false
#State private var table3: Bool = false
#State private var timeForShow1: String?
#State private var timeForShow2: String?
#State private var timeForShow3: String?
var body: some View {
VStack {
Text("Arrival Times")
.font(.title)
.fontWeight(.bold)
List {
HStack {
Text("Table 1 -")
.onTapGesture {
if table1 { table1.toggle() }
else { timeForShow1 = getCurrentTime; table1.toggle() }
}
if table1 { Text(timeForShow1 ?? "not available!") }
}
HStack {
Text("Table 2 -")
.onTapGesture {
if table2 { table2.toggle() }
else { timeForShow2 = getCurrentTime; table2.toggle() }
}
if table2 { Text(timeForShow2 ?? "not available!") }
}
HStack {
Text("Table 3 -")
.onTapGesture {
if table3 { table3.toggle() }
else { timeForShow3 = getCurrentTime; table3.toggle() }
}
if table3 { Text(timeForShow3 ?? "not available!") }
}
}
}
}
}
var getCurrentTime: String? {
if let date = Calendar.current.date(byAdding: .minute, value: 90, to: Date()) {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_UK_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter.string(from: date)
}
else {
return nil
}
}
You'll probably get a half dozen different ways of doing this but this version allows for adaptive/reusable code. You can easily add a Table by adding one to the var tables: [Table] everything would adjust automatically.
import SwiftUI
class Table: ObservableObject {
let id: UUID = UUID()
#Published var name: String
#Published var status: Bool
#Published var entryTime: Date
var exitTime: Date{
return entryTime.addingTimeInterval(60*60*1.5)
}
init(name: String, status: Bool = false, entryTime: Date = Date.init(timeIntervalSince1970: 0)) {
self.name = name
self.status = status
self.entryTime = entryTime
}
}
struct TimeListView: View {
#State var tables: [Table] = [Table(name: "Table 1 -"), Table(name: "Table 2 -"), Table(name: "Table 3 -")]
var body: some View {
VStack{
Text("Arrival Times")
.font(.title)
.fontWeight(.bold)
List {
ForEach(tables, id: \.id, content: { table in
TableView(table: table)
})
}
}
}
}
struct TableView: View {
#ObservedObject var table: Table
var body: some View {
HStack {
Button(action: {
table.status.toggle()
table.entryTime = Date()
}, label: {
Text(table.name)
})
if table.status{
Text(table.entryTime, formatter: dateFormatter)
Text(table.exitTime, formatter: dateFormatter)
}
}
}
}
struct TimeListView_Previews: PreviewProvider {
static var previews: some View {
TimeListView()
}
}
var dateFormatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_UK_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter
}

Resources