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")
}
Related
I'm new to Swift and I'm making an application to account expenses using Core Data and SwiftUI. The problem is that when I try to delete items from the List, it does not do so in the correct index. Probably because I applied a method that groups the items from the FetchRequest by date in a dictionary and displays them in the UI by Sections.
So, when I have rewritten the delete function so that it now takes the new indexes, I get an error that says: "Cannot convert value of type '[ExpenseLog]' to expected argument type 'NSManagedObject'", because I have previously made the change to the Dictionary.
Is there a way to be able to delete Core Data items by keeping the group by sections?
Because If I delete the ForEach that creates the Sections, the index is correct again and I can remove the correct item from Core Data. But that's not really what I want.
import SwiftUI
import CoreData
struct LogListView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: [SortDescriptor(\ExpenseLog.date, order: .reverse)]) var logs: FetchedResults<ExpenseLog>
#State var logToEdit: ExpenseLog?
#State var searchText = ""
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
var sections: [[ExpenseLog]] {
ExpenseLog.update(Array(logs), and: dateFormatter)
}
var body: some View {
List {
ForEach(sections, id: \.self) { (section: [ExpenseLog]) in
Section(header: Text(dateFormatter.string(from: section[0].date!))) {
ForEach(section.filter({
self.searchText.isEmpty ? true :
$0.name!.localizedStandardContains(self.searchText)
}), id: \.id) { log in
Button {
logToEdit = log
} label: {
HStack(spacing: 16) {
CategoryIconsView(category: log.categoryEnum)
VStack(alignment: .leading, spacing: 8) {
Text(log.nameText).font(.subheadline)
Text(log.dataText)
}
Spacer()
Text(log.typeTransaction != "expensive" ? log.amountText : "-\(log.amountText)").font(.subheadline).foregroundColor(log.typeTransaction == "income" ? .green : .primary)
}
.padding(.vertical, 4)
}
}
}
}
.onDelete(perform: deleteItem)
}
}
func deleteItem(at offsets: IndexSet) {
var sec = sections
sec.remove(atOffsets: offsets)
offsets.forEach { index in
let log = sec[index]
moc.delete(log) // Error: Cannot convert value of type '[ExpenseLog]' to expected argument type 'NSManagedObject'
try? moc.saveContext()
}
And here is the method that groups the FetchedResults by dates in the ExpensesLog extension.
static func update(_ result: [ExpenseLog], and dateFormatter: DateFormatter) -> [[ExpenseLog]] {
return Dictionary(grouping: result) { (element : ExpenseLog) in
dateFormatter.string(from: element.date!)
}.values.sorted() { $0[0].date! > $1[0].date! }
}
Thank you very much in advance.
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
}
}
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))
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) }
}