Im trying to make a simple app that when the "Add" button is pressed, it adds a new cell to the list with the title and the date it was created. Im creating this practice app using mvvm but I cant figure out how to use date and dateFormatter properly. The following code is what I have:
The model:
struct Model: Identifiable {
var id = UUID()
var title: String
var createdAt = Date()
}
The ViewModel:
class ViewModel: ObservableObject {
#Published var items = [Model]()
}
And the view:
struct ContentView: View {
static var DateFormatter: DateFormatter {
let formatter = self.DateFormatter
formatter.dateStyle = .long
return formatter
}
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
List {
ForEach(0 ..< viewModel.items.count, id: \.self) { index in
VStack {
Text(self.viewModel.items[index].title)
Text(self.viewModel.items[index].createdAt)
}
}
}
.navigationBarTitle("Practice")
.navigationBarItems(trailing: Button(action: makeNew) {
Text("Add")
})
}
}
func makeNew() {
withAnimation {
viewModel.items.append(Model(title: "New Item \(viewModel.items.count + 1)", createdAt: Date()))
}
}
}
First of all it is Swift naming convention to start your properties with a lowercase letter.
Second you need to fix your date formatter declaration:
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
Then you call your ContentView static property ContentView.dateFormatter.string(from: yourdate). In your case:
Text(ContentView.dateFormatter.string(from: self.viewModel.items[index].createdAt))
Related
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")
}
}
}
}
When the NavigationLink is pressed I want to create an object (with the time when it was pressed), add it to the savedObjects and pass then new object to the destination view.
How can I do this without changing the state while the view is updating?
struct ContentView: View {
#State private var savedObjects = [
MyObject(
id: 0,
date: Date()
),
MyObject(
id: 1,
date: Date()
),
MyObject(
id: 2,
date: Date()
),
MyObject(
id: 3,
date: Date()
)
]
var body: some View {
NavigationView {
List {
NavigationLink("Save new object and navigate to it", destination: DestinationView(object: MyObject(id: Int.random(in: 10...1000), date: Date())))
ForEach(savedObjects) { object in
NavigationLink("Navigate to object \(object.id)", destination: DestinationView(object: object))
}
}
}
}
}
class MyObject: ObservableObject, Identifiable {
var id: Int
var date: Date
init(id: Int, date: Date) {
self.id = id
self.date = date
}
}
struct DestinationView: View {
#ObservedObject var object: MyObject
var body: some View {
VStack {
Text("object \(object.id)")
Text("date: \(object.date.description)")
}
}
}
Here is a solution that refreshes state with a new Date when the link appears.
struct LinkWithPayloadView: View {
#State private var date = Date()
var body: some View {
NavigationView {
navigationLink(payload: "Cookies and Milk", date: date)
.onAppear(perform: {
date = Date()
})
}
}
func navigationLink(payload: String, date: Date) -> some View {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let payload = Payload(timestamp: date, inners: payload)
let vstack = VStack {
Text("\(payload.inners)")
Text("\(formatter.string(from: payload.timestamp))")
}
return NavigationLink(payload.inners, destination: vstack)
}
struct Payload {
let timestamp : Date
let inners : String
}
}
Credit #mallow for the idea
My question is why can't you make a separate destination view to save the object? Then you don't need to worry about saving it in the starting view.
struct DestinationView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var object: MyObject
init(id : Int, date : Date) {
let myObject = MyObject(context: managedObjectContext)
myObject.date = date
myObject.id = id
try! managedObjectContext.save()
self.object = myObject
}
var body: some View {
VStack {
Text("object \(object.id)")
Text("date: \(object.date.description)")
}
}
}
Then you can just reload the objects from coredata when the navigation closes.
I think programmatic navigation is the best solution in my case. So clicking a different button that creates and saves the object which in turn creates a new NavigationLink and then initiating the navigation immediately as explained here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-programmatic-navigation-in-swiftui
I think is very clear from this dummy example: if you remove the ForEach code row, magically, the propagation will flow and clock will tick, otherwise it will freeze once the detail view is presented.
class ModelView: ObservableObject {
#Published var clock = Date()
init() {
Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.print()
.assign(to: &$clock)
}
}
struct ContentView: View {
#StateObject var viewModel = ModelView()
var body: some View {
NavigationView {
List {
NavigationLink("Clock", destination: Text(viewModel.clock, formatter: formatter))
ForEach(0..<1) { _ in } // <- Remove this row and the clock will work
}
}
}
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
}
}
I'd say it is rather some kind of coincidence that it works in first case, because destination is (or can be) copied on navigation (that's probably happens in second case).
The correct approach would be to have separated standalone view for details with own observer. Parent view should not update child view property if there is no binding.
Tested and worked with Xcode 12.4 / iOS 14.4
struct ContentView: View {
#StateObject var viewModel = ModelView()
var body: some View {
NavigationView {
List {
NavigationLink("Clock", destination: DetailsView(viewModel: viewModel))
ForEach(0..<1) { _ in } // <- Remove this row and the clock will work
}
}
}
}
struct DetailsView: View {
#ObservedObject var viewModel: ModelView
var body: some View {
Text(viewModel.clock, formatter: formatter)
}
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
}
}
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")
}
I want to change the DatePicker's date view. I just want to get a month and year selection. I want to assign ObservedObject to a variable at each selection.
My Code:
#State private var date = Date()
var body: some View {
DatePicker("", selection: $date, in: Date()..., displayedComponents: .date)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
.onDisappear(){
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/yyyy"
self.cardData.validThru = dateFormatter.string(from: self.date)
}
}
As others have already commented You would need to implement an HStack with two Pickers:
struct ContentView: View {
#State var monthIndex: Int = 0
#State var yearIndex: Int = 0
let monthSymbols = Calendar.current.monthSymbols
let years = Array(Date().year..<Date().year+10)
var body: some View {
GeometryReader{ geometry in
HStack(spacing: 0) {
Picker(selection: self.$monthIndex.onChange(self.monthChanged), label: Text("")) {
ForEach(0..<self.monthSymbols.count) { index in
Text(self.monthSymbols[index])
}
}.frame(maxWidth: geometry.size.width / 2).clipped()
Picker(selection: self.$yearIndex.onChange(self.yearChanged), label: Text("")) {
ForEach(0..<self.years.count) { index in
Text(String(self.years[index]))
}
}.frame(maxWidth: geometry.size.width / 2).clipped()
}
}
}
func monthChanged(_ index: Int) {
print("\(years[yearIndex]), \(index+1)")
print("Month: \(monthSymbols[index])")
}
func yearChanged(_ index: Int) {
print("\(years[index]), \(monthIndex+1)")
print("Month: \(monthSymbols[monthIndex])")
}
}
You would need this helper from this post to monitor the Picker changes
extension Binding {
func onChange(_ completion: #escaping (Value) -> Void) -> Binding<Value> {
.init(get:{ self.wrappedValue }, set:{ self.wrappedValue = $0; completion($0) })
}
}
And this calendar helper
extension Date {
var year: Int { Calendar.current.component(.year, from: self) }
}