I just started learning Swift so the level of this question may be obvious enough for you, however, I simply can't understand what to do.
I "created" a simple app which allows you to add logs of your day. Each cell stores the time you added, a custom icon that changes depending on the time, and, the log text itself (simple text).
Everything works fine. But, as I didn't know about the "Userdefaults" stuff, the clock resets every time I kill the app.
I read many articles about Userdefaults but I have no idea what to do to keep saving my data even when I kill the app.
Here's what I tried to do:
class ToDoItem: NSObject, NSCoding {
var title: String
var date: String
var type: String!
public init(title: String, date: String, type: String) {
self.title = title
self.date = date
self.type = type
}
required init?(coder aDecoder: NSCoder)
{
// Try to unserialize the "title" variable
if let title = aDecoder.decodeObject(forKey: "title") as? String
{
self.title = title
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .short
self.date = dateFormatter.string(from: Date())
let hour = NSCalendar.current.component(.hour, from: Date())
var tempType = ""
switch hour {
case 5..<9: tempType = "morning_1"
case 6..<12: tempType = "morning_2"
case 12: tempType = "noon_1"
case 13..<16: tempType = "afternoon_1"
case 16..<20: tempType = "dusk_1"
case 20..<23: tempType = "evening_1"
case 23..<00: tempType = "midnight_1"
default: tempType = "morning_1"
}
self.type = tempType
}
else
{
// There were no objects encoded with the key "title",
// so that's an error.
return nil
}
let userDefaults = UserDefaults.standard
userDefaults.set(true, forKey: "title")
}
func encode(with aCoder: NSCoder)
{
// Store the objects into the coder object
aCoder.encode(self.title, forKey: "title")
let defaults = UserDefaults.standard
defaults.set(false, forKey: "title")
}
}
extension Collection where Iterator.Element == ToDoItem
{
// Builds the persistence URL. This is a location inside
// the "Application Support" directory for the App.
private static func persistencePath() -> URL?
{
let url = try? FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
return url?.appendingPathComponent("todoitems.bin")
}
// Write the array to persistence
func writeToPersistence() throws
{
if let url = Self.persistencePath(), let array = self as? NSArray
{
let data = NSKeyedArchiver.archivedData(withRootObject: array)
try data.write(to: url)
}
else
{
throw NSError(domain: "com.example.MyToDo", code: 10, userInfo: nil)
}
}
// Read the array from persistence
static func readFromPersistence() throws -> [ToDoItem]
{
if let url = persistencePath(), let data = (try Data(contentsOf: url) as Data?)
{
if let array = NSKeyedUnarchiver.unarchiveObject(with: data) as? [ToDoItem]
{
return array
}
else
{
throw NSError(domain: "com.example.MyToDo", code: 11, userInfo: nil)
}
}
else
{
throw NSError(domain: "com.example.MyToDo", code: 12, userInfo: nil)
}
}
}
can anyone help me or at least point what I have to do? thank you!
You're using NSKeyedArchiver and NSKeyedUnarchiver which is a different mechanism than UserDefaults. Both are perfectly valid, but you have to pick one, they don't work together.
Here you are archiving and unarchiving an array of ToDoItem. For this to work, ToDoItem needs to be archivable, meaning it must implement the NSCoding protocol, which is:
public protocol NSCoding {
public func encode(with aCoder: NSCoder)
public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}
All the properties that you want to save must be added to/extracted from the NSCoder object. Here is an example:
class ToDoItem: NSObject, NSCoding
{
var title: String
var date: Date
required init?(coder aDecoder: NSCoder)
{
guard let title = aDecoder.decodeObject(forKey: "title") as? String else {
return nil
}
guard let date = aDecoder.decodeObject(forKey: "date") as? Date else {
return nil
}
self.title = title
self.date = date
}
func encode(with aCoder: NSCoder)
{
aCoder.encode(self.title, forKey: "title")
aCoder.encode(self.date, forKey: "date")
}
}
Related
I am trying to save custom object in UserDefaults, when I print the object, the retrieved object Id is different then saved object. Below is the code.
Saving
let customAlertView:CustomAlertView = CustomAlertView()
customAlertView.buyingRoleInnerStackView = contextInnerItem
customAlertView.buyingRoleInnerArrStackView = contextArrInnerItem
print("customAlertView :: \(customAlertView)")
//--> Prints customAlertView ::CustomAlertView: 0x1702bc9e0>
let encodedData = NSKeyedArchiver.archivedData(withRootObject: customAlertView)
let defaults = UserDefaults.standard
defaults.set(encodedData, forKey: AppConstants().KEY_CUSTOM_ALERT_VIEW)
Retriving
let defaults = UserDefaults.standard
if let viewData = defaults.object(forKey: AppConstants().KEY_CUSTOM_ALERT_VIEW) as? Data {
let alertView = NSKeyedUnarchiver.unarchiveObject(with: viewData) as! CustomAlertView
print("alertView :: \(alertView)")
}
//--> Prints alertView ::CustomAlertView: 0x1702be120>
}
CustomAlertView Class
class CustomAlertView :NSObject, NSCoding {
var buyingRoleInnerStackView:UIStackView!
var buyingRoleInnerArrStackView:UIStackView!
override init() {}
required init(coder aDecoder: NSCoder) {
self.buyingRoleInnerStackView = aDecoder.decodeObject(forKey: "buyingRoleInnerStackView") as! UIStackView
self.buyingRoleInnerArrStackView = aDecoder.decodeObject(forKey: "buyingRoleInnerArrStackView") as! UIStackView
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.buyingRoleInnerStackView, forKey: "buyingRoleInnerStackView")
aCoder.encode(self.buyingRoleInnerArrStackView, forKey: "buyingRoleInnerArrStackView")
}
....
}
I am trying to add a key and a value to a dictionary then add this dictionary the user defaults and read back into a dictionary object. I have two questions that I would really appreciate any help in,
1) why is the dictionary being read from user defaults empty? Since I added a key and a value to the dictionary shouldn't those be saved to the dictionary I retrieve from user defaults?
let defaults = UserDefaults.standard;
var myDict = [String: String]()
myDict["key"] = "value"
defaults.setValue(myDict, forKey: "myDict")
let mydict2 = defaults.object(forKey: "myDict") as? [String: String] ?? [String:String]()
print(mydict2)
2) What can I do to this code if the dictionary stores a custom class that I created as a value or a key so if the dictionary was like this:
class Car {
var engineSize: Int
var color: String
init() {
engineSize = 2000
color = "blue"
}
}
class Boat {
var surfaceArea: Int
var weight: Int
init() {
surfaceArea = 3500
weight = 4000
}
}
var myDict = [Car: Boat]()
how can I save that second dict to user defaults and read it from there?
Thank you
EDIT:
This is the answer suggested by ebby94:
var myDict = [String:String]()
myDict["key"] = "value";
let data = NSKeyedArchiver.archivedData(withRootObject: myDict)
UserDefaults.standard.set(data, forKey: "myDict")
func foo()
{
guard let archivedData = UserDefaults.standard.value(forKey: "myDict") as? Data
else
{
print("failed1")
return
}
guard var unarchivedDictionary = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? [String:String]
else
{
print("failed2")
return
}
print(unarchivedDictionary["key"]!)
}
foo()
However this prints failed1, I'm assuming the data wasn't archived correctly. Can this be because I'm running it in playground?
If you want to save custom object to userDefault first you need to encode & decode variable then save using archive & get data using unarchive.
class Car {
var engineSize: Int
var color: String
init() {
engineSize = 2000
color = "blue"
}
// Decode
required convenience public init(coder decoder: NSCoder)
{
self.init()
if let engineSize = decoder.decodeObject(forKey: "engineSize") as? Int
{
self.engineSize = engineSize
}
if let color = decoder.decodeObject(forKey: "color") as? String
{
self.color = color
}
}
// Encode
func encodeWithCoder(coder : NSCoder)
{
if let engineSize = self.engineSize
{
coder.encode(engineSize, forKey: "engineSize")
}
if let color = self.color
{
coder.encode(color, forKey: "weight")
}
}
}
class Boat {
var surfaceArea: Int
var weight: Int
init() {
surfaceArea = 3500
weight = 4000
}
// Decode
required convenience public init(coder decoder: NSCoder)
{
self.init()
if let surfaceArea = decoder.decodeObject(forKey: "surfaceArea") as? Int
{
self.surfaceArea = surfaceArea
}
if let weight = decoder.decodeObject(forKey: "weight") as? Int
{
self.weight = weight
}
}
// Encode
func encodeWithCoder(coder : NSCoder)
{
if let surfaceArea = self.surfaceArea
{
coder.encode(surfaceArea, forKey: "surfaceArea")
}
if let weight = self.weight
{
coder.encode(weight, forKey: "weight")
}
}
You can't save a dictionary directly in UserDefaults. You'll have to convert the dictionary into data and save it and then retrieve the data and unarchive it into dictionary.
Archive and save to UserDefaults
let myDict = [String:String]()
let data = NSKeyedArchiver.archivedData(withRootObject: myDict)
UserDefaults.standard.set(data, forKey: "myDict")
Retrieve and unarchive the data to dictionary
guard let archivedData = UserDefaults.standard.value(forKey: "myDict") as? Data
else{return}
guard let unarchivedDictionary = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? [String:String]
else{return}
Userdefaults not work in Playground. you need to implement and start it in an App in Simulator
I am having no difficulty archiving Int, String, Bool and Date but archiving an array of Boolean arrays is not working, here is a portion of the code:
class PlayerInfo: NSObject, NSCoding
{
public var score: Int = 0
public var lastPlayed: Date = Date()
public var visited: [[Bool]] = []
public var ringFound: Bool = false
init(dictionary: [String : AnyObject])
{
score = dictionary[ArchiveKeys.Score] as! Int
lastPlayed = dictionary[ArchiveKeys.LastPlayed] as! Date
visited = dictionary[ArchiveKeys.Visited] as! [[Bool]]
ringFound = dictionary[ArchiveKeys.RingFound] as! Bool
}
func encode(with archiver: NSCoder)
{
archiver.encode(score, forKey: ArchiveKeys.Score)
archiver.encode(lastPlayed, forKey: ArchiveKeys.LastPlayed)
archiver.encode(visited, forKey: ArchiveKeys.Visited)
archiver.encode(ringFound, forKey: ArchiveKeys.RingFound)
}
required init(coder unarchiver: NSCoder)
{
super.init()
score = unarchiver.decodeInteger(forKey: ArchiveKeys.Score)
lastPlayed = unarchiver.decodeObject(forKey: ArchiveKeys.LastPlayed) as! Date
visited = unarchiver.decodeObject(forKey: ArchiveKeys.Visited) as! [[Bool]]
ringFound = unarchiver.decodeBool(forKey: ArchiveKeys.RingFound)
}
class func retrieveCurrentGame() -> PlayerInfo!
{
let filePath: String = retrievePreviousGameFilePath(fileName: currentGameName)
if let game = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? PlayerInfo
{
print("retrieving previous game - found a game for previous day")
return game
}
else
{
print("retrieving previous game - can't find game data")
return nil
}
}
class func retrievePreviousGameFilePath(fileName: String) -> String
{
let kFileNameExtension: String = ".game"
let fileNameWithExtension: String = fileName + kFileNameExtension
let fileManager = FileManager.default
let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! as NSURL
print("url= " + (url.appendingPathComponent(fileNameWithExtension)?.path)!)
return (url.appendingPathComponent(fileNameWithExtension)?.path)!
}
public func saveCurrentGame()
{
let fileName: String = currentGameName + ".game"
let fileManager = FileManager.default
let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! as NSURL
print("url= " + (url.appendingPathComponent(fileName)?.path)!)
let filePath: String = (url.appendingPathComponent(fileName)?.path)!
NSKeyedArchiver.archiveRootObject(self, toFile: filePath)
}
}
I am unable to retrieve the [[Bool]] visited with my code. Is there a better way to encode?
FOLLOWUP FOR werm098:
I revised the encoding to:
let visitedData: Data = NSKeyedArchiver.archivedData(withRootObject: visited)
archiver.encode(visitedData, forKey: ArchiveKeys.Visited)
// archiver.encode(visited, forKey: ArchiveKeys.Visited)
and decoding to:
let visitedData: Data = unarchiver.decodeObject(forKey: ArchiveKeys.Visited) as! Data
visited = NSKeyedUnarchiver.unarchiveObject(with: visitedData) as! [[Bool]]
// visited = unarchiver.decodeObject(forKey: ArchiveKeys.Visited) as! [[Bool]]
But this still does not work?
Any other ideas?
that simple example is pretty much a working solution for [[Bool]], you may take a look on that and check it how that works.
class MyClass: NSObject, NSCoding {
public var boolArray: [[Bool]]? = nil
// MARK: Init
override init() {
self.boolArray = [[true, false, false], [false, true, false], [true, true, false]] // or any random sequece...
debugPrint("It was inited.")
}
// MARK: - Archiving / Unarchiving
func archived() -> Data {
return NSKeyedArchiver.archivedData(withRootObject: self)
}
class func unarchived(fromData data: Data) -> MyClass? {
return NSKeyedUnarchiver.unarchiveObject(with: data) as? MyClass
}
// MARK: - <NSCoding>
private enum Key {
case boolArray
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.boolArray, forKey: String(describing: Key.boolArray))
debugPrint("It was encoded.")
}
required init?(coder aDecoder: NSCoder) {
self.boolArray = aDecoder.decodeObject(forKey: String(describing: Key.boolArray)) as? [[Bool]]
debugPrint("It was decoded.")
}
}
and it is time to test it:
let myClass = MyClass()
debugPrint(myClass.boolArray)
let archivedData: Data = myClass.archived()
// do something with the data here... e.g. permanent storing
// ...
// ...
// restore back the data here from e.g. background storage device
let unarchivedObject: MyClass = MyClass.unarchived(fromData: archivedData)
debugPrint(unarchivedObject.boolArray)
and the console should look like:
"It was inited"
Optional([[true, false, false], [false, true, false], [true, true, false]])
"It was encoded."
"It was decoded."
Optional([[true, false, false], [false, true, false], [true, true, false]])
which means after unarchiving you can get back your values in one piece.
So I've just converted a small app from Swift 2.2 to Swift 3. I've gotten rid of the usual errors and bits of mop up required after the auto converter but I've got a run time issue that I can't work out.
I've got a custom class that I am saving to NSUserDefaults with the NSCoding protocol. When I try to decode the encoded object from NSUserDefaults it fails on the guard let duration = decoder.decodeObject(forKey: "duration") as? Int line as duration is printing as nil. Decoding the title string works fine however so at least that line of the encode function is working correctly.
This worked fine in 2.2 and I can't find anything indicating that Swift 3 made changes to the NSCoding. Any help would be much appreciated.
class TimerModel: NSObject, NSCoding, AVAudioPlayerDelegate {
var name: String
var active: Bool
var paused: Bool
var duration: Int
var remainingWhenPaused: Int?
var timerEndTime: Date?
var timerStartTime: Date?
var audioAlert: AlertNoise
var UUID: String
var colorScheme: BaseColor
var alarmRepetitions: Int
var timerRepetitions: Int
var currentTimerRepetition: Int
var audioPlaying: Bool
var player: AVAudioPlayer = AVAudioPlayer()
var countDownTimer: Timer = Timer()
var delegate: timerProtocol? = nil
init(withName name: String, duration: Int, UUID: String, color: BaseColor, alertNoise: AlertNoise, timerRepetitions: Int, alarmRepetitions: Int) {
self.name = name
self.active = false
self.paused = false
self.duration = duration
self.UUID = UUID
self.audioAlert = alertNoise
self.colorScheme = color
self.alarmRepetitions = alarmRepetitions
self.audioPlaying = false
self.timerRepetitions = timerRepetitions
self.currentTimerRepetition = 0
super.init()
}
convenience override init() {
self.init(withName: "Tap Timer 1", duration: 10, UUID: Foundation.UUID().uuidString, color: .Red, alertNoise: .ChurchBell, timerRepetitions: 1, alarmRepetitions: 0)
}
// MARK: NSCoding
required convenience init? (coder decoder: NSCoder) {
print("in init coder:")
print("Name: \(decoder.decodeObject(forKey: "name"))")
print("Duration: \(decoder.decodeObject(forKey: "duration"))")
guard let name = decoder.decodeObject(forKey: "name") as? String
else {
print("init coder name guard failed")
return nil
}
guard let duration = decoder.decodeObject(forKey: "duration") as? Int
else {
print("init coder duration guard failed")
print("duration: \(decoder.decodeObject(forKey: "duration"))")
return nil
}
guard let audioAlertRawValue = decoder.decodeObject(forKey: "audioAlert") as? String
else {
print("init coder audioAlert guard failed")
return nil
}
guard let UUID = decoder.decodeObject(forKey: "UUID") as? String
else {
print("init coder UUID guard failed")
return nil
}
guard let colorSchemeRawValue = decoder.decodeObject(forKey: "colorScheme") as? String
else {
print("init coder colorScheme guard failed")
return nil
}
guard let alarmRepetitions = decoder.decodeObject(forKey: "alarmRepetitions") as? Int
else {
print("init coder alarmRepetitions guard failed")
return nil
}
guard let timerRepetitions = decoder.decodeObject(forKey: "timerRepetitions") as? Int
else {
print("init coder timerRepetitions guard failed")
return nil
}
guard let audioAlert = AlertNoise(rawValue: audioAlertRawValue)
else{
print("No AlertNoise rawValue case found")
return nil
}
guard let colorScheme = BaseColor(rawValue: colorSchemeRawValue)
else{
print("No BaseColor rawValue case found")
return nil
}
print("initCoder guards passed, initing timer")
print("\(name), \(duration), \(UUID), \(colorScheme), \(audioAlert), \(timerRepetitions), \(alarmRepetitions)")
self.init(withName: name, duration: duration, UUID: UUID, color: colorScheme, alertNoise: audioAlert, timerRepetitions: timerRepetitions, alarmRepetitions: alarmRepetitions)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.duration, forKey: "duration")
aCoder.encode(self.audioAlert.rawValue, forKey: "audioAlert")
aCoder.encode(self.UUID, forKey: "UUID")
aCoder.encode(self.colorScheme.rawValue, forKey: "colorScheme")
aCoder.encode(self.alarmRepetitions, forKey: "alarmRepetitions")
aCoder.encode(self.timerRepetitions, forKey: "timerRepetitions")
}
So it seems the solution is simple if a little unintuitive.
So I encoded the class ivars with the general method encode(self.ivar, forKey: "keyName") however if that ivar is an int it needs to be decoded with decodeInteger(forKey: "keyName") - this entails getting rid of the guard statements as well since this method return an non-optional. Seems odd to have to decode with the integer specific method if it was decoded with the generalist method - this wasn't the case in Swift 2.2.
Excellent answer by SimonBarker and it solved the same issue I had. I eventually applied his solution to my own code and revised it too so that encoding is done with the generalist method. You can "force" encoding with the generalist method by using:
func encode(_ objv: Any?, forKey key: String)
So in your code you could use:
aCoder.encode(self.name as Any?, forKey: "name")
That way self.name is encoded as an object and does not break your existing code, namely: decoder.decodeObject(forKey: "name") as? String
This might not be the most elegant solution, but at least it worked for me without the need to change code that worked beautifully in Swift 2.3, but which was broken in Swift 3...
In my scenario, I am trying to maintain cache data for UICollectionView. Here, VC2 to VC1 I am passing array data and VC1 I am loading passed data into UICollectionView. Now, If I close and reopen app then I can’t able to see UICollectionView data Its all removed but I have to maintain cache. How to do it?
Collection View Data Load From VC2 passed array
func pass(data: [TeamListData]) {
print("ARRAY DATA RECEIVED:\(data)")
participantsData = data
self.collectionView.reloadData()
}
My Array Data
ARRAY DATA RECEIVED:[TeamListData(userid: Optional("1"), firstname: Optional(“abc”), designation: Optional("Analyst"), profileimage: Optional(“url.jpg"), isSelected: true), TeamListData(userid: Optional(“2”), firstname: Optional(“def”), designation: Optional("Executive"), profileimage: Optional(“url.jpg"), isSelected: true)]
Saved your data after getting callback in VC1
Code
func pass(data: [TeamListData]) {
print("ARRAY DATA RECEIVED:\(data)")
participantsData = data
self.collectionView.reloadData()
UserDefaults.standard.setValue( try? PropertyListEncoder().encode(data), forKey: "sessiondata")
}
Inside viewDidLoad in VC1
func storeValidaion(){
// Retrive Array Values
if participantsData == nil {
if let data = UserDefaults.standard.value(forKey:"sessiondata") as? Data {
guard let sessionData = try? PropertyListDecoder().decode(Array<TeamListData>.self, from: data) else {
return
}
print("ARRAY VALUES: \(sessionData)")
self.participantsData = sessionData
self.collectionView.reloadData()
}
}
}
You're not looking for a cache, but for a persistent storage. Depending on where your data is coming from and how good solution you need, you can either use the disk, UserDefaults or a database approach such as CoreData, Realm or others.
There's a handy tutorial with a lot of code here for storing custom objects in UserDefaults with NSCoding: https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/PersistData.html
E.g.
Conforming to the NSCoding:
struct PropertyKey {
static let name = "name"
static let photo = "photo"
static let rating = "rating"
}
class Meal: NSObject, NSCoding {
let name: String
let photo: UIImage
let rating: Int
required convenience init?(coder aDecoder: NSCoder) {
// The name is required. If we cannot decode a name string, the initializer should fail.
guard let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String else {
return nil
}
// Because photo is an optional property of Meal, just use conditional cast.
let photo = aDecoder.decodeObject(forKey: PropertyKey.photo) as? UIImage
let rating = aDecoder.decodeInteger(forKey: PropertyKey.rating)
// Must call designated initializer.
self.init(name: name, photo: photo, rating: rating)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.name)
aCoder.encode(photo, forKey: PropertyKey.photo)
aCoder.encode(rating, forKey: PropertyKey.rating)
}
}
Saving data:
private func saveMeals() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path)
if isSuccessfulSave {
os_log("Meals successfully saved.", log: OSLog.default, type: .debug)
} else {
os_log("Failed to save meals...", log: OSLog.default, type: .error)
}
}
Loading data:
private func loadMeals() -> [Meal]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Meal.ArchiveURL.path) as? [Meal]
}
Realm on the other hand offers a lot of flexibility for a bit more time investment in learning a third party lib: https://realm.io/docs/swift/latest/#models