I have my file myAPI.swift, and two objet Round and GameStats. My Round object also have an attribute GameStats. So what I want to do is to get the GameStats property that I store inside my users defaults then assign it inside my Round object.
class myAPI: NSObject {
static let sharedInstance = myAPI()
var currentStats: GameStats?
var currentRound: Round?
private init(){
super.init()
self.loadData()
NSLog("Stats have been reload: \(self.currentRound?.gameStats)") // Return nil
// If I try to add this line the app stop running and nothing happens
NSLog("Test Singleton: \(myApp.sharedInstance.currentRound?.gameStats)")
}
func loadData(){
let backupNSData = NSUserDefaults.standardUserDefaults().objectForKey("backupNSData")
if let backupNSData = backupNSData as? NSData{
let backupData = NSKeyedUnarchiver.unarchiveObjectWithData(backupNSData)
if let backupData = backupData as? [String:AnyObject] {
guard let round = backupData["currentRound"] as? Round else {
print("error round loaddata")
return
}
self.currentRound = round
guard let stats = backupData["stats"] as? GameStats else {
print("error guard stats")
return
}
self.currentRound.gameStats = stats
NSLog("Stats reloaded: \(stats)") // This is not nil it works here
}
}
}
When my app crash I call this function to save the data
func backupData(){
var backupData:[String:AnyObject] = [String:AnyObject]()
if let round = self.currentRound {
backupData["currentRound"] = round
ColorLog.purple("Stats saved inside Round \(round.gameStats)")
}
if let stats = self.currentStat {
backupData["stats"] = stats
ColorLog.purple("Stats saved : \(stats)")
}
let backupNSData = NSKeyedArchiver.archivedDataWithRootObject(backupData)
NSUserDefaults.standardUserDefaults().setObject(backupNSData, forKey: "backupNSData")
NSUserDefaults.standardUserDefaults().synchronize()
}
So I have two question,
Is it normal that I can't call my singleton like myApp.sharedInstance.currentRound.id = 5 (for instance) inside the init() (I guess it is but I didn't find anything about that)
Why in my init() method my first NSLog self.currentRound?.gameStats is nil when in the function loadData() it wasn't ? It seems like it's losing its reference since we are leaving the function.
What am I doing right now is adding a currentStats property in my singleton, then when I retrieve data instead of doing self.currentRound.gameStats = stats I do self.currentStats = stats, then self.currentRoud.gameStats = self.currentStats and If I do that it works, I don't really know If I am doing the things right here.
Also my two objects Round and GameStats conform to NSCoding protocol as I implemented #objc func encodeWithCoder and #objc required init?(coder aDecoder: NSCoder) methods for both of them.
Thank for you help.
Related
I have data that I want to read from disk into memory that takes a nontrivial amount of time.
I want to be able to do two things:
I don't the data to be read every time the view loads.
I want to be able to invoke it from another view.
lazy var data: [String: String] = {
guard let data = readFromDisk() else { return [:] }
return processData(data: data)
}()
Above code gets initialized only once when the view loads for the first time, which is perfect for eliminating unnecessary computation. The problem is I also want to be able to trigger it from another view when needed.
I tried to trigger re-initialization:
func getData() {
guard let data = readFromDisk() else { return [:] }
data = processData(data: data)
}
and invoke it from another view:
let vc = ViewController()
vc.getData()
but, doesn't work.
I tried to see if I could use static since it's also lazy, but I get an error saying:
Instance member cannot be used on type 'ViewController'
Finally, I tried creating a separate class:
class DataImporter {
var data: [String: String] {
guard let data = readFromDisk() else { return [:] }
return processData(data: data)
}
func readFromDisk() -> [String: String] {}
func processData(data: [String: String]) -> [String: String] {}
}
and have the lazy property in ViewController:
lazy var importer = DataImporter()
thinking that instantiating a class achieves the dual effect of taking advantage of a lazy property and invoking it when needed:
let vc = ViewController()
vc.importer = DataImporter()
This instantiates the class about a hundred times for some reason which is not ideal.
I would suggest creating a function that loads the data into data and then whenever you need to reload data, simply reassign it.
class DataStore {
lazy var data: [String: String] = loadData()
func readFromDisk() -> Data? {...}
func processData(data: Data) -> [String:String] { ... }
func loadData() -> [String:String] {
guard let data = readFromDisk() else { return [:] }
return processData(data: data)
}
}
let store = DataStore()
let data = store.data // only loaded here
store.data = store.loadData() // reloads the data
If you don't want the loadData function to be exposed, you can also create a separate reloadData function.
class DataStore {
...
func reloadData() {
data = loadData()
}
}
and then instead of doing store.data = store.loadData(), simply call store.reloadData()
My current class has around 50 lines just encoding and decoding variables in order for my class to be NSUserDefaults compatible. Is there a better way to handle this?
Example:
init(coder aDecoder: NSCoder!) {
lightEnabled = aDecoder.decodeBoolForKey("lightEnabled")
soundEnabled = aDecoder.decodeBoolForKey("soundEnabled")
vibrateEnabled = aDecoder.decodeBoolForKey("vibrateEnabled")
pulseEnabled = aDecoder.decodeBoolForKey("pulseEnabled")
songs = aDecoder.decodeObjectForKey("songs") as! [Song]
currentSong = aDecoder.decodeIntegerForKey("currentSong")
enableBackgroundSound = aDecoder.decodeBoolForKey("enableBackgroundSound")
mixSound = aDecoder.decodeBoolForKey("mixSound")
playSoundInBackground = aDecoder.decodeBoolForKey("playSoundInBackground")
duckSounds = aDecoder.decodeBoolForKey("duckSounds")
BPMBackground = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("BPMBackgorund") as! NSData) as! UIColor!
BPMPulseColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("BPMPulseColor") as! NSData) as! UIColor!
TempoBackGround = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TempoBackGround") as! NSData) as! UIColor!
TempoPulseColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TempoPulseColor") as! NSData) as! UIColor!
TimeBackGround = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TimeBackGround") as! NSData) as! UIColor!
TimeStrokeColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TimeStrokeColor") as! NSData) as! UIColor!
TextColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TextColor") as! NSData) as! UIColor!
}
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeBool(lightEnabled, forKey: "lightEnabled")
aCoder.encodeBool(soundEnabled, forKey: "soundEnabled")
aCoder.encodeBool(vibrateEnabled, forKey: "vibrateEnabled")
aCoder.encodeBool(pulseEnabled, forKey: "pulseEnabled")
aCoder.encodeObject(songs, forKey: "songs")
aCoder.encodeInteger(currentSong, forKey: "currentSong")
aCoder.encodeBool(enableBackgroundSound, forKey: "enableBackgroundSound")
aCoder.encodeBool(mixSound, forKey: "mixSound")
aCoder.encodeBool(playSoundInBackground, forKey: "playSoundInBackground")
aCoder.encodeBool(duckSounds, forKey: "duckSounds")
aCoder.encodeObject(BPMBackground.archivedData(), forKey: "BPMBackground")
aCoder.encodeObject(BPMPulseColor.archivedData(), forKey: "BPMPulseColor")
aCoder.encodeObject(TempoBackGround.archivedData(), forKey: "TempoBackGround")
aCoder.encodeObject(TempoPulseColor.archivedData(), forKey: "TempoPulseColor")
aCoder.encodeObject(TimeBackGround.archivedData(), forKey: "TimeBackGround")
aCoder.encodeObject(TimeStrokeColor.archivedData(), forKey: "TimeStrokeColor")
aCoder.encodeObject(TextColor.archivedData(), forKey: "TextColor")
}
You should create a struct or enum to organise your keys, because your way is just prone to typos. Just put it right above your class
enum Key: String {
case allSettings
case lightEnabled
case soundEnabled
}
and than just call the keys like so
...forKey: Key.lightEnabled.rawValue)
Now in regards to your question, I was facing the same issue with my game trying to save properties for 40 levels (bestTimes, Level unlock status etc). I initially did what you tried and it was pure madness.
I ended up using arrays/dictionaries or even arrays of dictionaries for my data which cut down my code by like 80 percent.
Whats also nice about this is that say you need to save something like LevelUnlock bools, it will make your life so much easier later on. In my case I have a UnlockAllLevels button, and now I can just loop trough my dictionary/array and update/check the levelUnlock bools in a few lines of code. So much better than having massive if-else or switch statements to check each property individually.
For example
var settingsDict = [
Key.lightEnabled.rawValue: false,
Key.soundEnabled.rawValue: false,
...
]
Than in the decoder method you say this
Note: This way will take into account that you might add new values to the SettingsDict and than on the next app launch those values will not be removed because you are not replacing the whole dictionary with the saved one, you only update the values that already exist.
// If no saved data found do nothing
if var savedSettingsDict = decoder.decodeObjectForKey(Key.allSettings.rawValue) as? [String: Bool] {
// Update the dictionary values with the previously saved values
savedSettingsDict.forEach {
// If the key does not exist anymore remove it from saved data.
guard settingsDict.keys.contains($0) else {
savedSettingsDict.removeValue(forKey: $0)
return
}
settingsDict[$0] = $1
}
}
If you use multiple dictionaries than your decoder method will become a messy again and you will also repeat alot of code. To avoid this you can create an extension of NSCoder using generics.
extension NSCoder {
func decodeObject<T>(_ object: [String: T], forKey key: String) -> [String: T] {
guard var savedData = decodeObject(forKey: key) as? [String: T] else { return object }
var newData = object
savedData.forEach {
guard object.keys.contains($0) else {
savedData[$0] = nil
return
}
newData[$0] = $1
}
return newData
}
}
and than you can write this in the decoder method for each dictionary.
settingsDict = aDecoder.decodeObject(settingsDict, forKey: Key.allSettings.rawValue)
Your encoder method would look like this.
encoder.encodeObject(settingsDict, forKey: Key.allSettings.rawValue)
In your game/app you you can use them like so
settingsDict[Key.lightEnabled.rawValue] = true
if settingsDict[Key.lightEnabled.rawValue] == true {
/// light is turned on, do something
}
This way makes it also very easy to integrate iCloud KeyValue storage (just create an iCloud dictionary), again mainly because its so easy to save and compare a lot of values with very little code.
UPDATE:
To make calling these a bit easier I like to create some convenience getters/setters in the GameData class. This has the benefit that you can more easily call these properties in your project (like your old way) but your encode/decode method will still stay compact. You can also still do things such as looping to compare values.
var isLightEnabled: Bool {
get { return settingsDict[Key.lightEnabled.rawValue] ?? false }
set { settingsDict[Key.lightEnabled.rawValue] = newValue }
}
var isSoundEnabled: Bool {
get { return settingsDict[Key.soundEnabled.rawValue] ?? false }
set { settingsDict[Key.soundEnabled.rawValue] = newValue }
}
and than you can call them like normal properties.
isLightEnabled = true
if isLightEnabled {
/// light is turned on, do something
}
Look at protocol codeable in Swift 4.
The decoder and encoder will be auto-generated for you.
Check out: (starting about half way through)
https://developer.apple.com/videos/play/wwdc2017/212/
I created an UIViewController which has two UITexFields and an UIImageView inside of it.
What I want it to be is a profile page, which provides information usable everywhere in the app.
What I tried to do is the following:
I created a Class with this code (based on Apple's tutorial on creating apps):
import UIKit
Class ProfilClass: NSObject, NSCoding {
//MARK: Properties
var bild: UIImage?
var vorname: String
var geburt: String
//MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("profil")
//MARK: Types
struct propKey {
static let bildKey = "bild"
static let vornameKey = "vorname"
static let geburtKey = "geburt"
}
//MARK: Initialization
init?(bild: UIImage?, vorname: String, geburt: String){
self.bild = bild
self.vorname = vorname
self.geburt = geburt
super.init()
if vorname.isEmpty || geburt.isEmpty {
return nil
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(bild, forKey: propKey.bildKey)
aCoder.encodeObject(vorname, forKey: propKey.vornameKey)
aCoder.encodeObject(geburt, forKey: propKey.geburtKey)
}
required convenience init?(coder aDecoder: NSCoder){
let bild = aDecoder.decodeObjectForKey(propKey.bildKey) as? UIImage
let vorname = aDecoder.decodeObjectForKey(propKey.vornameKey) as! String
let geburt = aDecoder.decodeObjectForKey(propKey.geburtKey) as! String
self.init(bild: bild, vorname: vorname, geburt: geburt)
}
}
I try to use this Class inside of my UIViewController:
override func viewDidLoad() {
[...]
if let profil = profil{
vornameLabel.text = profil.vorname
vornameField.text = profil.vorname
profilePic.image = profil.bild
geburtstagsLabel.text = profil.geburt
geburtstagField.text = profil.geburt
}
}
And when a save button is tapped:
#IBAction func butTap(sender: UIBarButtonItem) {
let vorname = vornameField.text
let geburt = geburtstagField.text
let bild = profilePic.image
profil = ProfilClass(bild: bild, vorname: vorname!, geburt: geburt!)
}
But after I close the UIViewController by going back to another one and reopen it, all the information is lost.
I don't know how to get the information again (I assume it is saved somewhere).
Can anyone help me?
public class MyViewState : NSObject{
static var isFromLogin = false
static var isFromCQ = false
}
add above to anywhere in any view controller it will accessible everywhere like
MyViewState.isFromLogin
make a shared class like following this will save data all the time
Class ProfilClass: NSObject, NSCoding {
static let sharedInstance = ProfilClass()
// here methods etc will go
}
and
ProfilClass.sharedInstance.(properties or method)
If you want data after app relaunched too , then save this to Userdefaults and load this class again and access anywhere in the app
Your mistake comes right at the end:
I assume it is saved somewhere
It is not, unless you ask for it to be saved. You need to use NSUserDefaults or NSKeyedArchiver to write your object. You've written all the code required to make that work, now you just need to do the reading and writing.
For example, to write your saved data you'll need something like this:
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(profil, forKey: "SavedUserProfile")
I can't comment on reading because you have limited the code you posted:
[...]
if let profil = profil{
That is where your reading code should happen. I'm guessing(!) you're doing something like this:
let defaults = NSUserDefaults.standardUserDefaults()
if let profil = defaults.objectForKey("SavedUserProfile") as? ProfilClass
As you're following a specific Apple tutorial: You might find it easier just to continue following there rather than trying to use a different solution. Specifically, you need to continue onto the heading "Save and Load the Meal List", which is where the actual saving and loading happens. Apple writes the data to disk rather than user defaults, so you should follow along.
To be clear: the code you've written only enables your object to be saved and loaded. It doesn't actually do the saving.
var isFromLogin = false
var isFromCQ = false
Class ProfilClass: NSObject, NSCoding {.......}
add above to class it will accessible everywhere publicly . OR also create .swift file and declare constant variable to access globally
I am a novice programmer who has started to learn Swift to make apps.
So my question is: "How can I store an array of my objects using NSUserDefaults". Originally, I was looking at this question posted on stack overflow. However, I don't really understand exactly how they did it. After searching on youtube with really no success, I have no choice but ask a question on stack overflow. (I'm hesitant because people often vote my questions off as I'm a beginner asking stupid questions)
So here goes.
I am making a todo list using swift. I have one main class called Task Manager that contains an array of Task objects.
Task Manager (Functionality)
Contains the array of tasks
Saves and Loads data using NSUserDefaults in order to store the tasks
Task (Functionality)
Stores the name and description of a task
Not sure if it should have anything else
So now, after looking at several online tutorials, here is what i have. (It doesn't work)
Task Manager: Raw Code
//
// TaskManager.swift
// ToDoList
//
// Created by Ronak Shah on 7/5/15.
// Copyright (c) 2015 com.ShahIndustries. All rights reserved.
//
import UIKit
var taskMgr : TaskManager = TaskManager()
class TaskManager: NSObject{
var tasks: [Task]? = [Task]()
var time = 1 //debug, ignore
override init(){
super.init()
let taskData = NSUserDefaults.standardUserDefaults().objectForKey("tasks") as? NSData
if let taskData = taskData {
let taskArray = NSKeyedUnarchiver.unarchiveObjectWithData(taskData) as? [Task]
if let taskArray = taskArray {
tasks = taskArray
}
}
}
func addTask(taskName : String, taskDescription : String){
var newTask = Task(taskName: taskName , taskDescription: taskDescription)
tasks!.append(newTask)
}
func getTaskAtIndex(index: Int) ->Task {
return tasks![index]
}
func saveData() {
let ud = NSUserDefaults.standardUserDefaults()
ud.setObject(NSKeyedArchiver.archivedDataWithRootObject(taskMgr), forKey: "tasks")
}
required init(coder aDecoder: NSCoder) {
if let dataTasks = aDecoder.decodeObjectForKey("tasks") as? [Task] {
self.tasks = dataTasks
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let tasks = self.tasks {
aCoder.encodeObject(tasks, forKey: "tasks")
}
}
func loadData() {
let ud = NSUserDefaults.standardUserDefaults()
if let data = ud.objectForKey("tasks") as? NSData {
//not sure how to get the data
}
}
}
Task: Raw Code
//
// Task.swift
// ToDoList
//
// Created by Ronak Shah on 7/5/15.
// Copyright (c) 2015 com.ShahIndustries. All rights reserved.
//
import Foundation
class Task :NSObject{
var name : String?
var desc : String?
init (taskName : String, taskDescription : String){
self.name = taskName
self.desc = taskDescription
}
required init(coder aDecoder: NSCoder) {
if let dataDesc = aDecoder.decodeObjectForKey("desc") as? String {
self.desc = dataDesc
}
if let dataTitle = aDecoder.decodeObjectForKey("name") as? String {
self.name = dataTitle
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let name = self.name {
aCoder.encodeObject(name, forKey: "name")
}
if let desc = self.desc {
aCoder.encodeObject(desc, forKey: "desc")
}
}
}
So how might I store my array of tasks (named tasks in Task Manager)?
The docs say:
For NSArray and NSDictionary objects, their contents must be property list objects
There's NSData among the property list types. In order to convert you custom object to NSData you have to serialize it.
You are trying to implement the archiving step on Task and TaskManager objects which adds complexity. The easiest way to achieve your goal is to archive (convert to NSData) just Task objects and add them to an array before saving to the defaults. In order to do that you have to convert the tasks array of type [Task] to an array of type [NSData] It would look like this.
func saveData() {
let tasksAsData = tasks.map { task in
return NSKeyedArchiver.archivedDataWithRootObject(task)
}
let ud = NSUserDefaults.standardUserDefaults()
ud.setObject(tasksAsData, forKey: "tasks")
}
When you get the data back you have to convert NSData objects into Tasks. You can do it in the following way.
func loadData() {
let ud = NSUserDefaults.standardUserDefaults()
if let data = ud.objectForKey("tasks") as? [NSData] {
tasks = data.map { data in
return NSKeyedUnarchiver.unarchiveObjectWithData(data)
}
}
}
There you go! I think you can take it from here.
As a side note I am advising not to use NSUserDefaults for such purpose. The defaults are meant as a simple way of saving little pieces of (usually) unstructured data. When you add more properties to your tasks it is likely to get cumbersome (you'll probably serialize things like image, date, or location). It is not space efficient nor is it fast. I advise to take a look at other ways to persist data on iOS like SQLite, Core Data, Realm or many others.
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.