so as the title already states I'm trying to save an array to the database. If this is possible, how do I do it? If not I hope you can help me with some other solution.
I am making an iOS app where if the user touches (and moves) the screen I store this data in an array. Because it needs to be multi-touch all the CGPoints of the touches (either touchesBegan or touchesMoved) on one moment are stored in an array, which again is stored in the main array. Resulting in var everyLocation = [[CGPoint]](). I already found out that it's not possible to store a CGPoint in a database directly, so I can convert them to string with NSStringFromCGPoint(pointVariable). This however isn't very useful as long as I can't store the array...
I want to store the date on which it happened too, so in my database I created the entity 'Locations' with two attributes: 'locations' and 'date'. In the final application the entity name will be the name of the exercise the user was doing (I have about four exercises, so four entities).
Most of the sample code I've seen stores the CGPoint either in a separate x and y or in one string. I can maybe do this too, so I don't have to store arrays. To do this I think I will have to make the attribute(s) the coordinates of the touche(s), the entity name would be the date, and the db name would be the name of the exercise. If this is the only solution, how do I create an entity (with attributes) at run-time?
Thanks in advance
Swift3 makes it seamless,
just write
typealias Point = CGPoint
and set the attribute type to Transformable and set the Custom class of it to
Array<Point>
Works for me without having to do anything.
1) add a "Transformable" type attribute.
2) Event.h
#interface Event : NSManagedObject
#property (nonatomic, retain) NSArray * absentArray;
#interface AbsentArray : NSValueTransformer
#end
Event.m
#implementation AbsentArray
+ (Class)transformedValueClass
{
return [NSArray class];
}
+ (BOOL)allowsReverseTransformation
{
return YES;
}
- (id)transformedValue:(id)value
{
return [NSKeyedArchiver archivedDataWithRootObject:value];
}
- (id)reverseTransformedValue:(id)value
{
return [NSKeyedUnarchiver unarchiveObjectWithData:value];
}
#end
3) Just use it as a normal array
Event *event = //init
event.absentArray = #[1,2,3];
[context save:nil]
Just change these code in swift.
You can understand as .swfit combine .h/.m file. Objective C has .h as header file which many properties there. .m is implication file which methods should be there.
For example:
.swift
import Foundation
import CoreData
class Event: NSManagedObject {
#NSManaged var absentArray: AnyObject
}
3) save:
let appDelegate =
UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
I finally managed to put the pieces together after William pointed me in the direction of transformables. I used this tutorial to understand how to work with this: http://jamesonquave.com/blog/core-data-in-swift-tutorial-part-1/
Here are the things I learned from going through this exercise that was prompted by the warning message:
At some point, Core Data will default to using "NSSecureUnarchiveFromData" when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.
My app had collected the series of points [CGPoint] created by drawing on the screen with an Apple Pencil or finger and stored that in CoreData - basically the heart of a thing I called a Scribble. To store in CoreData, I created an attribute named “points” and set the type to Transformable. The Custom Class was set to [CGPoint]. Also, I set CodeGen to Manual rather than the automatic “Class Definition” option. When I generated the CoreData managed object subclass files, it generates a +CoreDataClass.swift file with the critical line of interest being:
#NSManaged public var points: [CGPoint]?
It should be noted, that there is actually a problem if you use the automatic option as the file that is generated doesn’t know what a CGPoint is and cannot be edited to add the import for UIKit for it to find the definition.
This worked fine until Apple started wanting to encourage secure coding. In the code file below, I developed a ScribblePoints object to work with the encoding and its associated data transformer.
//
// ScribblePoints.swift
//
import Foundation
import UIKit
public class ScribblePoints: NSObject, NSCoding {
var points: [CGPoint] = []
enum Key: String {
case points = "points"
}
init(points: [CGPoint]) {
self.points = points
}
public func encode(with coder: NSCoder) {
coder.encode(points, forKey: Key.points.rawValue)
}
public required convenience init?(coder: NSCoder) {
if let sPts = coder.decodeObject(of: ScribblePoints.self, forKey: Key.points.rawValue) {
self.init(points: sPts.points)
} else {
return nil
}
}
}
extension ScribblePoints : NSSecureCoding {
public static var supportsSecureCoding = true
}
#available(iOS 12.0, *)
#objc(ScribblePointsValueTransformer)
final class ScribblePointsValueTransformer: NSSecureUnarchiveFromDataTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: ScribblePointsValueTransformer.self))
override static var allowedTopLevelClasses: [AnyClass] {
return [ScribblePoints.self]
}
public static func register() {
let transformer = ScribblePointsValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
if let data = value as? Data {
// Following deprecated at iOS12:
// if let data = value as? Data {
// if let points = NSKeyedUnarchiver.unarchiveObject(with: data) as? [CGPoint] {
// return points
// }
// }
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let decodeResult = unarchiver.decodeObject(of: [NSArray.self, ScribblePoints.self], forKey: NSKeyedArchiveRootObjectKey)
if let points = decodeResult as? [CGPoint] {
return points
}
} catch {
}
}
return nil
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
if let points = value as? [CGPoint] {
// Following deprecated at iOS12:
// let data = NSKeyedArchiver.archivedData(withRootObject: points)
// return data
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: points, requiringSecureCoding: true)
return data
} catch {
}
}
return nil
}
}
With the above in place, I could finally fill in ScribblePointsValueTransformer for the Transformer name for the “points” attribute in CoreData.
One can also switch the Custom Class from [CGPoint] to ScribblePoints. This doesn’t appear to affect code execution. However, if you re-generate the +CoreDataClass.swift file, the critical line of interest will become:
#NSManaged public var points: ScribblePoints?
and when you re-compile you will have code changes to make to deal with the different definition. If you were starting from scratch, it seems you may want to simply use the ScribblePoints definition, and avoid the hassles of dealing with NSArrays and NSPoints and other stuff you magically encounter in strange ways with [CGPoint].
Above was with Swift 5.
Ran into a warning message with my answer above when I hooked up an older iOS device (iOS9) to Xcode. Things worked, but the warning message about not finding the value transformer was disturbing. The problem was that the previous answer only defined and registered the value transformer if you were on iOS12+. To work without complaint on earlier systems, one needs to avoid the NSSecureUnarchiveFromDataTransformer, use ValueTransformer instead, and rely on the NSSecureCoding conformance for your coding object. Then you can register your value transformer on older iOS systems. It should also be noted that the transformedValue() and reverseTransformedValue() functions became reversed.
The net result is the following code instead.
//
// ScribblePoints.swift
//
import Foundation
import UIKit
public class ScribblePoints: NSObject, NSCoding {
var points:[CGPoint] = []
enum Key: String {
case points = "points"
}
init(points: [CGPoint]) {
self.points = points
}
public func encode(with coder: NSCoder) {
coder.encode(points, forKey: Key.points.rawValue)
}
public required convenience init?(coder: NSCoder) {
if let sPts = coder.decodeObject(of: ScribblePoints.self, forKey: Key.points.rawValue) {
self.init(points: sPts.points)
} else {
return nil
}
}
}
extension ScribblePoints : NSSecureCoding {
public static var supportsSecureCoding = true
}
#objc(ScribblePointsValueTransformer)
final class ScribblePointsValueTransformer: ValueTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: ScribblePointsValueTransformer.self))
public static func register() {
let transformer = ScribblePointsValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
override class func transformedValueClass() -> AnyClass {
return ScribblePoints.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
if let data = value as? Data {
do {
if #available(iOS 11.0, *) {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let decodeResult = unarchiver.decodeObject(of: [NSArray.self, ScribblePoints.self], forKey: NSKeyedArchiveRootObjectKey)
if let points = decodeResult as? [CGPoint] {
return points
}
} else {
// Fallback on earlier versions
if let data = value as? Data {
if let points = NSKeyedUnarchiver.unarchiveObject(with: data) as? [CGPoint] {
return points
}
}
}
} catch {
}
}
return nil
}
override func transformedValue(_ value: Any?) -> Any? {
if let points = value as? [CGPoint] {
do {
if #available(iOS 11.0, *) {
let data = try NSKeyedArchiver.archivedData(withRootObject: points, requiringSecureCoding: true)
return data
} else {
// Fallback on earlier versions
let data = NSKeyedArchiver.archivedData(withRootObject: points)
return data
}
} catch {
}
}
return nil
}
}
In CoreData, the way things are defined is shown below.
Related
I'm using ReamSwift to store data in my iOS application and I have troubles to figure out, why sometimes objects are duplicated in my application.
I use an UUID as primary key on every object. This property is defined in a base class and all the others are subclasses of that.
I use the update approach with a dictionary. So new objects are created and existing should be updated.
I use a central class which holds the Realm object and handles the update. This method could be called from multiple different threads.
This is my base class
import RealmSwift
open class RealmObject : Object {
static let ID_ATTRIBUTE : String = "id"
#objc dynamic var id : String = ""
override public static func primaryKey() -> String? {
return "id"
}
}
Central class managing swift.
This class holds the Realm Object
public var realm: Realm {
var tempRealm: Realm?
do {
tempRealm = try Realm(configuration: configuration)
}
catch let err {
print("------ERROR----- initializing realm\n \(err)")
}
if let tempRealm = tempRealm{
return tempRealm
}
return self.realm
}
This is the update method in the class. As a fallback if the id property is not set, it will be set as it is the primary key
func update<T: RealmObject>(_ obj : T, values : [String:Any?] ) -> T? {
var vals = values
do {
var res : T?
try realm.write {
if let idattr = vals[T.ID_ATTRIBUTE] as? String {
if(idattr.isEmpty) {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
} else {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
res = realm.create(T.self, value: vals, update: .modified)
}
return res
} catch {
return nil
}
}
Could calling the update method cause in any case the duplication as I set the primary key of the object? The problem is, I cannot reproduce to find the problem, i just encounter the issue from time to time and in the field from users.
One interesting thing, when a copy is deleted, also the other object will be deleted.
This is the method which deletes objects by id and type
func delete<T: RealmObject>(_ id : String, _ type : T.Type) -> Bool {
do {
let obj = get(id, T.self)
if let obj = obj {
try realm.write {
realm.delete(obj)
}
return true
} else {
return false
}
} catch {
return false
}
}
I have developed an ios application that allow users to edit a musical score sheet and now i'd like to implement data persistence to prevent the discarding of changes.
Reading on ios documentation i've noticed that exists different ways to improve data persistence and I believe that the best way for my application is Core Data.
Considering that my application use a lot of custom object i met a lot of problems.
I'm trying to use core data to save an entity, referred to a score sheet, composed by two attributes:
name: String
score: Array of another custom object (Measure), composed by other custom object (Score Element)
According to documentation and other q/a I've decided to use a Trasformable type on the model:
So I've declared a generic class used as trasformer for score attribute:
public class NSSecureCodingValueTransformer<T: NSSecureCoding & NSObject>: ValueTransformer {
public override class func transformedValueClass() -> AnyClass { T.self }
public override class func allowsReverseTransformation() -> Bool { true }
public override func transformedValue(_ value: Any?) -> Any? {
guard let value = value as? T else { return nil }
return try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
}
public override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
let result = try? NSKeyedUnarchiver.unarchivedObject(
ofClass: T.self,
from: data as Data
)
return result
}
/// The name of this transformer. This is the name used to register the transformer using `ValueTransformer.setValueTransformer(_:forName:)`
public static var transformerName: NSValueTransformerName {
let className = NSStringFromClass(T.self)
return NSValueTransformerName("DHC\(className)ValueTransformer")
}
/// Registers the transformer by calling `ValueTransformer.setValueTransformer(_:forName:)`.
public static func registerTransformer() {
let transformer = NSSecureCodingValueTransformer<T>()
ValueTransformer.setValueTransformer(transformer, forName: transformerName)
}
}
Using in this way a DHCMeasureValueTransformer as trasformer in DataModel file.
The problem is that when i save, no error occurs but when i fetch data for a new restart of application, i can fetch just the name of score sheet, while the score array it's empty, like if no elements it's been put inside (clearly, before of save, i've try to print array content, that prove that i'm working with a non empty array)
Here is the code of the save:
static func saveContext() {
let context = getContext()
do {
try context.save()
} catch {
print("error during the save.")
}
}
And here is the code of two classes of the entity object:
// DataClass
#objc(ScoreSheet)
public class ScoreSheet: NSManagedObject {
static var supportsSecureCoding: Bool {
return true
}
}
//DataProperties
extension ScoreSheet {
#nonobjc public class func fetchRequest() -> NSFetchRequest<ScoreSheet> {
return NSFetchRequest<ScoreSheet>(entityName: "ScoreSheet")
}
#NSManaged public var name: String
#NSManaged public var score: [Measure]
}
Clearly Measure class implements NSSecureCoding and method for decode and encode the object.
Here is Measure class implementation:
import Foundation
class Measure: NSObject, NSCoding, NSSecureCoding {
var elements : [ScoreElement] = []
var timeSig : TimeSignature
var clef : Clef
static var supportsSecureCoding = true
init(time : TimeSignature, clef : Clef) {
self.timeSig = time
self.clef = clef
}
func encode(with encoder: NSCoder) {
encoder.encode(self.elements, forKey: "elements")
encoder.encode(self.timeSig, forKey: "timeSig")
encoder.encode(self.clef, forKey: "clef")
}
required convenience init? (coder decoder: NSCoder) {
let elements = decoder.decodeObject(forKey: "elements") as! [ScoreElement]
let timeSig = decoder.decodeObject(forKey: "timeSig") as! TimeSignature
let clef = decoder.decodeObject(forKey: "clef") as! Clef
self.init(time: timeSig, clef: clef)
self.elements = elements
}
}
I'm not sure what's going wrong but there are a couple of things that need fixing that might affect your results.
Firstly, the computed transformer name is not the same as the one you're trying to use. When this line executes, and T is Measure,
let className = NSStringFromClass(T.self)
Then className is going to be something like MyProjectName.Measure. The computed transformer name ends up as something like NSValueTransformerName(_rawValue: DHCMyProjectName.MeasureValueTransformer), which doesn't match what you're using in the data model. All of which means that your transformer isn't getting used.
But that probably doesn't matter because if Measure conforms to NSSecureCoding and all of Measure's properties (ScoreElement, TimeSignature, Clef) also conform to NSSecureCoding (which seems to be the case since your code isn't throwing exceptions), then you don't need a custom transformer at all. If a transformable property type conforms to NSSecureCoding then Core Data will automatically use NSSecureCoding. You don't need a custom transformer unless you don't want to or can't conform to NSSecureCoding for some reason. Because of this, it doesn't matter that your transformer isn't being used.
As for why Measure isn't surviving the encode/decode process, I don't know, but you may help clear things up by removing the distraction of the unnecessary encode/decode class. I'd also suggest putting a breakpoint in Measure in the encode(with:) and init(coder:) methods. You should hit those breakpoints when saving and fetching data.
Using NSObject and NSCode I can't seem to unarchive more than one member of my class. The class looks like this:
import Foundation
import os.log
class myObject:NSObject,NSCoding {
var type:String
var color:String = "White"
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory,in: .userDomainMask).first!
static let ArchiveURL =DocumentsDirectory.appendingPath("myObjects")
struct PropertyKey {
static let type = "type"
static let color = "color"
}
init(objType:String) {
self.type = objType)
}
func encode(with aCoder:NSCoder) {
aCoder.encode(type,forKey:PropertyKey.type)
aCoder.encode(color,forKey:PropertyKey.color)
}
required convenience init?(coder aDecoder: NSCoder) {
print("LOADING object")
guard let myType = (aDecoder.decodeObject(forKey: PropertyKey.type) as? String?)!
else {
os_log("!!! object class unable to decode type.",log:OSLog.default,type: .debug)
return nil
}
self.init(objType: myType)
color = aDecoder.decodeObject(forKey: PropertyKey.color) as! String
}
And I have a ViewController with a viewDidLoad that looks like:
override func viewDidLoad() {
super.viewDidLoad()
let thisObject = loadObject()
print("My object has type: \(String(describing: thisObject?.type))")
print("My object has color: \(String(describing: thisObject?.color))")
}
private func loadObject() -> myObject? {
print("TRYING to LOAD")
return (NSKeyedUnarchiver.unarchiveObject(withFile: myObject.ArchiveURL.path) as? myObject)
}
And the output I get from this is
TRYING to LOAD
My object has type: theTypeIgave
My object has color: White
Two things strike me as weird. 1. I can get different types when I run and save different types but the color is always White even if I saved Blue or Red last time I ran the program. 2. I never get the print statement to output anything in the required convenience init? function, so does that init ever get called?
I want to create a duplicate of a persisted object such that the new instance has all the same values but is not attached to Realm. Using Object(value: persistedInstance) works great for classes whose properties are all strings, dates, numbers, etc. However, when duplicating an instance of a class with List type properties, the duplicate's List and the List's elements continue to reference the persisted records. How can I create a duplicate that's fully detached from Realm, including any Lists and elements in those Lists?
You can make a deep copy of your object via the following extension functions:
import UIKit
import Realm
import RealmSwift
protocol RealmListDetachable {
func detached() -> Self
}
extension List: RealmListDetachable where Element: Object {
func detached() -> List<Element> {
let detached = self.detached
let result = List<Element>()
result.append(objectsIn: detached)
return result
}
}
#objc extension Object {
public func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard
property != objectSchema.primaryKeyProperty,
let value = value(forKey: property.name)
else { continue }
if let detachable = value as? Object {
detached.setValue(detachable.detached(), forKey: property.name)
} else if let list = value as? RealmListDetachable {
detached.setValue(list.detached(), forKey: property.name)
} else {
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension Sequence where Iterator.Element: Object {
public var detached: [Element] {
return self.map({ $0.detached() })
}
}
Use
/// in collections
let data = realm.objects(AbcDfg.self).detached
/// single object
data.first?.detached()
This is not yet supported natively by Realm, but a requested feature tracked by issue #3381.
For now, you would need to implement your own deep copy constructor. A common strategy is to do that on every model and call the deep copy constructors of related objects. You need to pay attention though that you don't run into cycles.
We use ObjectMapper to create a deep copy of the object by turning into JSON and then turn that JSON back into same object except it's not associated with Realm.
Mike.
As mentioned in the issue tracked at #3381, the solution for now is an implementation to create detached copies from Realm objects.
There is a better version of the detachable object implementation at
https://github.com/realm/realm-cocoa/issues/5433#issuecomment-415066361.
Incase the link doesnot work, the code by Alarson93 is:
protocol DetachableObject: AnyObject {
func detached() -> Self
}
extension Object: DetachableObject {
func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard let value = value(forKey: property.name) else { continue }
if property.isArray == true {
//Realm List property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else if property.type == .object {
//Realm Object property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else {
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension List: DetachableObject {
func detached() -> List<Element> {
let result = List<Element>()
forEach {
if let detachable = $0 as? DetachableObject {
let detached = detachable.detached() as! Element
result.append(detached)
} else {
result.append($0) //Primtives are pass by value; don't need to recreate
}
}
return result
}
func toArray() -> [Element] {
return Array(self.detached())
}
}
extension Results {
func toArray() -> [Element] {
let result = List<Element>()
forEach {
result.append($0)
}
return Array(result.detached())
}
}
In Realm 5.0.0 freeze() method has been added.
according to release notes:
Add support for frozen objects. Realm, Results, List and Object now have freeze() methods which return a frozen copy of the object. These objects behave similarly to creating unmanaged deep copies of the source objects. They can be read from any thread and do not update when writes are made to the Realm, but creating frozen objects does not actually copy data out of the Realm and so can be much faster and use less memory. Frozen objects cannot be mutated or observed for changes (as they never change). (PR #6427).
Previously answered here
As of now, Dec 2020, there is not proper solution of this issue. We have many workarounds though.
Here is the one I have been using, and one with less limitations in my opinion.
Make your Realm Model Object classes conform to codable
class Dog: Object, Codable{
#objc dynamic var breed:String = "JustAnyDog"
}
Create this helper class
class RealmHelper {
//Used to expose generic
static func DetachedCopy<T:Codable>(of object:T) -> T?{
do{
let json = try JSONEncoder().encode(object)
return try JSONDecoder().decode(T.self, from: json)
}
catch let error{
print(error)
return nil
}
}
}
Call this method whenever you need detached / true deep copy of your Realm Object, like this:
//Suppose your Realm managed object: let dog:Dog = RealmDBService.shared.getFirstDog()
guard let detachedDog = RealmHelper.DetachedCopy(of: dog) else{
print("Could not detach Dog")
return
}
//Change/mutate object properties as you want
detachedDog.breed = "rottweiler"
As you can see we are piggy backing on Swift's JSONEncoder and JSONDecoder, using power of Codable, making true deep copy no matter how many nested objects are there under our realm object. Just make sure all your Realm Model Classes conform to Codable.
Though its NOT an ideal solution, but its one of the most effective workaround.
I have a class
class Player {
var name = ""
func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(name)
}
func initWithCoder(decoder: NSCoder) -> Player {
self.name = decoder.decodeObjectForKey("name") as String
return self
}
init(coder aDecoder: NSCoder!) {
self.name = aDecoder.decodeObjectForKey("name") as String
}
init(name: String) {
self.name = name
}
}
and i want to serialise it and save to user defaults.
First of all I'm not sure how to correctly write encoder and decoder. So for init i wrote two methods.
When i try to execute this code:
func saveUserData() {
let player1 = Player(name: "player1")
let myEncodedObject = NSKeyedArchiver.archivedDataWithRootObject(player1)
NSUserDefaults.standardUserDefaults().setObject(myEncodedObject, forKey: "player")
}
app crashes and i get this message:
*** NSForwarding: warning: object 0xebf0000 of class '_TtC6GameOn6Player' does not implement methodSignatureForSelector: -- trouble ahead
What do i do wrong?
In Swift 4 you don't need NSCoding anymore! There is a new protocol called Codable!
class Player: Codable {
var name = ""
init(name: String) {
self.name = name
}
}
And Codable also supports Enums and Structs so you can rewrite your player class to a struct if you want!
struct Player: Codable {
let name: String
}
To save your player in Userdefaults:
let player = Player(name: "PlayerOne")
try? UserDefaults.standard.set(PropertyListEncoder().encode(player), forKey: "player")
Note: PropertyListEncoder() is a class from the framework Foundation
To Retrieve:
let encoded = UserDefault.standard.object(forKey: "player") as! Data
let storedPlayer = try! PropertyListDecoder().decode(Player.self, from: encoded)
For more information, read https://developer.apple.com/documentation/swift/codable
NSKeyedArchiver will only work with Objective-C classes, not pure Swift classes. You can bridge your class to Objective-C by marking it with the #objc attribute or by inheriting from an Objective-C class such as NSObject.
See Using Swift with Cocoa and Objective-C for more information.
tested with XCode 7.1.1, Swift 2.1 & iOS 9
You have a few options to save your (array of) custom objects :
NSUserDefaults : to store app settings, preferences, user defaults :-)
NSKeyedArchiver : for general data storage
Core data : for more complex data storage (database like)
I leave Core data out of this discussion, but want to show you why you should better use NSKeyedArchiver over NSUserdefaults.
I've updated your Player class and provided methods for both options. Although both options work, if you compare the 'load & save' methods you'll see that NSKeydArchiver requires less code to handle arrays of custom objects. Also with NSKeyedArchiver you can easily store things into separate files, rather than needing to worry about unique 'key' names for each property.
import UIKit
import Foundation
// a custom class like the one that you want to archive needs to conform to NSCoding, so it can encode and decode itself and its properties when it's asked for by the archiver (NSKeydedArchiver or NSUserDefaults)
// because of that, the class also needs to subclass NSObject
class Player: NSObject, NSCoding {
var name: String = ""
// designated initializer
init(name: String) {
print("designated initializer")
self.name = name
super.init()
}
// MARK: - Conform to NSCoding
func encodeWithCoder(aCoder: NSCoder) {
print("encodeWithCoder")
aCoder.encodeObject(name, forKey: "name")
}
// since we inherit from NSObject, we're not a final class -> therefore this initializer must be declared as 'required'
// it also must be declared as a 'convenience' initializer, because we still have a designated initializer as well
required convenience init?(coder aDecoder: NSCoder) {
print("decodeWithCoder")
guard let unarchivedName = aDecoder.decodeObjectForKey("name") as? String
else {
return nil
}
// now (we must) call the designated initializer
self.init(name: unarchivedName)
}
// MARK: - Archiving & Unarchiving using NSUserDefaults
class func savePlayersToUserDefaults(players: [Player]) {
// first we need to convert our array of custom Player objects to a NSData blob, as NSUserDefaults cannot handle arrays of custom objects. It is limited to NSString, NSNumber, NSDate, NSArray, NSData. There are also some convenience methods like setBool, setInteger, ... but of course no convenience method for a custom object
// note that NSKeyedArchiver will iterate over the 'players' array. So 'encodeWithCoder' will be called for each object in the array (see the print statements)
let dataBlob = NSKeyedArchiver.archivedDataWithRootObject(players)
// now we store the NSData blob in the user defaults
NSUserDefaults.standardUserDefaults().setObject(dataBlob, forKey: "PlayersInUserDefaults")
// make sure we save/sync before loading again
NSUserDefaults.standardUserDefaults().synchronize()
}
class func loadPlayersFromUserDefaults() -> [Player]? {
// now do everything in reverse :
//
// - first get the NSData blob back from the user defaults.
// - then try to convert it to an NSData blob (this is the 'as? NSData' part in the first guard statement)
// - then use the NSKeydedUnarchiver to decode each custom object in the NSData array. This again will generate a call to 'init?(coder aDecoder)' for each element in the array
// - and when that succeeded try to convert this [NSData] array to an [Player]
guard let decodedNSDataBlob = NSUserDefaults.standardUserDefaults().objectForKey("PlayersInUserDefaults") as? NSData,
let loadedPlayersFromUserDefault = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSDataBlob) as? [Player]
else {
return nil
}
return loadedPlayersFromUserDefault
}
// MARK: - Archivig & Unarchiving using a regular file (using NSKeyedUnarchiver)
private class func getFileURL() -> NSURL {
// construct a URL for a file named 'Players' in the DocumentDirectory
let documentsDirectory = NSFileManager().URLsForDirectory((.DocumentDirectory), inDomains: .UserDomainMask).first!
let archiveURL = documentsDirectory.URLByAppendingPathComponent("Players")
return archiveURL
}
class func savePlayersToDisk(players: [Player]) {
let success = NSKeyedArchiver.archiveRootObject(players, toFile: Player.getFileURL().path!)
if !success {
print("failed to save") // you could return the error here to the caller
}
}
class func loadPlayersFromDisk() -> [Player]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(Player.getFileURL().path!) as? [Player]
}
}
I've tested this class as follows (single view app, in the viewDidLoad method of the ViewController)
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// create some data
let player1 = Player(name: "John")
let player2 = Player(name: "Patrick")
let playersArray = [player1, player2]
print("--- NSUserDefaults demo ---")
Player.savePlayersToUserDefaults(playersArray)
if let retreivedPlayers = Player.loadPlayersFromUserDefaults() {
print("loaded \(retreivedPlayers.count) players from NSUserDefaults")
print("\(retreivedPlayers[0].name)")
print("\(retreivedPlayers[1].name)")
} else {
print("failed")
}
print("--- file demo ---")
Player.savePlayersToDisk(playersArray)
if let retreivedPlayers = Player.loadPlayersFromDisk() {
print("loaded \(retreivedPlayers.count) players from disk")
print("\(retreivedPlayers[0].name)")
print("\(retreivedPlayers[1].name)")
} else {
print("failed")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
as said above, both methods produce the same result
Also in a real life application you could do better error handling in the, as archiving & unarchiving could fail.
I have a class
class Player {
var name = ""
init(name: String) {
self.name = name
}
}
and i want to serialise it and save to user defaults.
In Swift 4 / iOS 11, there's a whole new way to do this. It has the advantage that any Swift object can use it — not just classes, but also structs and enums.
You'll notice that I've omitted your NSCoding-related methods, because you won't need them for this purpose. You can adopt NSCoding here, as you know; but you don't have to. (And a struct or enum cannot adopt NSCoding at all.)
You start by declaring your class as adopting the Codable protocol:
class Player : Codable {
var name = ""
init(name: String) {
self.name = name
}
}
It then becomes a simple matter to serialize it into a Data object (NSData) which can be stored in UserDefaults. The very simplest way is to use a property list as an intermediary:
let player = Player(name:"matt")
try? UserDefaults.standard.set(PropertyListEncoder().encode(player),
forKey:"player")
If you use that approach, let's now prove that you can pull the same Player back out of UserDefaults:
if let data = UserDefaults.standard.object(forKey:"player") as? Data {
if let p = try? PropertyListDecoder().decode(Player.self, from: data) {
print(p.name) // "matt"
}
}
If you'd rather pass through an NSKeyedArchiver / NSKeyedUnarchiver, you can do that instead. Indeed, there are some situations where you'll have to do so: you'll be presented with an NSCoder, and you'll need to encode your Codable object inside it. In a recent beta, Xcode 9 introduced a way to do that too. For example, if you're encoding, you cast the NSCoder down to an NSKeyedArchiver and call encodeEncodable.
With codable new ios 11 protocol you can now let your class implements it and archive/unarchive objects of it with JSONEncoder and JSONDecoder
struct Language: Codable {
var name: String
var version: Int
}
let swift = Language(name: "Swift", version: 4)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(swift) {
// save `encoded` somewhere
}
if let encoded = try? encoder.encode(swift) {
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Language.self, from: encoded) {
print(decoded.name)
}