swift saving an array set <Int> to usderdefaults - swift

I'm trying to save an array set to user defaults as part of a class, but I get an error whenever I add to the array in a view. Everything else works when I change the variable (i.e., saved to userdefaults and available in the changed state when I restart the app). The issue seems to be in writing the data to the UserDefaults rather than initializing it or reading it. Thanks!
Thread 1: "Attempt to insert non-property list object {(\n 20\n)} for key savedFavs"
Here's the code
import Foundation
import Combine
import SwiftUI
class Preferences: ObservableObject {
#Published var chosenVersion: Int {
didSet {
UserDefaults.standard.set(self.chosenVersion, forKey: "savedVersion")
}
}
#Published var isPsalmsExpanded: Bool {
didSet {
UserDefaults.standard.set(self.isPsalmsExpanded, forKey: "savedExpanded")
}
}
#Published var showMT: Bool {
didSet {
UserDefaults.standard.set(self.showMT, forKey: "savedMT")
}
}
#Published var fontSize: Int {
didSet {
UserDefaults.standard.set(self.fontSize, forKey: "savedSize")
}
}
#Published var scrollPosition: Int = 0
#Published var preferredColor: ColorScheme {
didSet {
UserDefaults.standard.set(self.preferredColor, forKey: "savedColor")
}
}
#Published var favPsalms: Set<Int> {
didSet {
UserDefaults.standard.set (self.favPsalms, forKey: "savedFavs")
}
}
init() {
self.chosenVersion = UserDefaults.standard.object(forKey: "savedVersion") as? Int ?? 0
self.isPsalmsExpanded = UserDefaults.standard.object(forKey: "savedExpanded") as? Bool ?? false
self.showMT = UserDefaults.standard.object(forKey: "savedMT") as? Bool ?? false
self.fontSize = UserDefaults.standard.object(forKey: "savedSize") as? Int ?? 20
self.preferredColor = UserDefaults.standard.object(forKey: "savedColor") as? ColorScheme ?? .light
self.favPsalms = UserDefaults.standard.object(forKey: "savedFavs") as? Set<Int> ?? Set<Int>()
}
}

Related

Function with a parameter inside init() not updating onAppear{}

Attempting to call a function within init(). I have a function that takes a parameter which I call in the initializer. I set the parameter to a blank string at first until I pass in the value in another view when it appears.
My issue is the function isn't updating immediately when the view first appears. My objective is to just have the function run immediately once the view is generated and have the view update immediately (where I display in my view values from the function).
My guess is since I'm passing in the initial blank string during the init(), my function isn't firing with the updated variable. I don't want to set the #State to #Binding as I don't want to have to pass in a value everytime I call the observedObject. Any advice is greatly appreciated.
To summarize my issue, when I call the grabItems recipe when my view first appears, it doesn't initially get called with the correct parameter (it gets called with a blank string).
class testClass: ObservableObject {
#State var uidNonUser: String
#Published var itemsNonUser = [Item]() // items saved
init(){
self.uidNonUser = ""
grabItems(userUID: uidNonUser) // << grabs things
}
}
struct testStruct: View {
#ObservedObject var rm = testClass()
#State var userUID: String // << variable passing to grabItems
var body: some View {
Text(rm.itemsNonUser.count)
.onAppear{
rm.grabItems(userUID: userUID)
}
}
}
FYI - pasting my actual grabItems recipe below just as reference in case it helps understand the issue.
func grabItems(userUID: String){
//grab user thats not current user
FirebaseManager.shared.firestore
.collection("users")
.document(userUID) // << passed in from view
.collection("userRecipes")
.addSnapshotListener { (snapshot, err) in
guard let documents = snapshot?.documents else{
print("no documents present")
return
}
self.itemsNonUser = documents.map { (querySnapshot) -> RecipeItem in
let data = querySnapshot.data()
let recipeTitle = data ["recipeTitle"] as? String ?? ""
let recipePrepTime = data ["recipePrepTime"] as? String ?? ""
let recipeImage = data ["recipeImage"] as? String ?? ""
let createdAt = data ["createdAt"] as? String ?? ""
let ingredients = data ["ingredientItem"] as? [String: String] ?? ["": ""]
let directions = data ["directions"] as? [String] ?? [""]
let recipeID = data ["recipeID"] as? String ?? ""
let recipeCaloriesMacro = data ["recipeCaloriesMacro"] as? Int ?? 0
let recipeFatMacro = data ["recipeFatMacro"] as? Int ?? 0
let recipeCarbMacro = data ["recipeCarbMacro"] as? Int ?? 0
let recipeProteinMacro = data ["recipeProteinMacro"] as? Int ?? 0
let recipe = RecipeItem(id: recipeID, recipeTitle:recipeTitle , recipePrepTime: recipePrepTime, recipeImage: recipeImage, createdAt: createdAt, recipeCaloriesMacro: recipeCaloriesMacro, recipeFatMacro: recipeFatMacro, recipeCarbMacro:recipeCarbMacro, recipeProteinMacro: recipeProteinMacro, directions: directions, ingredientItem: ingredients)
return recipe
}
}
}
Try this example code to update the View when it first appears.
// for testing
struct RecipeItem {
var recipeTitle: String
// ...
}
class TestClass: ObservableObject {
#Published var itemsNonUser = [RecipeItem]() // <-- here
func grabItems(userUID: String){
//...
// for testing
itemsNonUser = [RecipeItem(recipeTitle: "banana cake with ID: \(userUID)")]
}
}
struct TestStruct: View {
#StateObject var rm = TestClass() // <-- here
#State var userUID: String
var body: some View {
VStack {
Text(rm.itemsNonUser.first?.recipeTitle ?? "no name")
Text("count: \(rm.itemsNonUser.count)") // <-- here
}
.onAppear{
rm.grabItems(userUID: userUID) // <-- here
}
}
}
struct ContentView: View {
let userUID = "123456"
var body: some View {
TestStruct(userUID: userUID) // <-- here
}
}
In my instance (and could have been specific for my particular case) I updated the grabItems recipe to return an Int since my view was trying to display the count.
I then just called it directly into the view like so and it worked:
Text(String(rm.grabItems(userUID: userUID))).bold() // << calling function directly in view

Firestore responding with "cannot find 'cards' in scope"

I followed this tutorial to get data from firestore and changed what i needed to correspond to my model but it keeps responding with "cannot find 'cards' in scope" and I'm not sure what i did wrong. (i think i got the mvvm labels right)
VIEW
import SwiftUI
struct TestingView: View {
#ObservedObject private var viewModel = CardViewModel()
var body: some View {
List(viewModel.cards) {
Text(cards.name)
}
.onAppear() {
self.viewModel.fetchData()
}
}
}
VIEW MODEL
import Foundation
import Firebase
class CardViewModel: ObservableObject {
#Published var cards = [Cards]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("cards").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.cards = documents.map { queryDocumentSnapshot -> Cards in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let pronoun = data["pronoun"] as? String ?? ""
let bio = data["bio"] as? String ?? ""
let profileURLString = data["profileURLString"] as? String ?? ""
let gradiantColor1 = data["gradiantColor1"] as? UInt ?? 0
let gradiantColor2 = data["gradiantColor2"] as? UInt ?? 0
let gradiantColor3 = data["gradiantColor3"] as? UInt ?? 0
return Cards(name: name, pronoun: pronoun, bio: bio, profileURLString: profileURLString, gradiantColor1: gradiantColor1, gradiantColor2: gradiantColor2, gradiantColor3: gradiantColor3)
}
}
}
}
MODEL
import Foundation
struct Cards: Identifiable {
var id = UUID().uuidString
var name: String
var pronoun: String
var bio: String
var profileURLString: String
var gradiantColor1: UInt
var gradiantColor2: UInt
var gradiantColor3: UInt
var profileURL: URL {
return URL(string: profileURLString)!
}
}
List will provide an element to its trailing closure -- see card in in my code. Then, you can access that specific card in your Text element.
var body: some View {
List(viewModel.cards) { card in //<-- Here
Text(card.name) //<-- Here
}
.onAppear() {
self.viewModel.fetchData()
}
}
}
I'd suggest that you might want to rename the struct Cards to struct Card since it is one card. Then, your array would be #Published var cards = [Card]() -- ie an array of Cards. From a naming perspective, this would make a lot more sense.

Swift ObservedObject 'self' used in property access 'userPreferences' before all stored properties are initialized

I'm having the following problem can you tell me where I'm wrong?
import EventKit
import ServiceManagement
private struct PreferencesKeys {
static let backgroundIsTransparent = "backgroundIsTransparent"
static let isDarkMode = "isDarkMode"
}
class UserPreferences: ObservableObject {
static let instance = UserPreferences()
private init() {
// This prevents others from using the default '()' initializer for this class.
}
private static let defaults = UserDefaults.standard
#Published var backgroundIsTransparent: Bool = {
guard UserDefaults.standard.object(forKey: PreferencesKeys.backgroundIsTransparent) != nil else {
return true
}
return UserDefaults.standard.bool(forKey: PreferencesKeys.backgroundIsTransparent)
}() {
didSet {
UserPreferences.defaults.set(backgroundIsTransparent, forKey: PreferencesKeys.backgroundIsTransparent)
}
}
#Published var isDarkMode: Bool = {
guard UserDefaults.standard.object(forKey: PreferencesKeys.isDarkMode) != nil else {
return true
}
return UserDefaults.standard.bool(forKey: PreferencesKeys.isDarkMode)
}() {
didSet {
UserPreferences.defaults.set(isDarkMode, forKey: PreferencesKeys.isDarkMode)
}
}
}
This is a common mistake.
You are going to modify popover before having initialized statusItem. This breaks the rules and this is what the error says.
First initialize statusItem then set the background color of popover.
I would initialize statusItem even
let statusItem = NSStatusbar.system.status...
The code (text) in the question is irrelevant.

I Have A UserDefault String Value That I Want To Convert To A Date In SwiftUI

I have created a class in SwiftUI that I use to save UserDefaults:
import Foundation
import SwiftUI
import Combine
class UserSettings: ObservableObject {
#Published var vin: String {
didSet {
UserDefaults.standard.set(vin, forKey: "VIN")
}
}
#Published var dateOfPurchase: String {
didSet {
UserDefaults.standard.set(dateOfPurchase, forKey: "Date of Purchase")
}
}
#Published var currentMiles: String {
didSet {
UserDefaults.standard.set(currentMiles, forKey: "Current Miles")
}
}
#Published var cell: String {
didSet {
UserDefaults.standard.set(cell, forKey: "Cell")
}
}
#Published var email: String {
didSet {
UserDefaults.standard.set(email, forKey: "Email")
}
}
#Published var tcIsOn: Bool {
didSet {
UserDefaults.standard.set(tcIsOn, forKey: "Toyota Care")
}
}
init() {
self.vin = (UserDefaults.standard.object(forKey: "VIN") ?? "") as! String
self.dateOfPurchase = (UserDefaults.standard.object(forKey: "Date of Purchase") ?? "") as! String
self.currentMiles = (UserDefaults.standard.object(forKey: "Current Miles") ?? "") as! String
self.cell = (UserDefaults.standard.object(forKey: "Cell") ?? "") as! String
self.email = (UserDefaults.standard.object(forKey: "Email") ?? "") as! String
self.tcIsOn = (UserDefaults.standard.object(forKey: "Toyota Care") ?? false) as! Bool
}
}
Basically what I want to do is pull my string value "dateOfPurchase" from UserDefaults convert it to a date format it "MM/dd/yyyy" and then display it in a Text() object. It would seem simple but SwiftUI is not cooperating. My other string values work fine but converting from string --> date is proving problematic. Any insights would be appreciated.

Error Using property Wrapper in class swift 5.1

I am using UserDefaults and Combine in SwiftUI.
My UserDefault.swift file:
import SwiftUI
struct UserDefault<T> {
let key: String
let defaultValue:T
var wrappedValue:T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
} set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
When using this struct in the following class as follows:
DataStore.swift file:
import SwiftUI
import Combine
final class DataStore : ObservableObject { //(1)
let didChange = PassthroughSubject<DataStore, Never>()
#UserDefault(key: "firstLaunch", defaultValue: true) //(2)
var firstLaunch:Bool{
didSet{
didChange.send(self)
}
}
}
In the above code, I am getting 2 errors:
(1):Class 'DataStore' has no initializers
(2):Generic struct 'UserDefault' cannot be used as an attribute
I think there is a change or depreciation in swift 5.1, but I am unable to find it.
Use something like this:
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md
You need to add the #propertyWrapper annotation to your UserDefault struct.
#propertyWrapper
struct UserDefault<T> {
...