UserDefaults state not refresh - ios

I use "UserDefaults" to store the date time of the user to reload the page. However, I found that it does not refresh even if the time passed
final class ModelData: ObservableObject {
#Published var lastLaunchDate: Date = UserDefaults.standard.value(forKey: "lastLaunch") as? Date ?? Date()
func getTimeStamp() -> Date {
let now_components = Calendar.current.dateComponents([.month, .day, .hour, .minute], from: Date())
let lastLaunch_components = Calendar.current.dateComponents([.month, .day, .hour, .minute], from: lastLaunchDate)
print(now_components.minute, lastLaunch_components.minute)
if now_components.minute != lastLaunch_components.minute {
lastLaunchDate = Date()
UserDefaults.standard.setValue(lastLaunchDate, forKey: "lastLaunch")
}
print(now_components.minute, lastLaunch_components.minute)
}
return lastLaunchDate
}
}
Since I am testing, I use 'minute' to ease my test. I expect that when there is change of minute, the above piece of code will update my userDefaults.
struct HomeView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
Text("\(modelData.getTimeStamp())")
}
}
I found that I browse into another page, go back to HomeView. The timestamp did not get update at all. I printed the lastLaunchDate, it also did not update as well.

I found that I browse into another page, go back to HomeView. The
timestamp did not get update at all.
If you navigate back to a parent View, SwiftUI does not re-render it unless something changes.
Instead... you will need to use the .onAppear view modifier to call your function.
Also, Text("\(modelData.getTimeStamp())") is not very SwiftUI especially in your case.
Since you have #Published var lastLaunchDate, use it as it's meant to be used:
Text("\(modelData.lastLaunchDate)")
Now when lastLaunchDate is changed, the SwiftUI engine will automatically update the View.
Solution:
final class ModelData: ObservableObject {
#Published var lastLaunchDate = UserDefaults.standard.value(forKey: "lastLaunch") as? Date ?? Date()
func refresh() {
let compareDate = Date()
let refreshInterval = TimeInterval(3) //3 second interval for test purpose
if compareDate.timeIntervalSince(lastLaunchDate) > refreshInterval {
lastLaunchDate = compareDate
UserDefaults.standard.setValue(lastLaunchDate, forKey: "lastLaunch")
}
}
}
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
NavigationView {
VStack {
Text("\(modelData.lastLaunchDate)")
NavigationLink(destination: Text("Destination"),
label: { Text("Navigate") })
}
.onAppear {
modelData.refresh()
}
}
}
}
NOTE:
if now_components.minute != lastLaunch_components.minute is error prone.
Example: compare last-update-time of 00:01 with current-time of 01:01.
Check will fail.

Related

Passing variable from view inside the class ObservableObject?

i got this struct
struct Calendar: View {
#State private var selectDate = Date()
var body: some View {
// my code
}
Class fetchmydate: ObservableObject {
func fetchmydata (){
// i want to pass selectDate to here in "dd-mm-yyyy" format as string
}
}
}
the idea i want to pass the selectdate from the view into observedobject class and not vice versa, but in this format "dd-mm-yyyy" and as a string. and if i don't pass this selected date it says it can't read the selected date inside the function.
i know it is somehow weird question but if it can't be done in logic (passing) then what do you suggest to pass the date data and thank you alot my friends.
What are you trying to achieve is not something unheard of, is just easier to pass the entire logic to the viewModel, I believe you are looking for this:
The view:
struct ViewDate: View {
#State private var date = Date()
var myviewModel = viewModel()
var body: some View {
DatePicker(selection: $date, displayedComponents: .date) {
Text("Select your date")
}
.datePickerStyle(.wheel)
.onChange(of: self.date) { newDate in
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let stringDate = dateFormatter.string(from: newDate)
myviewModel.fetchmydata(selectedDate: stringDate)
}
}
}
The "View Model"
class viewModel: ObservableObject {
init() {}
func fetchmydata (selectedDate: String) {
// i want to pass selectDate to here in "dd-mm-yyyy" format as string
print(selectedDate)
}
}

Detect when user taps out of or saves on DatePicker

I'm trying to fetch some data on a network call when a user selects a time from DatePicker.
I originally tested using .onChange but this fires every time that changes, and not on the final tap off.
DatePicker("Title", selection: $currentDate, displayedComponents: .hourAndMinute)
.onChange(of: $currentDate) { value in
print(value)
}
I also tried using the didset{} but that also fired on every change too.
#Published var currentDate: Date = Date() {
didSet { print(currentDate) }
}
What I want to do is once the user selects a time, I fire off some functions. I wanted to not do it every cycle in the wheel.
Is there a way to do this in SwiftUI or UIKit importing into SwiftUI?
Please see attached of what I'm looking at:
You could try rolling your own DatePickerView, such as this approach:
struct ContentView: View {
#State private var birthdate = Date()
var body: some View {
DatePickerView(date: $birthdate)
}
}
struct DatePickerView: View {
#Binding var date: Date
#State var hasChanged = false
var body: some View {
ZStack {
Color.white.opacity(0.01).ignoresSafeArea()
.onTapGesture {
if hasChanged {
print("---> do a network call now with: \(date)")
hasChanged = false
}
}
DatePicker("Birth Date", selection: $date, displayedComponents: .hourAndMinute)
.pickerStyle(.wheel)
.onChange(of: date) { val in
hasChanged = true
}
}
}
}

How to remove duplicate set elements in SwiftUI

I have an entity of places called Place which has a relationship (Set<Activity>) and I am passing the places (fetch request) to the PlacesView and each place has a list of Activities. What I try to accomplish is, that instead of showing all activities related to a place directly, I want to pre-group them in date elements in a ActivityDatesView. Now I want to select the date and see all activities related to that place and date. Date is an attribute of the Activity entity.
How can I modify my code, to not display activities, that have the same activity.date (without time), where activity.date is Date() value coming from a CoreData entity and Activity is a NSManagedObject Subclass.
List(Array(place.activities as Set), id: \.self) { activity in
Text(dateFormatter.string(from: activity.date))
}
For Views, I had:
struct PlacesView {
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Place.name, ascending: true)]) var places: FetchedResults<Foo>
var body: some View {
//...
}
}
struct ActivityDatesView {
#ObservedObject var place: Place
var body: some View {
List(Array(place.activities as Set), id: \.self) { activity in
Text(dateFormatter.string(from: activity.date)) // This displays duplicate entries
}
}
}
struct ActivityView {
#ObservedObject var place: Place
var selectedDate: Date
var body: some View {
//...
}
}
Because to get the features of Set() your struct must be Hashable and Equatable if you customize this protocols and override the default provided functions you get the desired functionality with much less code. The best is all your dates save their time and you dont lose any information.
struct ContentView: View {
let activities = [Activity(date: Date()), Activity(date: Date().addingTimeInterval(3600)), Activity(date: Date().addingTimeInterval(24*3600))]
var body: some View {
NavigationView {
VStack {
List(Array(Set(activities))) { activity in
//prints 2 dates
Text(activity.date.description)
}
}
}
}
}
struct Activity: Equatable, Identifiable, Hashable {
var id = UUID()
var name = "Activity"
var date: Date
//Add this function to your struct
static func == (lhs: Activity, rhs: Activity) -> Bool {
Calendar.current.dateComponents([.year, .month, .day], from: lhs.date) == Calendar.current.dateComponents([.year, .month, .day], from: rhs.date)
}
//Add this function also to your struct
func hash(into hasher: inout Hasher) {
hasher.combine(Calendar.current.dateComponents([.year, .month, .day], from: date))
}
}

Start and End date of month in SwiftUI

How to get startDateOfMonth and endDateOfMonth based on selected date in SwiftUI?
I have found some answers for Swift (DateComponents), but couldn't make it work with SwiftUI.
Why I need this: I am going to use dynamic filters using predicate to filter all the data in the currently selected month (using custom control to switch months). But first I need to get the start and end dates per selected month.
EXAMPLE code:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var currentDate = Date()
// How to make startDateOfMonth and endDateOfMonth dependent on selected month?
#State private var startDateOfMonth = "1st January"
#State private var endDateOfMonth = "31st January"
var body: some View {
VStack {
DateView(date: $currentDate)
Text("\(currentDate)")
Text(startDateOfMonth)
Text(endDateOfMonth)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DateView.swift
import SwiftUI
struct DateView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
#Binding var date : Date
var body: some View {
HStack {
Image(systemName: "chevron.left")
.padding()
.onTapGesture {
print("Month -1")
self.changeDateBy(-1)
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Image(systemName: "chevron.right")
.padding()
.onTapGesture {
print("Month +1")
self.changeDateBy(1)
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.background(Color.yellow)
}
func changeDateBy(_ months: Int) {
if let date = Calendar.current.date(byAdding: .month, value: months, to: date) {
self.date = date
}
}
}
struct DateView_Previews: PreviewProvider {
struct BindingTestHolder: View {
#State var testItem: Date = Date()
var body: some View {
DateView(date: $testItem)
}
}
static var previews: some View {
BindingTestHolder()
}
}
I managed to solve it by the following implementation of ContentView
#State var currentDate = Date()
private var startDateOfMonth: String {
let components = Calendar.current.dateComponents([.year, .month], from: currentDate)
let startOfMonth = Calendar.current.date(from: components)!
return format(date: startOfMonth)
}
private var endDateOfMonth: String {
var components = Calendar.current.dateComponents([.year, .month], from: currentDate)
components.month = (components.month ?? 0) + 1
components.hour = (components.hour ?? 0) - 1
let endOfMonth = Calendar.current.date(from: components)!
return format(date: endOfMonth)
}
var body: some View {
VStack {
DateView(date: $currentDate)
Text("\(currentDate)")
Text(startDateOfMonth)
Text(endDateOfMonth)
}
}
private func format(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
return dateFormatter.string(from: date)
}
Because currentDate is changed by DateView through Binding the body computed property will be invoked thus startDateOfMonth and endDateOfMonth computed properties will return the updated values.

How to properly group a list fetched from CoreData by date?

For the sake of simplicity lets assume I want to create a simple todo app. I have an entity Todo in my xcdatamodeld with the properties id, title and date, and the following swiftui view (example pictured):
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>
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
var body: some View {
VStack {
List {
ForEach(todos, id: \.self) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
}
}
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
}
}
The todos are sorted by date upon fetching, and are displayed in a list like this:
I want to group the list based on each todos respective date as such (mockup):
From my understanding this could work with Dictionaries in the init() function, however I couldn't come up with anything remotely useful. Is there an efficient way to group data?
Update for iOS 15
SwiftUI now has built-in support for Sectioned Fetch Requests in a List via the #SectionedFetchRequest property wrapper. This wrapper reduces the amount of boilerplate required to group Core Data lists.
Example code
#Environment(\.managedObjectContext) var moc
#State private var date = Date()
#SectionedFetchRequest( // Here we use SectionedFetchRequest
entity: Todo.entity(),
sectionIdentifier: \.dateString // Add this line
sortDescriptors: [
SortDescriptor(\.date, order: .forward)
]
) var todos: SectionedFetchResults<Todo>
var body: some View {
VStack {
List {
ForEach(todos) { (section: [Todo]) in
Section(section[0].dateString!))) {
ForEach(section) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatted: todo.dateFormatter)")
}
}
}
}.id(todos.count)
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
The Todo class can also be refactored to contain the logic for getting the date string. As a bonus, we can also use the .formatted beta method on Date to produce the relevant String.
struct Todo {
...
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}()
var dateString: String? {
formatter.string(from: date)
}
}
You may try the following, It should work in your situation.
#Environment(\.managedObjectContext) var moc
#State private var date = Date()
#FetchRequest(
entity: Todo.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Todo.date, ascending: true)
]
) var todos: FetchedResults<Todo>
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
func update(_ result : FetchedResults<Todo>)-> [[Todo]]{
return Dictionary(grouping: result){ (element : Todo) in
dateFormatter.string(from: element.date!)
}.values.map{$0}
}
var body: some View {
VStack {
List {
ForEach(update(todos), id: \.self) { (section: [Todo]) in
Section(header: Text( self.dateFormatter.string(from: section[0].date!))) {
ForEach(section, id: \.self) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
}
}
}
}.id(todos.count)
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
}
To divide SwiftUI List backed by Core Data into sections, you can change your data model to support grouping. In this particular case, this can be achieved by introducing TodoSection entity to your managed object model. The entity would have a date attribute for sorting sections and a unique name string attribute that would serve as a section id, as well as a section header name. The unique quality can be enforced by using Core Data unique constraints on your managed object. Todos in each section can be modeled as a to many relationship to your Todo entity.
When saving a new Todo object, you would have to use Find or Create pattern to find out whether you already have a section in store or you would have to create a new one.
let sectionName = dateFormatter.string(from: date)
let sectionFetch: NSFetchRequest<TodoSection> = TodoSection.fetchRequest()
sectionFetch.predicate = NSPredicate(format: "%K == %#", #keyPath(TodoSection.name), sectionName)
let results = try! moc.fetch(sectionFetch)
if results.isEmpty {
// Section not found, create new section.
let newSection = TodoSection(context: moc)
newSection.name = sectionName
newSection.date = date
newSection.addToTodos(newTodo)
} else {
// Section found, use it.
let existingSection = results.first!
existingSection.addToTodos(newTodo)
}
To display your sections and accompanying todos nested ForEach views can be used with Section in between. Core Data uses NSSet? for to many relationships so you would have to use an array proxy and conform Todo to Comparable for everything to work with SwiftUI.
extension TodoSection {
var todosArrayProxy: [Todo] {
(todos as? Set<Todo> ?? []).sorted()
}
}
extension Todo: Comparable {
public static func < (lhs: Todo, rhs: Todo) -> Bool {
lhs.title! < rhs.title!
}
}
If you need to delete a certain todo, bear in mind that the last removed todo in section should also delete the entire section object.
I tried using init(grouping:by:) on Dictionary, as it has been suggested here, and, in my case, it causes jaggy animations, which are probably the sign that we are going in the wrong direction. I’m guessing the whole list of items has to be recompiled when we delete a single item. Furthermore, embedding grouping into a data model would be more performant and future-proof as our data set grows.
I have provided a sample project if you need any further reference.

Resources