Swiftui searchable list with a datepicker? - swift

I have a List which is searchable.
Is is possible to make the search with a datepicker UI?
NavigationView {
List {
ForEach (articles) { article in
ArticleRow(article: article)
}
}
Text("Content")
.navigationTitle("Articles")
}
.searchable(text: $searchText)

Your code is more or less generic. So you would need to adapt this solution to fit your code. But it should give you an idea on how to do this.
//struct used for this example
struct Article: Identifiable{
let id = UUID()
var date: Date
}
struct TestView: View{
//holds the unfiltered array
let articles: [Article]
#State private var selectedDate: Date = Date()
//filtered array
#State private var sorted: [Article] = []
var body: some View{
NavigationView {
List {
//iterate over the sorted array
ForEach (sorted) { article in
// ArticleRow(article: article)
Text(article.date, format: .dateTime)
}
}
Text("Content")
.navigationTitle("Articles")
}
//remove the searchable modifier
// .searchable(text: $searchText)
// react to changes of the selected Date
.onChange(of: selectedDate) { newValue in
//filter the array
// I filtered only for the day itself ignoring time
self.sorted = articles.filter{ Calendar.current.compare($0.date, to: selectedDate, toGranularity: .day) == .orderedSame}
}.onAppear{
//assign the initial array
sorted = articles
}
//DatePicker to select the date
DatePicker("Select Date", selection: $selectedDate)
}
}

Here is the code I implemented to search using the DatePicker. It uses the Calendar.current.compare function as implemented by #burnsi. I also added a reset button to reset the filter and get access to the complete list of books.
import SwiftUI
extension Date {
static func from(year: Int, month: Int, day: Int) -> Date {
let calendar = Calendar.current
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
return calendar.date(from: dateComponents) ?? Date()
}
}
struct Book: Identifiable {
let id = UUID()
let title: String
let datePublished: Date
}
struct ContentView: View {
#State private var selectedDate: Date = Date()
#State private var reset: Bool = false
#State private var books = [
Book(title: "Introduction to JavaScript", datePublished: Date.from(year: 2022, month: 09, day: 10)),
Book(title: "Mastering Swift", datePublished: Date.from(year: 2022, month: 09, day: 11)),
Book(title: "Beginning SQL", datePublished: Date.from(year: 2022, month: 09, day: 12)),
Book(title: "Professional Git", datePublished: Date.from(year: 2022, month: 09, day: 11)),
]
#State private var filteredBooks: [Book] = []
var body: some View {
VStack(alignment: .leading) {
DatePicker("Search by date", selection: $selectedDate, displayedComponents: .date)
Button("Reset") {
reset = true
}
Spacer()
if filteredBooks.isEmpty && !reset {
Text("No books found.")
} else {
List(reset ? books: filteredBooks) { book in
VStack(alignment: .leading) {
Text(book.title)
Text(book.datePublished, style: .date)
}
}
}
Spacer()
}.onChange(of: selectedDate) { value in
reset = false
// filter out the books
filteredBooks = books.filter { Calendar.current.compare($0.datePublished, to: selectedDate, toGranularity: .day) == .orderedSame }
}
.onAppear {
filteredBooks = books
}
.padding()
}
}

Related

Check Same Dates Inside ForEach Swift

How can I check if item 1 date is equal to item 2 date inside the foreach. Can I check the same dates inside the predicate like below without using 2 foreach loops?
struct TimedView: View {
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
#State var releaseDate = Date()
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest var reminder: FetchedResults<CDReminder>
init(){
var calendar = Calendar.current
calendar.timeZone = NSTimeZone.local
let dateFrom = calendar.startOfDay(for: Date()) // eg. 2016-10-10 00:00:00
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
let predicate = NSPredicate(format : "date >= %# AND date == %#", dateFrom as CVarArg)
self._reminder = FetchRequest(
entity: CDReminder.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CDReminder.date, ascending: false)],
predicate: predicate)
}
var body: some View {
NavigationView {
VStack{
HStack{
Text("Zamanlanmis")
.font(.system(size: 40, weight: .bold, design: .rounded))
.foregroundColor(.red)
Spacer()
}
.padding(.leading)
List{
ForEach(reminder, id: \.self) { item in
DatedReminderCell(reminder: item, isSelected: false, onComplete: {})
.foregroundColor(.black)
}
}
}
}
}
Now my output is coming like this
Date objects are only equal if they represent the exact same instant, down to the fraction of a second.
If you want to see if a date falls on a give month/day/year, you will need to use a Calendar object to generate beginning_of_day and end_of_day dates and see if each date fall between them.
This thread includes several examples of that. Most of the code is in Objective-C but it should be pretty easy to translate the approach to Swift, if not the exact code.
To solve this complex problem I created a helper file called ScheduledViewHelper. Inside this helper, I have created a new array that has two elements(reminder and date). Inside this helper first I fetched all the reminders from core data then I compared all their date data and when I find a new date I append the date element inside the array. Finally, I sorted those dates and return [ReminderList] to be able to get this array from my ScheduledView.
My ScheduledViewHelper is
public struct ReminderList: Hashable {
let date: String
let reminder: [CDReminder]
}
open class ScheduledViewHelper: NSObject {
public static let shared = ScheduledViewHelper()
private let viewContext = PersistenceController.shared.container.viewContext
private override init() {
super.init()
}
private func getReminders() -> [CDReminder] {
let request: NSFetchRequest<CDReminder> = CDReminder.fetchRequest()
do {
let items = try viewContext.fetch(request) as [CDReminder]
return items
} catch {
print("Error occured: ", error)
}
return []
}
public func getScheduledData() -> [ReminderList] {
let calendar = Calendar.current
var list: [ReminderList] = []
var dateList: [String] = []
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
let reminders = getReminders()
reminders.forEach { (reminder) in
let date = dateFormatter.string(from: reminder.date!)
if !dateList.contains(date) && date >= dateFormatter.string(from: Date()) {
dateList.append(date)
}
}
dateList.forEach { (date) in
let dateFrom = calendar.startOfDay(for: dateFormatter.date(from: date)!) // eg. 2016-10-10 00:00:00
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
let predicate = NSPredicate(format: "date <= %# AND date >= %#", dateTo! as NSDate, dateFrom as NSDate)
let request: NSFetchRequest<CDReminder> = CDReminder.fetchRequest()
request.predicate = predicate
do {
let items = try viewContext.fetch(request) as [CDReminder]
let newItem = ReminderList(date: date, reminder: items)
list.append(newItem)
} catch {
print("Error occured: ", error)
}
}
return list.sorted {
$0.date < $1.date
}
}
private func saveContext() {
do {
try viewContext.save()
} catch {
print("Error occured: ", error)
}
}
}
I added to my ScheduledView those lines:
created a new var before init() statement
#State var remindersList : [ReminderList] = []
just under NavigationView I initialized remindersList with onAppear
.onAppear {
remindersList = ScheduledViewHelper.shared.getScheduledData()
}
Reorganized my List like this
List{
ForEach(remindersList, id: .self) { item in
VStack(alignment: .leading) {
Text(item.date)
ForEach(item.reminder, id: .self) { reminder in
DatedReminderCell(reminder: reminder, isSelected: false, onComplete: {})
}
}
.foregroundColor(.black)
}
}

Linking ObservableObject to DatePicker in SwiftUI

I am new to Swift and all its frameworks. I have a JSON file with readings for each day of the year. I have made a Decodable struct for the reading and an ObservableObject class which stores the readings in an array. I have made the ObservableObject an #EnvironmentObject so it can be accessed in all views. Can I link the readings to the date picker so that selecting a date will take me to a detailed view?
import SwiftUI
struct CalendarView: View {
// this is where ObserveableObject is required
#EnvironmentObject var days: Days
#State private var date = Date()
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "LLLL d"
return formatter
}()
var body: some View {
VStack {
Text("Select a date to read")
.font(.largeTitle)
DatePicker("Select a date", selection: $date, displayedComponents: .date)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.frame(maxHeight: 400)
Text("\(date, formatter: dateFormatter)")
}
.navigationTitle("Datepicker")
}
}
struct CalendarView_Previews: PreviewProvider {
static var previews: some View {
CalendarView()
}
}
//Replace this with your Decodable object
class Day : ObservableObject{
let id: UUID = UUID()
var date: Date
init( date: Date) {
self.date = date
}
}
class CalendarViewModel: ObservableObject, Identifiable {
#Published var days: [Day] = []
#Published var selectedDate: Date = Date()
var filteredDay: Day?{
days.filter({
Calendar.current.isDate($0.date, equalTo: selectedDate, toGranularity: .day)
}).first
}
init() {
//Create sample Days you can remove this and populate your array with acual data
for n in 1...30{
days.append(Day(date: Date().addingTimeInterval(TimeInterval(60*60*24*n))))
days.append(Day(date: Date().addingTimeInterval(TimeInterval(-60*60*24*n))))
}
}
}
struct CalendarView: View {
#StateObject var vm: CalendarViewModel = CalendarViewModel()
#State var isVisible: Bool = false
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "LLLL d"
return formatter
}()
var body: some View {
NavigationView{
ScrollView {
Text("Select a date to read")
.font(.largeTitle)
DatePicker("Select a date", selection: $vm.selectedDate, displayedComponents: .date)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.frame(maxHeight: 400)
Text("\(vm.selectedDate, formatter: dateFormatter)")
if vm.filteredDay != nil{
NavigationLink(
destination: DayView(day: vm.filteredDay!),
isActive: $isVisible,
label: {
Text("View Day")
})
}else{
Text("No results for selected day")
}
}
.navigationTitle("Datepicker")
}
}
}
struct DayView:View {
//Observe the actual Day here for changes
#ObservedObject var day: Day
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "LLLL d"
return formatter
}()
var body: some View {
VStack{
Text(dateFormatter.string(from: day.date))
}
}
}

How can I initialize SwiftUI DatePicker .hourAndMinute element?

I am trying to initialize this "WakeUpDate" date element so that the default displayed value is 10:00 AM. This date picker is HourandMinute only and is being stored in userdefaults.
I tried to init the date element but it is not building. With this error: Cannot assign value of type 'State<Date>' to type 'Published<Date>'
UserData: Currently, the following init does not build
import Foundation
import SwiftUI
class UserData: ObservableObject {
init() {
_wakeUpTime = State<Date>(initialValue: Calendar.current.date(DateComponents(Hour: 10)) ?? Date())
}
#Published var wakeUpTime: Date = UserDefaults.standard.object(forKey: "wakeUpTime") as? Date ?? Date() {
didSet {
UserDefaults.standard.set(self.wakeUpTime, forKey: "wakeUpTime")
}
}
}
SettingsDetail: Where the DatePicker is being selected:
struct SettingsDetailView: View {
#ObservedObject var userData: UserData
var body: some View {
Form{
DatePicker("Select a new time", selection: $userData.wakeUpTime, displayedComponents: .hourAndMinute)
}
}
}
MainSettings: Where the selected DatePicker Date is being displayed:
import SwiftUI
import UserNotifications
struct SettingsView: View {
#ObservedObject var userData = UserData()
static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm a"
return formatter
}()
var body: some View {
NavigationView {
VStack (alignment: .center, spacing: 0) {
Form{
Section(header: Text("NOTIFICATION SETTINGS")) {
HStack {
Text("Current Notification Time")
.foregroundColor(Color("MainText"))
Spacer()
Text("\(self.userData.wakeUpTime, formatter: SettingsView.self.dateFormatter)")
}
}
}
}
}
}
}
EDIT I tried initializing UserData like this, but now when I pick a new time with the date picker and quit the app, the new time is gone and 5PM (the init time) is displayed again.
import Foundation
import SwiftUI
class UserData: ObservableObject {
init() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
if let date = dateFormatter.date(from:"17:00") {
wakeUpTime = date
}
}
#Published var wakeUpTime: Date = UserDefaults.standard.object(forKey: "wakeUpTime") as? Date ?? Date() {
didSet {
UserDefaults.standard.set(self.wakeUpTime, forKey: "wakeUpTime")
}
}
}
How can I run init only on the first launch, and be removed once the selected time has been picked with the datepicker?
I figured it out by doing this:
class UserData: ObservableObject {
#AppStorage("userdatahaslaunched") var userdatahaslaunched = false
init() {
if !userdatahaslaunched {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
if let date = dateFormatter.date(from:"10:00") {
wakeUpTime = date
}
userdatahaslaunched = true
}
}
If your intent is to let the user initialize a wake up time you should always expect a date after now. So what you are looking for is calendar method nextDate(after:) and you can match the desired components (hour and minute). Note that you don't need to include the minutes component when calling this method if it is zero.
let date = Calendar.current.nextDate(after: Date(), matching: .init(hour: 10), matchingPolicy: .strict)!
print(date.description(with: .current))

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()
}

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.