How to save an UIButton image using UserDefaults? - swift

I want to save an UIButton image inside UserDefaults and retrieve it with a key later on in my code. The UIImage comes from a tuple that is being used by an UIPickerView.
let defaults = UserDefaults.standard
#IBOutlet weak var topCurrencyPicker: UIPickerView!
var topPickerOptions = [
(symbol: String, name: String, sign: String, flag: UIImage)]()
func createTopAndBottomCurrency() {
let topUserIndex = topCurrencyPicker.selectedRow(inComponent: 0)
let topUserChoice = money.currencyISOCode[topUserIndex]
userCurrencyChoice = Currencies(top: topUserChoice, bottom: bottomUserChoice)
// Save user pickerViews Choice
defaults.set(topUserIndex,
forKey: ExchangePreferencesVC.topExPickerKey)
defaults.set(userCurrencyChoice.top.sign,
forKey: ExchangePreferencesVC.topExSignKey)
defaults.set(userCurrencyChoice.top.symbol,
forKey: ExchangePreferencesVC.topExSymbolKey)
}
Is it then possible to save this UIImage as easily as a String can be?

Swift 4.2
public final class PreferencesService {
private let userDefaults: UserDefaults
init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
var avatar: UIImage? {
get {
guard let filename = documentsDirectory?.appendingPathComponent(#function, isDirectory: false) else {
return nil
}
return UIImage(contentsOfFile: filename.path)
}
set {
guard let filename = documentsDirectory?.appendingPathComponent(#function, isDirectory: false) else {
return
}
guard let newImage = newValue else {
try? FileManager.default.removeItem(at: filename)
return
}
if let data = newImage.jpegData(compressionQuality: 1.0) {
try? data.write(to: filename)
}
}
}
private var documentsDirectory: URL? {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths.first
}
var coolDown: Bool {
get { return userDefaults.bool(forKey: #function) }
set { userDefaults.set(newValue, forKey: #function) }
}
var lastVisited: Date {
get { return userDefaults.object(forKey: #function) as? Date ?? Date() }
set { userDefaults.set(newValue, forKey: #function) }
}
}
Better to store image in Document Directory.
If needed to Store UserDefaults - you can store it like Data.
Usage
PreferencesService().avatar = button.image(for: .normal)
Bonus: Note how to store simply other types Bool, Date, String.

Related

NSKeyedUnarchiver seems not to be reading anything

I'm trying to write an array of objects using NSKeyedArchiver.
Here some parts from my code:
EventStore.swift - holding the event array:
class EventStore{
private var events: [EventItem] = [EventItem]()
static let sharedStore = EventStore()
private init() {
}
static func getEventFile() -> URL{
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let file = directory.appendingPathComponent("events.bin")
return file
}
func addEvent(withEvent event:EventItem){
events.append(event)
}
func getEvents()->[EventItem]{
return events
}
}
No the eventItem where I implemented NSCoding:
class EventItem: NSObject, NSCoding {
private var id:Int
private var timestamp:Int64
//Object initialization
init(withId id:Int,withTimestamp timestamp:Int64) {
self.id = id
self.timestamp = timestamp
}
required convenience init?(coder: NSCoder) {
//get value from stored key if exists
guard let id = coder.decodeObject(forKey: "id") as? Int,
let timestamp = coder.decodeObject(forKey: "timestamp") as? Int64
//exit init after decoding if a value is missing
else {
NSLog("Unable to decode event")
return nil
}
self.init(withId:id,withTimestamp:timestamp)
}
func getId()->Int{
return id
}
func getTimestamp()->Int64{
return timestamp
}
//encode values to keys
func encode(with aCoder: NSCoder) {
NSLog("Encoding event")
aCoder.encode(id, forKey: "id")
aCoder.encode(timestamp, forKey: "timestamp")
}
}
Finally when the user tape on a button I'm adding an event into the array and saving it:
var eventStore = EventStore.sharedStore
#IBAction func TakeAction() {
//generate new event
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
let newEvent = EventItem(withId: eventStore.eventCount(), withTimestamp: timestamp)
eventStore.addEvent(withEvent: newEvent)
saveEvents()
//refresh ui
updateTakeText()
}
func saveEvents(){
do{
let data = try NSKeyedArchiver.archivedData(withRootObject: eventStore.getEvents(), requiringSecureCoding: false)
NSLog("Data being written : \(data)")
try data.write(to: EventStore.getEventFile())
NSLog("Write events to file :\(EventStore.getEventFile())")
}catch{
NSLog(error.localizedDescription)
}
}
func loadEvents() {
do{
let data = try Data(contentsOf: EventStore.getEventFile())
NSLog("Data loaded from file path: \(data)")
//try get data else return empty array
let events = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [EventItem] ?? [EventItem]()
NSLog("Events retrived from file: \(events.count)")
eventStore.setEvents(withEvents:events)
}catch{
NSLog(error.localizedDescription)
}
}
I added a lot of debug and it seems that the encoding and file write are working fine but the decoding fail. It always get nil values.
Any clue?
Thanks in advance
When encoding Int values you have to decode them with coder.decodeInteger(forKey: "xxx")

nil Check in User Defaults (Swift 5)

I have a single image stored in UserDefaults, but until a user sets it, UserDefaults is empty. I need to check
If UserDefaults is empty and if it is, display an image.
Right now on a fresh app install until a user sets an image into UserDefaults, the app crashed.
My decode is working perfectly after I set an image in UserDefaults, just the code checking for nil is the problem.
I've only been learning swift for a month and I am stuck in this scenario.
here is my code:
//Image Decode
let Data = UserDefaults.standdard.object(forKey: "savedImage" as! NSData){
if (Data as Data?) != nil {
//display Image
dogImageView.image = UIImage(data: Data as Data)
}else {
dogImageView.image = Image Literal
}
}
Use If-let to safaely unwrap optional.
if let data = UserDefaults.standard.data(forKey: "savedImage") { // image is present
dogImageView.image = UIImage(data: data)
} else { // image is not present. set a default image
dogImageView.image = Image Literal
}
For swift 4.2, swift 5+
Write a class like:
import Foundation
public class PreferencesUtils {
private init() {
}
public static func setBoolData(boolValue: Bool, dataName: String) {
UserDefaults.standard.set(boolValue, forKey: dataName)
}
public static func getBoolData(dataName: String)-> Bool{
let defaults = UserDefaults.standard
if(defaults.value(forKey: dataName) != nil) {
return defaults.value(forKey: dataName)! as! Bool
} else {
return false
}
}
public static func saveStringData(data: String, dataName: String){
let preferences = UserDefaults.standard
preferences.set(data, forKey: dataName)
let didSave = preferences.synchronize()
if !didSave {
debugPrint("Not saved yet")
}
}
public static func getSavedStringData(dataName: String)-> String{
let defaults = UserDefaults.standard
if(defaults.value(forKey: dataName) != nil){
return defaults.value(forKey: dataName) as! String
} else {
return ""
}
}
public static func saveIntData(data : Int, dataName: String){
let preferences = UserDefaults.standard
preferences.set(data, forKey: dataName)
let didSave = preferences.synchronize()
if !didSave {
debugPrint("Not saved yet")
}
}
public static func getSavedIntData(dataName: String) -> Int {
let defaults = UserDefaults.standard
if(defaults.value(forKey: dataName) != nil){
return defaults.value(forKey: dataName) as! Int
}else{
return 0
}
}
}
Usage:
Save your data in preferences with line:
PreferencesUtils.saveStringData(data: "YOUR_DATA", dataName: "YOUR_KEY")
and for get this saved data :
PreferencesUtils.getSavedStringData(dataName: "YOUR_KEY")
Or check this library: Link

Get 'NSInvalidArgumentException' 'Attempt to insert non-property list object when saving class type array to UserDefaults [duplicate]

I have a simple object which conforms to the NSCoding protocol.
import Foundation
class JobCategory: NSObject, NSCoding {
var id: Int
var name: String
var URLString: String
init(id: Int, name: String, URLString: String) {
self.id = id
self.name = name
self.URLString = URLString
}
// MARK: - NSCoding
required init(coder aDecoder: NSCoder) {
id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id")
name = aDecoder.decodeObject(forKey: "name") as! String
URLString = aDecoder.decodeObject(forKey: "URLString") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(URLString, forKey: "URLString")
}
}
I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'
This is the code where I'm saving in UserDefaults.
enum UserDefaultsKeys: String {
case jobCategory
}
class ViewController: UIViewController {
#IBAction func didTapSaveButton(_ sender: UIButton) {
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let userDefaults = UserDefaults.standard
userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue)
userDefaults.synchronize()
}
}
I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?
You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.
struct JobCategory: Codable {
let id: Int
let name: String
}
// To store in UserDefaults
if let encoded = try? JSONEncoder().encode(category) {
UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue)
}
// Retrieve from UserDefaults
if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data,
let category = try? JSONDecoder().decode(JobCategory.self, from: data) {
print(category.name)
}
Old Answer
You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.
For Storing data in UserDefaults
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue)
For retrieving data from UserDefaults
let decoded = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data
let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory
print(decodedTeams.name)
Update Swift 4, Xcode 10
I have written a struct around it for easy access.
//set, get & remove User own profile in cache
struct UserProfileCache {
static let key = "userProfileCache"
static func save(_ value: Profile!) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
}
static func get() -> Profile! {
var userData: Profile!
if let data = UserDefaults.standard.value(forKey: key) as? Data {
userData = try? PropertyListDecoder().decode(Profile.self, from: data)
return userData!
} else {
return userData
}
}
static func remove() {
UserDefaults.standard.removeObject(forKey: key)
}
}
Profile is a Json encoded object.
struct Profile: Codable {
let id: Int!
let firstName: String
let dob: String!
}
Usage:
//save details in user defaults...
UserProfileCache.save(profileDetails)
Hope that helps!!!
Thanks
Swift save Codable object to UserDefault with #propertyWrapper
#propertyWrapper
struct UserDefault<T: Codable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data,
let user = try? JSONDecoder().decode(T.self, from: data) {
return user
}
return defaultValue
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
enum GlobalSettings {
#UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}
Example User model confirm Codable
struct User:Codable {
let name:String
let pass:String
}
How to use it
//Set value
GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")
//GetValue
print(GlobalSettings.user)
Save dictionary Into userdefault
let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData)
UserDefaults.standard.set(data, forKey: kUserData)
Retrieving the dictionary
let outData = UserDefaults.standard.data(forKey: kUserData)
let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary
Based on Harjot Singh answer. I've used like this:
struct AppData {
static var myObject: MyObject? {
get {
if UserDefaults.standard.object(forKey: "UserLocationKey") != nil {
if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data {
let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data)
return myObject!
}
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey")
}
}
}
Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:
extension Encodable {
var asDictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return try? JSONSerialization.jsonObject(with: data) as? [String : Any]
}
}
extension Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil }
guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = object
}
}
extension UserDefaults {
func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) {
self.set(encodable.asDictionary, forKey: key)
}
func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? {
guard let dictionary = self.dictionary(forKey: key) else {
return nil
}
return T(dictionary: dictionary)
}
}
If you want to also support array (of codables) to and from plist array, add the following to the extension:
extension UserDefaults {
func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) {
let arrayOfDictionaries = encodables.map({ $0.asDictionary })
self.set(arrayOfDictionaries, forKey: key)
}
func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? {
guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else {
return nil
}
return arrayOfDictionaries.compactMap({ T(dictionary: $0) })
}
}
If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):
extension UserDefaults {
func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws {
let data = try PropertyListEncoder().encode(encodable)
self.set(data, forKey: key)
}
func getDecodable<T: Decodable>(for key: String) -> T? {
guard
self.object(forKey: key) != nil,
let data = self.value(forKey: key) as? Data
else {
return nil
}
let obj = try? PropertyListDecoder().decode(T.self, from: data)
return obj
}
}
(With this second approach, you don't need the Encodable and Decodable extensions from the top)

How to store and retrieve data in userdefault? (Ex:Array of Struct )

struct ExpandableNames:Codable {
var isExpanded: Bool
var names: [String]
var icons: [String]
}
var names = ["Food/Drink", "Tours", "Transport", "Gifts","Flights", "Shopping", "Activities","Entertainment","Accomodation","Other"]
var icons = ["Food_Category", "Tours", "Transport_Category", "Gifts_Category","Flights_Category","Shopping_Category","Activities_category","Entertainment_category", "Accomodation_Category","Other_Category"]
var categoryWholeArray = [Int:ExpandableNames]()
How to store categoryWholeArray to user default?
I tried
Store userDefault:
UserDefaults.standard.set(try? PropertyListEncoder().encode(categoryWholeArray), forKey:"categoryWholeArray")
Retrieve data problem is here
if let data = UserDefaults.standard.value(forKey:"categoryWholeArray") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<categoryWholeArray.values>.self, from: data)
}
Anyone tried?
You need to encode data while storing in UserDefaults and when you try to get those data you must need to decode it.
Here is extension i have that might help you.
extension UserDefaults {
func setCustomObjToUserDefaults(CustomeObj: AnyObject, forKey:String) {
let defaults = UserDefaults.standard
let encodedData = NSKeyedArchiver.archivedData(withRootObject: CustomeObj)
defaults.set(encodedData, forKey: forKey)
defaults.synchronize()
}
func getCustomObjFromUserDefaults(forKey:String) -> AnyObject? {
let defaults = UserDefaults.standard
if defaults.object(forKey: forKey) != nil {
if let decoded = defaults.object(forKey: forKey) as? Data {
let decodedTeams = NSKeyedUnarchiver.unarchiveObject(with:decoded) as AnyObject
return decodedTeams
}
}
return nil
}
func setJsonObject<T: Encodable>(encodable: T, forKey key: String) {
if let data = try? JSONEncoder().encode(encodable) {
set(data, forKey: key)
}
}
func getJsonObject<T: Decodable>(_ type: T.Type, forKey key: String) -> T? {
if let data = object(forKey: key) as? Data,
let value = try? JSONDecoder().decode(type, from: data) {
return value
}
return nil
}
func removeCustomObject(forKey:String)
{
let defaults = UserDefaults.standard
defaults.removeObject(forKey: forKey)
defaults.synchronize()
}
}
All correct except
if let data = UserDefaults.standard.value(forKey:"categoryWholeArray") as? Data {
let songs2 = try? PropertyListDecoder().decode([Int:ExpandableNames].self, from: data)
print(songs2)
}

How to save array of objects (with image variables) in Swift and Xcode?

I am wondering how to save an array of objects from the following class:
class CustomDocument: NSObject, NSCoding {
let name : String
let image : UIImage
init(n: String, i: UIImage){
name = n
image = i
}
//other code excluded
}
Originally, I saved this array to User Defaults. Because the objects took up a lot of space, it caused a lot of lag in the app.
What is the best way to save an array of data that takes up a lot of space?
Thank you so much for the help and all responses are appreciated.
Try this code, Hope it helps:
class CustomDocument: NSObject, NSCoding {
var name : String?
var image : UIImage?
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "namekey")
if let imageData = image!.jpegData(compressionQuality: 1.0){
aCoder.encode(imageData, forKey: "imagekey")
}
UserDefaults.standard.synchronize()
}
required convenience init?(coder aDecoder: NSCoder) {
self.init()
if let name = (aDecoder.decodeObject(forKey: "namekey") as? String){
self.name = name
}
if let imageData = (aDecoder.decodeObject(forKey: "imagekey") as? Data){
if let image = UIImage(data: imageData){
self.image = image
}
}
}
}
func archiveDocument(document:CustomDocument) -> Data? {
do {
let archivedObject = try NSKeyedArchiver.archivedData(withRootObject: document, requiringSecureCoding: false)
return archivedObject
} catch {
// do something with the error
}
return nil
}
func unarchiveDocument(unarchivedObject:Data) -> CustomDocument? {
do {
if let document = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? CustomDocument {
return document
}
} catch {
// do something with the error
}
return nil
}
Example:
//Set the object, also you can use an array instead of an object
let obj = CustomDocument()
obj.name = "doc1"
obj.image = UIImage(named: "my_image")
if let archivedObject = archiveDocument(document: obj){
UserDefaults.standard.set(archivedObject, forKey: "obj")
}
//Get the object
if let archivedObject = UserDefaults.standard.data(forKey: "obj"){
obj = unarchiveDocument(unarchivedObject: archivedObject)
let myImage = obj?.image
}