How to read property from a struct when being in #ObservableObject - swift

I don't know how to read a property that is in a struct from a class that is an Observable Object.
Context:
I'm trying to build an app which consists of 2 views:
a custom calendar;
a popup with a header 'Daily Joke', date formatted as 'MM-dd-yyyy' and a joke text that is fetched from Firebase using id. When the user clicks on a date in the calendar, the popup appears and shows the joke for a selected date.
The problem is that the 'currentDate' property (holds the value of the selected date) that I reference in the ObservableObject of the 'getJoke' class won't update when the user selects a different date. It always fetches the joke on today's date and not on the one the user has selected.
Here is the code of:
the custom calendar (selected date is held in the property 'currentDate')
import SwiftUI
import grpc
struct CustomDatePicker: View {
#State var currentDate: Date
#State var dailyJokePopUp = false
//some code here
// When the user selects the date, currentDate property changes to the selected date
.onTapGesture {
currentDate = value.date
}
// Getting selected day for displaying in dailyJokePopUp
func getCurrentDay()->String{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let date = dateFormatter.string(from: currentDate)
return date
}
the class which is an #ObservableObject (I use it to add a listener to the Firebase to fetch the joke text by its id. Here I need to read 'currentDate' which is originally declared and changed in CustomDatePicker. I need to do it to check if 'currentDate' matches the id in Firebase (that way the joke text is fetched from Firebase on the selected date)).
class getJoke : ObservableObject {
#Published var data = [JokeX]()
#Published var noData = false
#Published var currentDate = Date()
//some code here including adding SnapShotListener
let callCDP = CustomDatePicker(currentDate: currentDate).getCurrentDay()
if id == callCDP {
self.data.append(joke_text_imported)}
}
}
}
}
the popup (I call the result of the #ObservableObject to get the display the text fetched from Firebase)
import SwiftUI
struct dailyJokePopUp: View {
#Binding var show: Bool
#ObservedObject var Jokes = getJoke()
var currentDate: Date = Date()
//some code here
ForEach(self.Jokes.data){i in
Text(i.joke_text)
}
//some code here
}
I can suspect something is wrong with how I declare properties. I've tried various wrappers (#Binding, #StateObject), but I got confused and it didn't work. Hope someone can be kind enough to help me solve the problem.

ViewModel
class getJoke: ObservableObject {
#Published var currentDate = Date()
}
View that can change passing data
struct CustomDatePicker: View {
#Binding var currentDate: Date
var body: some View{
VStack {
DatePicker(selection: $currentDate, displayedComponents: .date){
Text("Select your date")
}
.datePickerStyle(.compact)
}
}
}
And put everything together
struct ContentView: View {
#StateObject var vm = getJoke()
var body: some View {
VStack(spacing: 40) {
CustomDatePicker(currentDate: $vm.currentDate)
Button {
print(vm.currentDate)
} label: {
Text("Show selected date")
}
}
}
}

Related

SwiftUI Picker with unselected default and objects not working

I am using a Picker to select a Currency, which is an Identifiable Class based on a Core Data Entity. I thereby would want that there is no initial selection in the picker. However, somehow that is not working, and I can't select anything in the picker. My code looks like this (explorerViewModel.currencies is a Set of Currency):
#State private var selectedCurrency: Currency?
var body: some View {
Form {
Picker("Currency", selection: $selectedCurrency) {
ForEach(explorerViewModel.currencies) { currency in
Text(currency.name)
}
}
}
Any help what is wrong here?
In case required, that is the code for the explorerViewModel:
class ExplorerViewModel: ObservableObject {
private let moc: NSManagedObjectContext
#Published var currencies: [Currency]
init() {
self.moc = PersistenceController.shared.container.viewContext
currencies = (try? moc.fetch(NSFetchRequest<Currency>(entityName : "Currency"))) ?? []
}
}
I actuallt found the issue: I had to append a tag to make the currency optional.
Text(currency.name).tag(currency as Currency?)
worked.

How to check the date everyday in swiftUI

I am new to the swiftUI. Right now, I am making an app that takes down your task progress. In the app, I need to refill the list with goals of user have every day (I guess 12 AM), where and how do I check the time in swift? I know that we could use app delegate in storyboard, but for SwiftUI, after applying CoreData Manager, the app delegate has gone and we have app.swift instead, where should I do the checking now? Thank you!
Building off of Leo Dabus' suggestion to watch for NSCalendarDayChanged notifications here's some code showing how that can be done in SwiftUI.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text(viewModel.displayDate)
// List of goals
}
}
}
class ContentViewModel: ObservableObject {
#Published var currentDate: Date = Date()
var displayDate: String {
Self.simpleDateFormatter.string(from: currentDate)
}
private static let simpleDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d"
return formatter
}()
init() {
NotificationCenter.default.addObserver(self, selector: #selector(dayDidChange), name: .NSCalendarDayChanged, object: nil)
}
#objc
func dayDidChange() {
currentDate = Date()
}
}
You can use - (void)applicationSignificantTimeChange:(UIApplication *)application; in AppDelegate to monitor such changes.
You can also register for a notification in AppDelegate UIApplication.significantTimeChangeNotification
iOS will call both the registered notification method as well above delegate method.
NotificationCenter.default.addObserver(self, selector: #selector(timeChanged), name: UIApplication.significantTimeChangeNotification , object: nil)
#objc func timeChanged() {
print("App Time Changed")
}
In case you want to hook up with your SwiftUI directly, you can register your Swift view with your publisher.
Publisher will listen for notification name UIApplication.significantTimeChangeNotification.
Either of the ways can be used based on your requirement.
struct ContentView: View {
#State var dayDetails: String = "Hello World"
var body: some View {
Text(dayDetails)
.padding().onReceive(NotificationCenter.default.publisher(for: UIApplication.significantTimeChangeNotification), perform: { _ in
dayDetails = "Day has changed"
})
}
}
You can use the NSCalendarDayChanged notification to execute some code when the day changes.
struct ContentView: View {
#State var text: String = "Hello World"
var body: some View {
Text(text)
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.NSCalendarDayChanged)) { _ in
text = "Day has changed"
})
}
}

How can I use the #Environment(\.calendar) in a View to initialize a #State variable?

I have a calendar view like so:
struct CalendarView: View {
#Environment(\.calendar) var calendar
#State var daysForMonthView: [DateInterval]
...
}
where i need to initialize the daysForMonthView array by using the #Environment(\.calendar). How can I do that?
Trying to do:
init() {
_daysForMonthView = State(initialValue: calendar.daysForMonthViewContaining(date: Date()))
}
produces a Variable 'self.daysForMonthView' used before being initialized -error.
You need to assign all the properties before you can access the #Environment. For this reason you can't use calendar to initialise daysForMonthView.
A possible solution is to use onAppear:
struct CalendarView: View {
#Environment(\.calendar) private var calendar
#State private var daysForMonthView: [DateInterval] = [] // assign empty
var body: some View {
//...
.onAppear {
daysForMonthView = calendar...
}
}
}
If for some reason you need calendar to be available in the init, you can pass it as a parameter in init:
struct ContentView: View {
#Environment(\.calendar) private var calendar
var body: some View {
CalendarView(calendar: calendar)
}
}
struct CalendarView: View {
private let calendar: Calendar
#State private var daysForMonthView: [DateInterval]
init(calendar: Calendar) {
self.calendar = calendar
self._daysForMonthView = .init(initialValue: calendar...)
}
//...
}
Note: the downside of this approach is that a change to the calendar will reinitialise the whole CalendarView.

View parameter only passed to view when application first loads - SwiftUI

I have a navigation view (SettingsView) as shown below. When I go to PayScheduleForm the first time, the date from UserDefaults is passed properly. If I change the payDate value in PayScheduleForm, I can see that it updates the UserDefaults key properly. However, if I go back to SettingsView, and then go to PayScheduleForm again, the original value is still shown in the picker.
It's kind of an odd scenario so maybe it's better explained step by step:
Start App
Go to Settings -> Pay Schedule
Last UserDefaults payDate value is in DatePicker (10/08/2020)
Change value to 10/14/2020 - console shows that string of UserDefaults.standard.object(forKey: "payDate") = 10/14/2020
Go back to settings (using back button)
Go back to Pay Schedule and see that DatePicker has its original value (10/08/2020)
Of course if I restart the app again, I see 10/14/2020 in the DatePicker
struct SettingsView: View {
var body: some View {
NavigationView{
if #available(iOS 14.0, *) {
List{
NavigationLink(destination: AccountsList()) {
Text("Accounts")
}
NavigationLink(destination: CategoriesList()) {
Text("Categories")
}
NavigationLink(destination: PayScheduleForm(date: getPayDate()).onAppear(){
getPayDate()
}) {
Text("Pay Schedule")
}
}.navigationTitle("Settings")
} else {
// Fallback on earlier versions
}
}.onAppear(perform: {
getUserDefaults()
})
}
func getPayDate() -> Date{
var date = Date()
if UserDefaults.standard.object(forKey: "payDate") != nil {
date = UserDefaults.standard.object(forKey: "payDate") as! Date
}
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
print(df.string(from: date))
return date
}
struct PayScheduleForm: View {
var frequencies = ["Bi-Weekly"]
#Environment(\.managedObjectContext) var context
#State var payFrequency: String?
#State var date: Date
var nextPayDay: String{
let nextPaydate = (Calendar.current.date(byAdding: .day, value: 14, to: date ))
return Utils.dateFormatterMed.string(from: nextPaydate ?? Date() )
}
var body: some View {
Form{
Picker(selection: $payFrequency, label: Text("Pay Frequency")) {
if #available(iOS 14.0, *) {
ForEach(0 ..< frequencies.count) {
Text(self.frequencies[$0]).tag(payFrequency)
}.onChange(of: payFrequency, perform: { value in
UserDefaults.standard.set(payFrequency, forKey:"payFrequency")
print(UserDefaults.standard.string(forKey:"payFrequency")!)
})
} else {
// Fallback on earlier versions
}
}
if #available(iOS 14.0, *) {
DatePicker(selection: $date, displayedComponents: .date) {
Text("Last Payday")
}
.onChange(of: date, perform: { value in
UserDefaults.standard.set(date, forKey: "payDate")
let date = UserDefaults.standard.object(forKey: "payDate") as! Date
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
print(df.string(from: date))
})
} else {
// Fallback on earlier versions
}
}
```
Fixed this. Posting so others with the same issue can find the answer.
iOS 14 allows the #AppStorage property wrapper which easily allows access to UserDefaults, BUT the Date type is not allowed with #AppStorage as of now.
Instead you can use an #Observable object
First create a swift file called UserSettings.swift and create a class in there:
class UserSettings: ObservableObject {
#Published var date: Date {
didSet {
UserDefaults.standard.set(date, forKey: "payDate")
}
}
init() {
self.date = UserDefaults.standard.object(forKey: "payDate") as? Date ?? Date()
}
}
Then add an #ObservableObject to your view
#ObservedObject var userSettings = UserSettings()
...
DatePicker(selection: $userSettings.date, displayedComponents: .date) {
Text("Last Payday")
}

How to re-trigger function whenever a variable is updated

UPDATED
I'm pulling events from IOS calendar with EventKit. Working great.
Here's the class function that retrieves the events.
#ObservedObject var selectDate = datepicker() // does NOT update as expected.
func loadEvents(completion: #escaping (([EKEvent]?) -> Void)) {
requestAccess(onGranted: {
let startOfDay = Calendar.current.startOfDay(for: self.selectDate.selectedDate)
let endOfDay = Calendar.current.startOfDay(for: self.selectDate.selectedDate.advanced(by: TimeInterval.day))
let predicate = self.eventStore.predicateForEvents(withStart: startOfDay, end: endOfDay, calendars: Array(self.selectedCalendars ?? []))
let events = self.eventStore.events(matching: predicate)
completion(events)
print("loadEvents triggered for \(self.selectDate.selectedDate)") // This ALWAYS returns the current date
}) {
completion(nil)
}
}
I want to update the results based on a chosen date.
so I have a date picker assigned to a #ObservedObject var selectDate in my main view
#ObservedObject var selectDate = datepicker()
DatePicker(
selection: $selectDate.selectedDate,
in: dateClosedRange,
displayedComponents: [.date, .hourAndMinute], label: { Text("Is hidden label") })
}
And this class is acting as the middle man. The didSet method is triggering the function to run, confirmed by the print statement in the loadEvents func above.
class datepicker: ObservableObject {
#Published var selectedDate: Date = Date() {
didSet {
print("class datepicker date changed to \(selectedDate)") // This returns the changed dates correctly
EventsRepository.shared.loadAndUpdateEvents()
}
}
}
I've tried passing the new date like this too.
let startOfDay = Calendar.current.startOfDay(for: datepicker.init().selectedDate)
With the same persistent "current date" only result.
How can I pass the new selectedDate to the function whenever the selectDate.selectedDate from the datePicker is changed?
At the moment it doesn't update at all. Always returns events for the current day only.
The print statements contained in the above code in Debug return.
"class datepicker date changed to 2020-08-13 19:23:28 +0000" // YEP That's right. I used datePicker to select this date.
"loadEvents triggered for 2020-08-10 19:23:28 +0000" // NOT updated, ALWAYS shows current date/time as first run.
So it looks like #Published property is a constant and can not be changed once set. Is there an alternative?
How can I re-trigger the func loadEvents whenever the
selectDate.selectedDate from the datePicker is changed?
You can try using didSet:
class datepicker: ObservableObject{
#Published var selectedDate: Date = Date() {
didSet {
// loadEvents()
}
}
}
Tested with Xcode 11.6, iOS 13.6.
EDIT
It looks like you're using two instances of datepicker object. Make sure you're using the same instance in all relevant places.
This object should only be created once:
#ObservedObject var selectDate = datepicker()
Then passed to other places in init.
One way to do this, is to actually subscribe to your own publisher when you create your model object.
class DateManager: ObservableObject {
#Published var selectedDate: Date = .init()
private var bag: Set<AnyCancellable> = []
init() {
$selectedDate // $ prefix gets the projected value, a `Publisher` from `Published<>`
.receive(on: DispatchQueue.main)
.sink { [weak self] selectedDate in
self?.loadEvents(date: selectedDate)
}
.store(in: &bag)
}
}
This will subscribe to that publisher and trigger your API call when it changes. you can get fancy and add debounce or small delays if the user is rapidly changing dates, etc. you now have the full power of combine to control this data flow.