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

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

Related

Setup UserDefaults property as Published property in View Model [duplicate]

I have an #ObservedObject in my View:
struct HomeView: View {
#ObservedObject var station = Station()
var body: some View {
Text(self.station.status)
}
which updates text based on a String from Station.status:
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.string(forKey: "status") ?? "OFFLINE" {
didSet {
UserDefaults.standard.set(status, forKey: "status")
}
}
However, I need to change the value of status in my AppDelegate, because that is where I receive my Firebase Cloud Messages:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// Print full message.
let rawType = userInfo["type"]
// CHANGE VALUE OF status HERE
}
But if I change the status UserDefaults value in AppDelegate - it won't update in my view.
How can my #ObservedObjectin my view be notified when status changes?
EDIT: Forgot to mention that the 2.0 beta version of SwiftUI is used in the said example.
Here is possible solution
import Combine
// define key for observing
extension UserDefaults {
#objc dynamic var status: String {
get { string(forKey: "status") ?? "OFFLINE" }
set { setValue(newValue, forKey: "status") }
}
}
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.status {
didSet {
UserDefaults.standard.status = status
}
}
private var cancelable: AnyCancellable?
init() {
cancelable = UserDefaults.standard.publisher(for: \.status)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.status { // avoid cycling !!
self.status = newValue
}
})
}
}
Note: SwiftUI 2.0 allows you to use/observe UserDefaults in view directly via AppStorage, so if you need that status only in view, you can just use
struct SomeView: View {
#AppStorage("status") var status: String = "OFFLINE"
...
I would suggest you to use environment object instead or a combination of both of them if required. Environment objects are basically a global state objects. Thus if you change a published property of your environment object it will reflect your view. To set it up you need to pass the object to your initial view through SceneDelegate and you can work with the state in your whole view hierarchy. This is also the way to pass data across very distant sibling views (or if you have more complex scenario).
Simple Example
In your SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView().environmentObject(GlobalState())
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
The global state should conform ObservableObject. You should put your global variables in there as #Published.
class GlobalState: ObservableObject {
#Published var isLoggedIn: Bool
init(isLoggedIn : Bool) {
self.isLoggedIn = isLoggedIn
}
}
Example of how you publish a variable, not relevant to the already shown example in SceneDelegate
This is then how you can work with your global state inside your view. You need to inject it with the #EnvironmentObject wrapper like this:
struct ContentView: View {
#EnvironmentObject var globalState: GlobalState
var body: some View {
Text("Hello World")
}
}
Now in your case you want to also work with the state in AppDelegate. In order to do this I would suggest you safe the global state variable in your AppDelegate and access it from there in your SceneDelegate before passing to the initial view. To achieve this you should add the following in your AppDelegate:
var globalState : GlobalState!
static func shared() -> AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
Now you can go back to your SceneDelegate and do the following instead of initialising GlobalState directly:
let contentView = ContentView().environmentObject(AppDelegate.shared().globalState)

Core Data with SwiftUI MVVM Feedback

I am looking for a way to use CoreData Objects using MVVM (ditching #FetchRequest). After experimenting, I have arrived at the following implementation:
Package URL: https://github.com/TimmysApp/DataStruct
Datable.swift:
protocol Datable {
associatedtype Object: NSManagedObject
//MARK: - Mapping
static func map(from object: Object) -> Self
func map(from object: Object) -> Self
//MARK: - Entity
var object: Object {get}
//MARK: - Fetching
static var modelData: ModelData<Self> {get}
//MARK: - Writing
func save()
}
extension Datable {
static var modelData: ModelData<Self> {
return ModelData()
}
func map(from object: Object) -> Self {
return Self.map(from: object)
}
func save() {
_ = object
let viewContext = PersistenceController.shared.container.viewContext
do {
try viewContext.save()
}catch {
print(String(describing: error))
}
}
}
extension Array {
func model<T: Datable>() -> [T] {
return self.map({T.map(from: $0 as! T.Object)})
}
}
ModelData.swift:
class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var publishedData = CurrentValueSubject<[T], Never>([])
private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
override init() {
let fetchRequest = T.Object.fetchRequest()
fetchRequest.sortDescriptors = []
fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchController.delegate = self
do {
try fetchController.performFetch()
publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
}catch {
print(String(describing: error))
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let data = controller.fetchedObjects as? [T.Object] else {return}
self.publishedData.value = data.model()
}
}
Attempt.swift:
struct Attempt: Identifiable, Hashable {
var id: UUID?
var password: String
var timestamp: Date
var image: Data?
}
//MARK: - Datable
extension Attempt: Datable {
var object: AttemptData {
let viewContext = PersistenceController.shared.container.viewContext
let newAttemptData = AttemptData(context: viewContext)
newAttemptData.password = password
newAttemptData.timestamp = timestamp
newAttemptData.image = image
return newAttemptData
}
static func map(from object: AttemptData) -> Attempt {
return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
}
}
ViewModel.swift:
class HomeViewModel: BaseViewModel {
#Published var attempts = [Attempt]()
required init() {
super.init()
Attempt.modelData.publishedData.eraseToAnyPublisher()
.sink { [weak self] attempts in
self?.attempts = attempts
}.store(in: &cancellables)
}
}
So far this is working like a charm, however I wanted to check if this is the best way to do it, and improve it if possible. Please note that I have been using #FetchRequest with SwiftUI for over a year now and decided to move to MVVM since I am using it in all my Storyboard projects.
For a cutting edge way to wrap the NSFetchedResultsController in SwiftUI compatible code you might want to take a look at AsyncStream.
However, #FetchRequest currently is implemented as a DynamicProperty so if you did that too it would allow access the managed object context from the #Environment in the update func which is called on the DynamicProperty before body is called on the View. You can use an #StateObject internally as the FRC delegate.
Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.
And FYI, when using Combine's ObservableObject we don't sink the pipeline or use cancellables. Instead, assign the output of the pipeline to an #Published. However, if you aren't using CombineLatest, then perhaps reconsider if you should really be using Combine at all.

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

Strange behavior of #Binding variable in SwiftUI integrating UIKit (iOS app)

I am developing an App for my school's courses, displaying the descriptions of the courses as well as a calendar showing the appointments. I decided to use KVKCalendar as a library for the Calendar. Developing with SwiftUI and being the KVKCalendar library a UIKit environment, I had to interface the two systems (with the struct CalendarRepresentable, see code).
The app is constructed as follow: I load from the memory an #ObservedObject with the saved objects, pass it to some sub-views as #Binding in order to save/delete new courses from the course-catalogue.
All changes made in the views are updated accordingly in all the other views, but unfortunately not in the TimetableView, connected with the struct CalendarRepresentable.
The problem: I want to update the Calendar whenever new courses are saved (e.g. added to the array courses of the type [Courses]). At the moment the struct CalendarRepresentable is calling the makeUIView as expected but the calendar is not updated anymore. In particular the saved courses in courses are not constantly updated but in a (apparently) inconsitent way: in some functions of the struct CalendarRepresentable are indeed up to date, in some other functions (and subclasses) not. See code where I show where and in what case these inconsistencies occure.
Code of the View calling the UIKit-SwiftUI interface:
import SwiftUI
import KVKCalendar
struct TimetableView: View {
#Binding var courses: [Courses]
let saveAction: () -> Void
var body: some View {
CalendarRepresentable(courses: $courses)
}
}
struct TimetableView_Previews: PreviewProvider {
static var previews: some View {
TimetableView(courses: .constant(Courses.data), saveAction: {})
}
}
Code of the interface:
import SwiftUI
import KVKCalendar
import EventKit
struct CalendarRepresentable: UIViewRepresentable{
#Binding var courses: [Courses]
var events = [Event]()
var calendar: CalendarView = {
print("Representable has been launched")
var style = Style()
//style of the calendar
//...
return CalendarView(frame: UIScreen.main.bounds, style: style)
}()
func updateUIView(_ uiView: CalendarView, context: Context ){
print("updateUIView has been called. Courses has \(courses.count) elements") // **Here courses has always the correct amount of elements**
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
calendar.reloadInputViews()
print("updateUIView is finished")
}
public func passCourses() -> [Courses]{
print("Called passCourses with courses having \(courses.count) elements") //**Here courses has NOT the corrent amount of elements, i.e. after saving/deleting a course courses.count is different from what is printed in the previous function updateUIView**
return courses
}
func makeUIView(context: Context) -> CalendarView {
print("makeUIView has been called")
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
return calendar
}
func makeCoordinator() -> Coordinator {
print("makeCoordinator")
return Coordinator(self)
}
class Coordinator: NSObject, CalendarDelegate, CalendarDataSource {
var events = [Event]()
var parent: CalendarRepresentable
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
print("eventsForCalendar called. In Coordinator \(events.count) events and \(parent.courses.count) courses") //**here courses are again not uptodate to the CalendarRepresentable' courses**
loadEvents { (events) in
self.events = events
}
return self.events
}
init(_ parent: CalendarRepresentable){
print("Initialize Coordinator")
self.parent = parent
super.init()
loadEvents { (events) in
self.events = events
self.parent.calendar.reloadData()
}
}
func eventsForCalendar() -> [Event] {
print("eventsForCalendar without parameter")
return events
}
func loadEvents(completion: ([Event]) -> Void) {
var events = [Event]()
print("loadEvents with courses having \(self.parent.courses.count) elements")//**here courses are again not uptodate to the CalendarRepresentable' courses**
var i: Int=0
for course in self.parent.passCourses() {
let isoDateStart = course.startEvent
let dateStartFormatter = ISO8601DateFormatter()
let isoDateEnd = course.endEvent
let dateEndFormatter = ISO8601DateFormatter()
var event = Event(ID: "\(i)")
event.start = dateStartFormatter.date(from: isoDateStart)!
event.end = dateEndFormatter.date(from: isoDateEnd)!
event.text = course.name
events.append(event)
i=i+1
}
completion(events)
}
}
}
Example of console output after saving a course (done in another view) with (apparent) inconsistency:
Representable has been launched
updateUIView has been called. Courses has 4 elements
eventsForCalendar called. In Coordinator 3 events and 3 courses
loadEvents with courses having 3 elements
Called passCourses with courses having 3 elements
updateUIView is finished
Does anyone know what is happening to courses? Suggestions on how should I solve the issue?
Thanks a lot for your time and help!
After extensive testing I found that the problem was that somehow the coordinator had been called with an old parent although the outerstruct had been changed. I am not sure about the internal cause of that behavior, but anyway it should now be solved: the courses data is now successfully passed to the coordinator. However, the view still doesn't update itself despite a change in courses correctly triggering updateUIView. Any advice is appreciated.

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.