Combine these into a single var - swift

Apologies for the stupid question, I'm still really new to the Swift language.
Following up on this answer by #matt, I want to combine these two statements into a single var
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
I've thought maybe using a var with didSet {} like something along the lines of
var test: Array = UserDefaults.standard. { // ??
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(test), forKey: "songs")
}
}
But I can't think of where to go from here.
Thanks for the help in advance :))

The property should not be a stored property. It should be a computed property, with get and set accessors:
var songsFromUserDefaults: [Song]? {
get {
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
return try? PropertyListDecoder().decode(Array<Song>.self, from: data)
} else {
return nil
}
}
set {
if let val = newValue {
UserDefaults.standard.set(try? PropertyListEncoder().encode(val), forKey:"songs")
}
}
}
Notice that since the decoding can fail, the getter returns an optional. This forces the setter to accept an optional newValue, and I have decided to only update UserDefaults when the value is not nil. Another design is to use try! when decoding, or return an empty array when the decoding fails. This way the type of the property can be non-optional, and the nil-check in the setter can be removed.

While you can use computed properties like Sweeper suggested (+1), I might consider putting this logic in a property wrapper.
In SwiftUI you can use AppStorage. Or you can roll your own. Here is a simplified example:
#propertyWrapper public struct Saved<Value: Codable> {
private let key: String
public var wrappedValue: Value? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return (try? JSONDecoder().decode(Value.self, from: data))
}
set {
guard
let value = newValue,
let data = try? JSONEncoder().encode(value)
else {
UserDefaults.standard.removeObject(forKey: key)
return
}
UserDefaults.standard.set(data, forKey: key)
}
}
init(key: String) {
self.key = key
}
}
And then you can do things like:
#Saved(key: "username") var username: String?
Or
#Saved(key: "songs") var songs: [Song]?

Related

Fatal error when publishing UserDefaults with Combine

I try to observe my array of custom objects in my UserDefaults using a Combine publisher.
First my extension:
extension UserDefaults {
var ratedProducts: [Product] {
get {
guard let data = UserDefaults.standard.data(forKey: "ratedProducts") else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "ratedProducts")
}
}
}
Then in my View model, within my init() I do:
UserDefaults.standard
.publisher(for: \.ratedProducts)
.sink { ratedProducts in
self.ratedProducts = ratedProducts
}
.store(in: &subscriptions)
You can see that I basically want to update my #Published property ratedProducts in the sink call.
Now when I run it, I get:
Fatal error: Could not extract a String from KeyPath
Swift.ReferenceWritableKeyPath<__C.NSUserDefaults,
Swift.Array<RebuyImageRating.Product>>
I think I know that this is because in my extension the ratedProduct property is not marked as #objc, but I cant mark it as such because I need to store a custom type.
Anyone know what to do?
Thanks
As you found out you can not observe your custom types directly, but you could add a possibility to observe the data change and decode that data to your custom type in your View model:
extension UserDefaults{
// Make it private(set) so you cannot use this from the outside and set arbitary data by accident
#objc dynamic private(set) var observableRatedProductsData: Data? {
get {
UserDefaults.standard.data(forKey: "ratedProducts")
}
set { UserDefaults.standard.set(newValue, forKey: "ratedProducts") }
}
var ratedProducts: [Product]{
get{
guard let data = UserDefaults.standard.data(forKey: "ratedProducts") else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
} set{
// set the custom objects array through your observable data property.
observableRatedProductsData = try? PropertyListEncoder().encode(newValue)
}
}
}
and the observer in your init:
UserDefaults.standard.publisher(for: \.observableRatedProductsData)
.map{ data -> [Product] in
// check data and decode it
guard let data = data else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
}
.receive(on: RunLoop.main) // Needed if changes come from background
.assign(to: &$ratedProducts) // assign it directly

How can I implement a generic struct that manages key-value pairs for UserDefaults in Swift?

How would one implement a struct that manages UserDefaults mappings in Swift?
Right now I have some computed properties a, b, c, d of different types and corresponding keys that look like this:
enum UserDefaultsKeys {
a_key
b_key
...
}
var a: String {
get { UserDefaults.standard.string(forKey: UserDefaultsKeys.a_key.rawValue) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.a_key.rawValue) }
}
var b: Int {
get { UserDefaults.standard.integer(forKey: UserDefaultsKeys.b_key.rawValue) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.b_key.rawValue) }
}
...
What I would like to achieve instead is to implement a struct that has a key of type String and a generic type value.
The value get function should - depending on its type - chose wether to use UserDefaults.standard.string or UserDefaults.standard.integer so that I can just create a DefaultsVar with some key and everything else is managed automatically for me.
What I have so far is this:
struct DefaultsVar<T> {
let key: String
var value: T {
get {
switch self {
case is String: return UserDefaults.standard.string(forKey: key) as! T
case is Int: return UserDefaults.standard.integer(forKey: key) as! T
default: return UserDefaults.standard.float(forKey: key) as! T
}
}
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
I get the following error: "Cast from DefaultsVar to unrelated Type 'String' always fails.
I am completely new to swift (and relatively new to programming) and don't really understand how to implement this the right way - or if this is even an approach that would be considered a good practice. Could someone please shine some light on this?
Thank you in advance!
You can use a custom
Property wrapper:
#propertyWrapper
struct UserDefaultStorage<T: Codable> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(key: String, default: T, store: UserDefaults = .standard) {
self.key = key
self.defaultValue = `default`
self.userDefaults = store
}
var wrappedValue: T {
get {
guard let data = userDefaults.data(forKey: key) else {
return defaultValue
}
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: key)
}
}
}
This wrapper can store/restore any kind of codable into/from the user defaults.
Usage
#UserDefaultStorage(key: "myCustomKey", default: 0)
var myValue: Int
iOS 14
SwiftUI has a similar wrapper (only for iOS 14) called #AppStorage and it can be used as a state. The advantage of using this is that it can be used directly as a State. But it requires SwiftUI and it only works from the iOS 14.

How to store user data locally in SwiftUI

I try to accomplish having an observable object with a published value training. On every change it should save the custom struct to the user defaults. On every load (AppState init) it should load the data:
class AppState: ObservableObject {
var trainings: [Training] {
willSet {
if let encoded = try? JSONEncoder().encode(trainings) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "trainings")
}
objectWillChange.send()
}
}
init() {
self.trainings = []
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data {
if let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
self.trainings = loadedTraining
}
}
}
}
I know if this is best practice, but I want to save the data locally.
The code I wrote is not working and I can't figure out why.
I'm a beginner and I never stored data to a device.
Each time you call the init method the first line resets the value stored in UserDefaults and in-turn returns the empty array instead of the value that was previously stored. Try this modification to your init method to fix it:
init() {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
self.trainings = loadedTraining
} else {
self.trainings = []
}
}
Better Approach: A much better approach would to modify your trainings property to have a get and set instead of the current setup. Here is an example:
var trainings: [Training] {
set {
if let encoded = try? JSONEncoder().encode(newValue) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "trainings")
}
objectWillChange.send()
}
get {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
return loadedTraining
}
return []
}
}
Note: This can again be improved using Swift 5.1's #PropertyWrapper. Let me know in the comments if anyone wants me to include that as well in the answer.
Update: Here's the solution that makes it simpler to use UserDefaults using Swift's #PropertyWrapper as you have requested for:-
#propertyWrapper struct UserDefault<T: Codable> {
var key: String
var wrappedValue: T? {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data {
return try? JSONDecoder().decode(T.self, from: data)
}
return nil
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
class AppState: ObservableObject {
#UserDefault(key: "trainings") var trainings: [Training]?
#UserDefault(key: "anotherProperty") var anotherPropertyInUserDefault: AnotherType?
}

How to encode/decode a dictionary with Codable values for storage in UserDefaults?

I am trying to store a dictionary of company names (string) mapped to Company objects (from a struct Company) in iOS UserDefaults. I have created the Company struct and made it conform to Codable. I have one example a friend helped me with in my project where we created a class Account and stored it in UserDefaults by making a Defaults struct (will include example code). I have read in the swift docs that dictionaries conform to Codable and in order to stay Codable, must contain Codable objects. That is why I made struct Company conform to Codable.
I have created a struct for Company that conforms to Codable. I have tried using model code to create a new struct CompanyDefaults to handle the getting and setting of the Company dictionary from/to UserDefaults. I feel I have some beginner misconceptions about what needs to happen and about how it should be implemented (with good design in mind).
The dictionary I wish to store looks like [String:Company]
where company name will be String and a Company object for Company
I used conform to Codable as I did some research and it seemed like a newer method for completing similar tasks.
Company struct
struct Company: Codable {
var name:String?
var initials:String? = nil
var logoURL:URL? = nil
var brandColor:String? = nil // Change to UIColor
enum CodingKeys:String, CodingKey {
case name = "name"
case initials = "initials"
case logoURL = "logoURL"
case brandColor = "brandColor"
}
init(name:String?, initials:String?, logoURL:URL?, brandColor:String?) {
self.name = name
self.initials = initials
self.logoURL = logoURL
self.brandColor = brandColor
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
initials = try values.decode(String.self, forKey: .initials)
logoURL = try values.decode(URL.self, forKey: .logoURL)
brandColor = try values.decode(String.self, forKey: .brandColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(initials, forKey: .initials)
try container.encode(logoURL, forKey: .logoURL)
try container.encode(brandColor, forKey: .brandColor)
}
}
Defaults struct to control storage
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
let companies = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [String : Company] ?? [:]
return companies!
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: companies, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}
I should be able to reference the stored dictionary throughout my code like CompanyDefaults.companies.count
For reference, a friend helped me do a similar task for an array of Account classes stored in user defaults. The code that works perfectly for that is below. The reason I tried a different way is that I had a different data structure (dictionary) and made the decision to use structs.
class Account: NSObject, NSCoding {
let service: String
var username: String
var password: String
func encode(with aCoder: NSCoder) {
aCoder.encode(service)
aCoder.encode(username)
aCoder.encode(password)
}
required init?(coder aDecoder: NSCoder) {
guard let service = aDecoder.decodeObject() as? String,
var username = aDecoder.decodeObject() as? String,
var password = aDecoder.decodeObject() as? String else {
return nil
}
self.service = service
self.username = username
self.password = password
}
init(service: String, username: String, password: String) {
self.service = service
self.username = username
self.password = password
}
}
struct Defaults {
static private let accountsKey = "accountsKey"
static var accounts: [Account] = {
guard let data = UserDefaults.standard.data(forKey: accountsKey) else { return [] }
let accounts = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Account] ?? []
return accounts
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: accounts, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: accountsKey)
}
}
}
You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.
Your struct can be reduced to
struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}
That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.
To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}

How can I store a Swift enum value in NSUserDefaults

I have an enum like this:
enum Environment {
case Production
case Staging
case Dev
}
And I'd like to save an instance in NSUserDefaults like this:
func saveEnvironment(environment : Environment){
NSUserDefaults.standardUserDefaults().setObject(environment, forKey: kSavedEnvironmentDefaultsKey)
}
I understand that a Swift enum isn't an NSObject, and that makes it difficult to save, but I'm unsure what the best way is to convert it to something storable.
Using rawValue for the enum is one way of using types that can be stored in NSUserDefaults, define your enum to use a rawValue. Raw values can be strings, characters, or any of the integer or floating-point number types :
enum Environment: String {
case Production = "Prod"
case Staging = "Stg"
case Dev = "Dev"
}
You can also create an enum instance directly using the rawValue (which could come from NSUserDefaults) like:
let env = Environment(rawValue: "Dev")
You can extract the rawValue (String) from the enum object like this and then store it in NSUserDefaults if needed:
if let myEnv = env {
println(myEnv.rawValue)
}
func saveEnvironment(environment : Environment){
NSUserDefaults.standardUserDefaults().setObject(environment.rawValue, forKey: kSavedEnvironmentDefaultsKey)
}
If you would like to save/read data from UserDefaults and separate some logic, you can do it in following way (Swift 3):
enum Environment: String {
case Production
case Staging
case Dev
}
class UserDefaultsManager {
static let shared = UserDefaultsManager()
var environment: Environment? {
get {
guard let environment = UserDefaults.standard.value(forKey: kSavedEnvironmentDefaultsKey) as? String else {
return nil
}
return Environment(rawValue: environment)
}
set(environment) {
UserDefaults.standard.set(environment?.rawValue, forKey: kSavedEnvironmentDefaultsKey)
}
}
}
So saving data in UserDefaults will look this way:
UserDefaultsManager.shared.environment = Environment.Production
And reading data, saved in UserDefaults in this way:
if let environment = UserDefaultsManager.shared.environment {
//you can do some magic with this variable
} else {
print("environment data not saved in UserDefaults")
}
Using Codable protocol
Extent Environment enum that conforms to Codable protocol to encode and decode values as Data.
enum Environment: String, Codable {
case Production
case Staging
case Dev
}
A wrapper for UserDefaults:
struct UserDefaultsManager {
static var userDefaults: UserDefaults = .standard
static func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
userDefaults.set(encoded, forKey: forKey)
}
}
static func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = userDefaults.value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
Usage
// Set
let environment: Environment = .Production
UserDefaultsManager.set(environment, forKey: "environment")
// Get
let environment: Environment? = UserDefaultsManager.get(forKey: "environment")
Here is another alternative that can be be easily used with enums based on types (like String, Int etc) that can be stored by NSUserDefaults.
#propertyWrapper
struct StoredProperty<T: RawRepresentable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
guard let rawValue = UserDefaults.standard.object(forKey: key) as? T.RawValue, let value = T(rawValue: rawValue) else {
return defaultValue
}
return value
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: key)
}
}
}
Example usage:
enum Environment: String {
case Production
case Staging
case Dev
}
#StoredProperty("Environment", defaultValue: .Dev)
var storedProperty: Environment
Swift 5.1 You can create a generic property wrapper, using Codable to transform values in and out the UserDefaults
extension UserDefaults {
// let value: Value already set somewhere
// UserDefaults.standard.set(newValue, forKey: "foo")
//
func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
setValue(encoded, forKey: forKey)
}
}
// let value: Value? = UserDefaults.standard.get(forKey: "foo")
//
func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
#propertyWrapper
public struct UserDefaultsBacked<Value>: Equatable where Value: Equatable, Value: Codable {
let key: String
let defaultValue: Value
var storage: UserDefaults = .standard
public init(key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
}
// if the value is nil return defaultValue
// if the value empty return defaultValue
// otherwise return the value
//
public var wrappedValue: Value {
get {
let value: Value? = storage.get(forKey: key)
if let stringValue = value as? String, stringValue.isEmpty {
// for string values we want to equate nil with empty string as well
return defaultValue
}
return value ?? defaultValue
}
set {
storage.set(newValue, forKey: key)
storage.synchronize()
}
}
}
// use it
struct AppState: Equatable {
enum TabItem: String, Codable {
case home
case book
case trips
case account
}
var isAppReady = false
#UserDefaultsBacked(key: "selectedTab", defaultValue: TabItem.home)
var selectedTab
// default value will be TabItem.home
#UserDefaultsBacked(key: "selectedIndex", defaultValue: 33)
var selectedIndex
// default value will be 33
}
I am using like this type staging. Can you please try this it will help you.
enum Environment: String {
case Production = "Production URL"
case Testing = "Testing URl"
case Development = "Development URL"
}
//your button actions
// MARK: set Development api
#IBAction func didTapDevelopmentAction(_ sender: Any) {
let env = Environment.Development.rawValue
print(env)
UserDefaults.standard.set(env, forKey:Key.UserDefaults.stagingURL)
}
// MARK: set Production api
#IBAction func didTapProductionAction(_ sender: Any) {
let env = Environment.Production.rawValue
print(env)
UserDefaults.standard.set(env, forKey:Key.UserDefaults.stagingURL)
}
// MARK: set Testing api
#IBAction func didTapTestingAction(_ sender: Any) {
let env = Environment.Testing.rawValue
print(env)
UserDefaults.standard.set(env, forKey:Key.UserDefaults.stagingURL)
}
//Based on selection act
print("\(UserDefaults.standard.object(forKey: "stagingURL") ?? "")")
Swift 5.1
You can create property wrapper for this
#propertyWrapper final class UserDefaultsLanguageValue {
var defaultValue: LanguageType
var key: UserDefaultsKey
init(key: UserDefaultsKey, defaultValue: LanguageType) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: LanguageType {
get { LanguageType(rawValue: UserDefaults.standard.object(forKey: key.rawValue) as? String ?? defaultValue.rawValue) ?? .en }
set { UserDefaults.standard.set(newValue.rawValue, forKey: key.rawValue) }
}
}
enum UserDefaultsKey: String {
case language
}
enum LanguageType: String {
case en
case ar
}
And use it just like that
#UserDefaultsLanguageValue(key: .language, defaultValue: LanguageType.en) var language