Store dictionary in UserDefaults - swiftui

This is a similar approach to Save dictionary to UserDefaults, however, it is intended for SwiftUI, not using a single line like set, so I want to store the value somewhere with a variable so I can call it easily. Also it's different because I'm asking for an initialization.
I have the following:
#Published var mealAndStatus: Dictionary
init() {
mealAndStatus = ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"]
if let storedDay = UserDefaults.standard.value(forKey: "mealAndStatus") {
mealAndStatus = storedDay as! Dictionary
}
}
1- How do I correctly store that dictionary in UserDefaults in SwiftUI?
2- That init, do I have to call it at the beginning of ContentView? Or can I leave it on the other swift file like that? Not sure how the init gets called.
I already made one with bool working:
#Published var startDay: Bool
init() {
startDay = true
if let storedDay = UserDefaults.standard.value(forKey: "startDay") {
startDay = storedDay as! Bool
}
}
but the dictionary doesn't seem to work. I need to initialize that dictionary and also store it in UserDefaults so I can access it later. Any help is appreciated.

This is the perfect solution I found for SwiftUI:
Store this somewhere, in my case I created a class just for UserDefaults:
#Published var mealAndStatus: [String: Date] =
UserDefaults.standard.dictionary(forKey: "mealAndStatus") as? [String: Date] ?? [:] {
didSet {
UserDefaults.standard.set(self.mealAndStatus, forKey: "mealAndStatus")
}
}
That above initializes the dictionary and also creates a variable to be easily called and use to update the value. This can be modified at lunch time and add new values, that way is initialized with whatever I want.
Furthermore, now on Apple Dev wwdc20 they announced a new way of handling UserDefaults with SwiftUI which may be even better than the above. The propery wrapper is called: #AppStorage.

Using JSONEncoder and JSONDecoder would help you convert to data any struct or dictionary that conforms to codable.
let arrayKey = "arrayKey"
func store(dictionary: [String: String], key: String) {
var data: Data?
let encoder = JSONEncoder()
do {
data = try encoder.encode(dictionary)
} catch {
print("failed to get data")
}
UserDefaults.standard.set(data, forKey: key)
}
func fetchDictionay(key: String) -> [String: String]? {
let decoder = JSONDecoder()
do {
if let storedData = UserDefaults.standard.data(forKey: key) {
let newArray = try decoder.decode([String: String].self, from: storedData)
print("new array: \(newArray)")
return newArray
}
} catch {
print("couldn't decode array: \(error)")
}
return nil
}
// You would put this where you want to save the dictionary
let mealAndStatus = ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"]
store(dictionary: mealAndStatus, key: arrayKey)
// You would put this where you want to access the dictionary
let savedDictionary = fetchDictionay(key: arrayKey)
On a side note, you probably shouldn't be using standard defaults for storing stuff like this. Storing it as a database, or saving it in a file especially with encryption on eith the database or the file might be a bit safer.

Related

Why does a pre-configured #FetchRequest not update the SwiftUI view?

Whenever I am using a pre-configured NSFetchRequest like so:
extension Note {
static var requestPrivateDBNotesByDate: NSFetchRequest<Note> {
let request = Note.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Note.createdDate, ascending: true)]
request.affectedStores = [PersistenceController.shared.privatePersistentStore]
return request
}
to do a #FetchRequest within a SwiftUI view:
#FetchRequest(fetchRequest: Note.requestPrivateDBNotesByDate)
private var notes: FetchedResults<Note>
the SwiftUI view is not updating when I add a Note entity to CoreData:
func addNote(name: String, context: NSManagedObjectContext) {
context.perform {
let note = Note(context: context)
note.displayName = name
note.createdDate = .now
try? context.save()
}
}
If I use a simple #FetchRequest within my SwiftUI view like so:
#FetchRequest(sortDescriptors: [SortDescriptor(\.displayName, order: .forward)]
) private var notes: FetchedResults<Note>
the view updates whenever I add a now Note.
Why is the pre-configured #FetchRequest not updating my SwiftUI view?
Note: I can force a view update by adding context.refresh(chat, mergeChanges: false) after context.save() but then my question would be, why do I need to force a refresh with a pre-configured #FetchRequest while it is not necessary with a simple #FetchRequest.
Is the forced refresh the only/correct way to go?
Am I missing something?
Update:
This is how I get the privatePersistentStore for the affectedStores property in the pre-configured NSFetchRequest.
var privatePersistentStore: NSPersistentStore {
var privateStore: NSPersistentStore?
let descriptions = persistentContainer.persistentStoreDescriptions
for description in descriptions {
if description.cloudKitContainerOptions?.databaseScope == .private {
guard let url = description.url else { fatalError("NO STORE URL!") }
guard let store = persistentContainer.persistentStoreCoordinator.persistentStore(for: url) else { fatalError("NO STORE!") }
privateStore = store
}
}
guard let privateStore else { fatalError("NO PRIVATE STORE!") }
return privateStore
}
you forgot to assign the new note to the store you are fetching from, e.g.
context.assign(to: PersistenceController.shared.privatePersistentStore)
try? context.save()

JSON data not copied to structure

This api is getting called correctly and is not falling into the "fetch failed" error line. The ContentView structure contains a standard menu with .onAppear at the bottom. Using the Xcode debugger I can see the data in decodedResponse but not in result. Is it really necessary to use #State results? Some of values in result / UserRates will be immediately pulled out and stored elsewhere. I would also like to use the retrieved date but currently my results definition keeps its non-defined. I'm assuming that to retrieve the data from the structure it is: UserRates.rates[9].key and UserRates.rates[9].value. Trying to read these with a print statement returns an error.
Galen Smith
struct UserRates: Codable {
let rates:[String: Double]
let base: String
let date: String
}
struct ContentView: View {
#State private var results = UserRates(rates: [:], base: "", date: "")
var body: some View {
... standard menu system in body
.onAppear(perform: loadCurrencies)
}
private func loadCurrencies() {
guard let url = URL(string: "https://api.exchangeratesapi.io/latest?base=USD") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(UserRates.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
**self.results = decodedResponse**
}
return
}
}
// problem if we fail into this
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
I had planned to copy 32 String / Double data pairs from decodedResponse to the UserRates structure then find and copy 11 exchange rates using the dictionary key / value method to a rateArray. But since the copy always fails, I decided to use the dictionary key / value method on the decodedResponse structure and copying directly to rateArray skipping the UserRate structure entirely. I may in the future use more exchange rates so that is why I load all available rates from my source. The real purpose of rateArray is to store the exchange rate reciprocals.
Galen

How do you use the result of a function as a Bindable object in Swiftui?

I'm developing a simple SwiftUI app, using Xcode 11 beta5.
I have a list of Place, and i want to display the list, and add / edit them.
The data come from core data.
I have 3 classes for this :
- CoreDataController, which handle the connection to core data
- PlaceController, which handle operation on the Places.
public class CoreDataController {
static let instance = CoreDataController()
private let container = NSPersistentContainer(name: "RememberV2")
private init() {
print("Start Init DataController")
container.loadPersistentStores { (storeDescription, error) in
if let error = error {
fatalError("Failed to load store: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
print("End Init DataController")
}
func getContext() -> NSManagedObjectContext {
return container.viewContext
}
func save() {
print("Start Save context")
do{
try container.viewContext.save()
} catch {
print("ERROR - saving context")
}
print("End Save context")
}
}
public class PlaceController {
static let instance = PlaceController()
private let dc = CoreDataController.instance
private let entityName:String = "Place"
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
func createPlace(name:String) -> Bool {
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
dc.save()
DataController.instance.places = getAllPlaces()
return true
}
func createPlace(name:String, comment:String) -> Bool {
print("Start - create place with comment")
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
newPlace.setValue(comment, forKey: "comment")
dc.save()
print("End - create place with comment")
DataController.instance.places = getAllPlaces()
return true
}
func getAllPlaces() -> [Place] {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
if let fp = try? dc.getContext().fetch(r) as? [Place] {
return fp
}
return [Place]()
}
func truncatePlaces() -> Bool {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let batch = NSBatchDeleteRequest(fetchRequest: r)
if (try? dc.getContext().execute(batch)) != nil {
return true
}
return false
}
}
In my view i simply use the function :
List (pc.getAllPlaces(), id: \.id) { place in
NavigationLink(destination: PlaceDetail(place: place)) {
PlacesRow(place:place)
}
}
It works to display the information, but if i add a new place, the list is not updated.
I have to go back to the home screen, then display again the Places screen for the list to be updated.
So i use another controller :
class DataController: ObservableObject {
#Published var places:[Place] = []
static let instance = DataController()
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
}
In my view, i just display the ObservedObject places.
#ObservedObject var data: DataController = DataController.instance
And in my PlaceController, i update the table in the DataController
DataController.instance.places = getAllPlaces()
That works, but i have this warning :
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes
Also i'm pretty sure there is a better way to do this ...
Any idea what is this better way ?
Thanks,
Nicolas

Getting nil response while passing string to another view controller - Swift3

In FirstViewController i'm fetching the response from JSON and want to pass that fetched response to another view controller.Below is the code which i have used so far for parsing and passing the response.
FirstViewController
var fn:String! //globally declared variable
code i have tried for parsing in FirstViewController
do {
let detailsDictionary:NSDictionary = try JSONSerialization.jsonObject(with: data!, options:.allowFragments) as! Dictionary<String, AnyObject> as NSDictionary
print(detailsDictionary)
let details = detailsDictionary["Data"] as! [[String:AnyObject]]
print(details)
for dtl in details
{
self.fn = dtl["Father_Name"] as? String ?? "NA"
print(self.fn) //here i'm getting the exact value from JSON
}
}
}
SecondViewController
In SecondViewController there is a Label called profile_name and want to set that parsed string(fn) as Label's text. for that i declared another variable as global.
var pname:String!
below is the code i have used to fetch the value from FirstViewController.
viewDidLoad()
{
let othervc = FirstViewController()
self.pname = othervc.fn
self.profile_name.text = self.pname
}
Problem : I tried my best efforts to get the desired output but i'm getting nil response.
Please Help.
In Second ViewController
let strName:String!
In First ViewController
let strOne = "This is for testing"
let objstory = self.storyboard?.instantiateViewController(withIdentifier: "yout Secoond ViewController Storybord ID") as! YourSecondViewControllerName
objstory.strNam = strOne
self.navigationController?.pushViewController(objstory, animated: true)
Your updated code just won't work.
let othervc = FirstViewController()
creates a new instance of FirstViewController (not the one that got the JSON).
You should be handling it something like this:
In FirstViewController
let fn = dtl["Father_Name"] as? String ?? "NA"
let svc = SecondViewController() // Or maybe instantiate from Storyboard, or maybe you already have a reference to it
svc.pname = fn
present(svc, animated: true, completion: nil)
Then in SecondViewController
override func viewDidLoad() {
super.viewDidLoad()
profile_name.text = pname
}
I'd suggest you take some time out and re-read Apple's View Controller programming guide.
Original Answer
The problem you have here…
vcvalue.profile_name.text = fn
is that profile_name is nil as the view for the view controller hasn't been loaded at this point.
You should handle this by creating a property in LeftSideMenuViewController
var name: String?
Then set
vcvalue.name = fn
And then in LeftSideMenuViewController
override func viewDidLoad() {
super.viewDidLoad()
profile_name.text = name
}
Also, some basic tips…
Don't force unwrap (!) apart from IBOutlets. You may have to write a bit more code, but you will reduce crashes.
Make #IBOutlets private - this will prevent you accidentally assigning to them as you are now
If you're overriding any viewWill/DidDis/Appear methods, you must call super at some point.
You need to re-read the section on switch/case
So this…
let a = indexPath.row
switch(a)
{
case 0 :
if(a == 0)
{
return 45
}
break
etc
could just be…
switch indexPath.row {
case 0...4:
return 45
case 5:
return 50
default:
break
}

Retrieving value from database

Hi I have a problem and would be grateful to any advice or answer.
func getUserProfileMDP(){
// set attributes to textField
var ref: DatabaseReference!
ref = Database.database().reference()
let user = Auth.auth().currentUser
print(user!.uid)
ref.child("users").child((user?.uid)!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
guard let value = snapshot.value as? [String: String] else { return }
print(value)
let passwordValue = value["password"]!as! String
print(passwordValue)
self.MDP = passwordValue // prints the right value from database
}){ (error) in
print(error.localizedDescription)
}
print(self.MDP) // prints the value innitialised in class(nope)
}
Here is the function that gets the value from database. It works (the first print gets the right value)
#IBAction func register(_ sender: Any) {
print(self.MDP)// prints the value innitialised in class(nope)
getUserProfileMDP()
print(self.MDP) // prints the value innitialised in class(nope)
let MDP = self.MDP
That is were I need the password to compare it. It doesn't get me the value of the database but the value initialized in class above:
var MDP = "nope"
Have a nice day
Given your last comment, I'd say that you're almost there, but here's an example. I did not fix the other parts of your code, I only added the completion handler in the method signature, and passed the password value to the handler, to show you how this works. The handler must be called inside the async closure.
func getUserProfileMDP(completion: #escaping (String)->()) {
// set attributes to textField
var ref: DatabaseReference!
ref = Database.database().reference()
let user = Auth.auth().currentUser
print(user!.uid)
ref.child("users").child((user?.uid)!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
guard let value = snapshot.value as? [String: String] else { return }
print(value)
let passwordValue = value["password"]!as! String
completion(passwordValue)
}){ (error) in
print(error.localizedDescription)
}
}
And you call it like that:
getUserProfileMDP() { pass in
print(pass)
self.MDP = pass
}