How to re-trigger function whenever a variable is updated - swift

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.

Related

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

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

run when a view redraws in SwiftUI

I have a view in SwiftUI, and I would like it to both redraw and to run a closure whenever a variable in my model changes. I am using this closure to update a state var I am storing in the view, which should be the previous value of the variable in my model before it changes
The following code simulates my situation:
let viewModel = ViewModel()
struct someView: View {
#observedObject var viewModel: ViewModel = viewModel
#State var previousSomeValue: CGFloat = 0
var body: some View {
Text("\(viewModel.model.someValue)")
}
}
class ViewModel: ObservableObject {
#Published var model = Model()
}
struct model {
var someValue: CGFloat = 0
}
With this setup, if someValue ever changes, someView redraws, however, I am unable to fire a closure.
//Solutions I have tried:
The main one was to attach onChangeOf(_ (T)->Void) to my view. With .onChangeOf( viewModel.model.someValue ) { _ in //do something } I was able to fire a closure whenever it changed however, by the time it ran, viewModel.model.someValue had already updated to the newValue, and I wasnt able to capture the old one. I read in the documentation that this is by design and that you must capture the thing you want to store the old value of, but I (to my knowledge) am only able to capture self, viewModel, but not viewModel.model.someValue.
.onChangeOf( viewModel.model.someValue ) { [self] newValue in //do something } //works but doesnt capture the var
.onChangeOf( viewModel.model.someValue ) { [viewModel] newValue in //do something } //works but doesnt capture the var
.onChangeOf( viewModel.model.someValue ) { [viewModel.model.someValue] newValue in //do something } //does not compile ( Expected 'weak', 'unowned', or no specifier in capture list )
I have also tried creating a binding in the view such as Binding { gameView.model.someValue } set: { _ in } and having the onChange observer this instead, but even when I capture self, when the closure is called, the old and new values are identicial.
This seems like a common thing to do (detecting external changes and firing a closure), how should I go about it?
If I correctly understood your needs then you should do this not in view but in view model, like
class ViewModel: ObservableObject {
var onModelChanged: (_ old: Model, _ new: Model) -> Void
#Published var model = Model() {
didSet {
onModelChanged(oldValue, model)
}
}
init(onModelChanged: #escaping (_ old: Model, _ new: Model) -> Void = {_, _ in}) {
self.onModelChanged = onModelChanged
}
}
so instantiating ViewModel you can provide a callback to observe values changed in model and have old and new values, like
#StateObject var viewModel = ViewModel() {
print("Old value: \($0.someValue)")
print("New value: \($1.someValue)")
}

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

Userdefaults with published enum

try to save user setting, but UserDefaults is not working, Xcode 12.3, swiftui 2.0, when I am reload my app, my setting not updating for new value)
class PrayerTimeViewModel: ObservableObject {
#Published var lm = LocationManager()
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults.standard.set(method.params, forKey: "method")
self.getPrayerTime()
}
}
func getPrayerTime() {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: lm.location?.latitude ?? 0.0, longitude: lm.location?.longitude ?? 0.0)
var par = method.params
par.madhab = mashab
self.times = PrayerTimes(coordinates: coordinates, date: date, calculationParameters: par)
}
and view.. update with AppStorage
struct MethodView: View {
#ObservedObject var model: PrayerTimeViewModel
#Environment(\.presentationMode) var presentationMode
#AppStorage("method", store: UserDefaults(suiteName: "method")) var method: CalculationMethod = .dubai
var body: some View {
List(CalculationMethod.allCases, id: \.self) { item in
Button(action: {
self.model.objectWillChange.send()
self.presentationMode.wrappedValue.dismiss()
self.model.method = item
method = item
}) {
HStack {
Text("\(item.rawValue)")
if model.method == item {
Image(systemName: "checkmark")
.foregroundColor(.black)
}
}
}
}
}
}
You have two issues.
First, as I mentioned in my comment above that you are using two different suites for UserDefaults. This means that you are storing and retrieving from two different locations. Either use UserDefaults.standard or use the one with your chosen suite UserDefaults(suitName: "method") - you don't have to use a suite unless you plan on sharing your defaults with other extensions then it would be prudent to do so.
Secondly you are storing the wrong item in UserDefaults. You are storing a computed property params rather than the actual enum value. When you try to retrieve the value it fails as it is not getting what it expects and uses the default value that you have set.
Here is a simple example that shows what you could do. There is a simple enum that has a raw value (String) and conforms to Codable, it also has a computed property. This matches your enum.
I have added an initialiser to my ObservableObject. This serves the purpose to populate my published Place from UserDefaults when the Race object is constructed.
Then in my ContentView I update the place depending on a button press. This updates the UI and it updates the value in UserDefaults.
This should be enough for you to understand how it works.
enum Place: String, Codable {
case first
case second
case third
case notPlaced
var someComputedProperty: String {
"Value stored: \(self.rawValue)"
}
}
class Race: ObservableObject {
#Published var place: Place = .notPlaced {
didSet {
// Store the rawValue of the enum into UserDefaults
// We can store the actual enum but that requires more code
UserDefaults.standard.setValue(place.rawValue, forKey: "method")
// Using a custom suite
// UserDefaults(suiteName: "method").setValue(place.rawValue, forKey: "method")
}
}
init() {
// Load the value from UserDefaults if it exists
if let rawValue = UserDefaults.standard.string(forKey: "method") {
// We need to nil-coalesce here as this is a failable initializer
self.place = Place(rawValue: rawValue) ?? .notPlaced
}
// Using a custom suite
// if let rawValue = UserDefaults(suiteName: "method")?.string(forKey: "method") {
// self.place = Place(rawValue: rawValue) ?? .notPlaced
// }
}
}
struct ContentView: View {
#StateObject var race: Race = Race()
var body: some View {
VStack(spacing: 20) {
Text(race.place.someComputedProperty)
.padding(.bottom, 20)
Button("Set First") {
race.place = .first
}
Button("Set Second") {
race.place = .second
}
Button("Set Third") {
race.place = .third
}
}
}
}
Addendum:
Because the enum conforms to Codable it would be possible to use AppStorage to read and write the property. However, that won't update the value in your ObservableObject so they could easily get out of sync. It is best to have one place where you control a value. In this case your ObservableObject should be the source of truth, and all updates (reading and writing to UserDefaults) should take place through there.
You write in one UserDefaults domain but read from the different. Assuming your intention is to use suite only UserDefaults, you should change one in model, like
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults(suiteName: "method").set(method.params, forKey: "method")
self.getPrayerTime()
}
}
or if you want to use standard then just use AppStorage with default constructor, like
// use UserDefaults.standard by default
#AppStorage("method") var method: CalculationMethod = .dubai

Observation of NSUserDefaults does not work when using .init(suitename:)

If I use the default UserDefaults I can subscribe to changes via keypath to specific changes.
However, if I use a UserDefaults instance created with the init(suiteName:), no changes are delivered.
According to the MAC Os release notes, the problem was fixed a while ago. However, I cannot confirm this. Does anyone know the problem and possibly a solution for it?
In previous releases, KVO could only be used on the instance of NSUserDefaults returned by the +standardUserDefaults method. Additionally, changes from other processes (such as defaults(1), extensions, or other apps in an app group) were ignored by KVO. These limitations have both been corrected. Changes from other processes will be delivered asynchronously on the main queue, and ignore NSKeyValueObservingOptionPrior.
https://developer.apple.com/library/archive/releasenotes/Miscellaneous/RN-Foundation-OSX10.12/index.html
Code Example:
import UIKit
import Combine
import Foundation
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
let provider = CalendarProvider()
}
extension UserDefaults {
#objc dynamic var startOfTheWeek: Int {
return integer(forKey: "startOfTheWeek")
}
}
final class CalendarProvider: ObservableObject {
#Published var currentCalendar: Calendar = Calendar.current
private var observer: NSKeyValueObservation?
init() {
var calendar = Calendar(identifier: .gregorian)
calendar.firstWeekday = 1
self.currentCalendar = calendar
observer = UserDefaults(suiteName: "group.foo.com")?
.observe(\.startOfTheWeek, options: [.new , .initial],
changeHandler: { (defaults, change) in
DispatchQueue.main.async {
var calendar = Calendar(identifier: .gregorian)
calendar.firstWeekday = change.newValue ?? 1
self.currentCalendar = calendar
}
})
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
UserDefaults(suiteName: "group.foo.com")?.setValue(8, forKey: "startOfTheWeek")
})
}
deinit {
observer?.invalidate()
}
}