Fr4nc3sc0NL
Fr4nc3sc0NL

Reputation: 575

store Array containing CGPoints in CoreData database (Swift)

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

Upvotes: 0

Views: 1733

Answers (5)

anorskdev
anorskdev

Reputation: 1925

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.

CoreData Scribble Definition

Upvotes: 0

anorskdev
anorskdev

Reputation: 1925

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.

Upvotes: 0

Fabian
Fabian

Reputation: 5348

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.

Upvotes: 1

Fr4nc3sc0NL
Fr4nc3sc0NL

Reputation: 575

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/

Upvotes: 0

William Hu
William Hu

Reputation: 16141

1) add a "Transformable" type attribute.

enter image description here

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)")
 } 

Upvotes: 0

Related Questions