NSKeyedUnarchiver error in Swift Core Data - ios

I have this core data issue:
*** -[NSKeyedUnarchiver decodeObjectForKey:]: value for key (id) is not an object. This will become an error in the future.
First I would like to describe what I am trying to do:
I perform a GET request to my API, get an array of elements and add the data I got to Core Data.
Here is my CoreData Entity:
extension PetEntity {
#nonobjc public class func fetchRequest() -> NSFetchRequest<PetEntity> {
return NSFetchRequest<PetEntity>(entityName: "PetEntity")
}
#NSManaged public var category: CategoryDTO?
#NSManaged public var id: Int64
#NSManaged public var name: String?
#NSManaged public var photoUrls: [String]?
#NSManaged public var status: String?
#NSManaged public var tags: [TagDTO]?
}
extension PetEntity : Identifiable {
}
My DTO I use to get Json data in and then map it to my model:
struct PetDTO: Codable {
var id: Int
var category: CategoryDTO?
var name: String?
var photoUrls: [String]?
var tags: [TagDTO]?
var status: StatusDTO
}
public class CategoryDTO: NSObject, Codable {
var id: Int
var name: String?
public enum CodingKeys: String, CodingKey {
case id
case name
}
public required init?(coder: NSCoder) {
id = coder.decodeObject(forKey: CodingKeys.id.rawValue) as? Int ?? 0 // error here
name = coder.decodeObject(forKey: CodingKeys.name.rawValue) as? String
}
}
extension CategoryDTO: NSSecureCoding{
public static var supportsSecureCoding: Bool{
return true
}
public func encode(with coder: NSCoder) {
coder.encode(id, forKey: CodingKeys.id.rawValue)
coder.encode(name, forKey: CodingKeys.name.rawValue)
}
}
#objc(CategoryDtoTransformer)
public final class CategoryDtoTransformer: NSSecureUnarchiveFromDataTransformer {
public static let name = NSValueTransformerName(rawValue: String(describing: CategoryDtoTransformer.self))
public override static var allowedTopLevelClasses: [AnyClass] {
return [CategoryDTO.self, NSString.self, NSArray.self]
}
#objc dynamic
public static func register() {
let transformer = CategoryDtoTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}
public class TagDTO: NSObject, Codable {
var id: Int
var name: String?
public enum CodingKeys: String, CodingKey {
case id
case name
}
public required init?(coder: NSCoder) {
id = coder.decodeObject(forKey: CodingKeys.id.rawValue) as? Int ?? 0 // Error here
name = coder.decodeObject(forKey: CodingKeys.name.rawValue) as? String
}
}
extension TagDTO: NSSecureCoding{
public static var supportsSecureCoding: Bool{
return true
}
public func encode(with coder: NSCoder) {
coder.encode(id, forKey: CodingKeys.id.rawValue)
coder.encode(name, forKey: CodingKeys.name.rawValue)
}
}
#objc(TagDtoTransformer)
public final class TagDtoTransformer: NSSecureUnarchiveFromDataTransformer {
public static let name = NSValueTransformerName(rawValue: String(describing: TagDtoTransformer.self))
public override static var allowedTopLevelClasses: [AnyClass] {
return [TagDTO.self, NSString.self, NSArray.self]
}
#objc dynamic
public static func register() {
let transformer = TagDtoTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}
enum StatusDTO: String, Codable {
case available
case sold
case pending
}
And here is my CRUD code for adding to database:
class CoreDataPetPersistance: PetPerstitenceProtocol {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func add(pet: PetDTO) {
let newPet = PetEntity(context: self.context)
newPet.id = Int64(pet.id)
newPet.name = pet.name
newPet.category = pet.category
newPet.photoUrls = pet.photoUrls
newPet.tags = pet.tags
newPet.status = pet.status.rawValue
do {
try self.context.save()
} catch {
print(error)
}
}
}
Also I have added constraints to my ID to make each pet I get from API unique and registered CategoryDTO and TagDTO in app delegate. It should be fine.
Full error I get:
CoreData: error: -executeRequest: encountered exception = Error Domain=NSCocoaErrorDomain Code=133000 "Attempt to access an object not found in store." UserInfo={objectID=0xa57351074a65fcb3 <x-coredata://FEB02FEE-9B8B-4785-82A1-263F82D4CDBC/PetEntity/p1095>} with userInfo = {
NSCocoaErrorDomain = 133000;
NSUnderlyingError = "Error Domain=NSCocoaErrorDomain Code=133000 \"Attempt to access an object not found in store.\" UserInfo={objectID=0xa57351074a65fcb3 <x-coredata://FEB02FEE-9B8B-4785-82A1-263F82D4CDBC/PetEntity/p1095>}";
}
Please help me fix this and explain why this error happens
Thanks

There are dedicated APIs for scalar types
id = coder.decodeInt64(forKey: CodingKeys.id.rawValue)
Have a look for all available APIs
To match the types declare id as Int64 or convert the type.

Related

How to test and mock property wrappers in Swift?

Let's say I have a very common use case for a property wrapper using UserDefaults.
#propertyWrapper
struct DefaultsStorage<Value> {
private let key: String
private let storage: UserDefaults
var wrappedValue: Value? {
get {
guard let value = storage.value(forKey: key) as? Value else {
return nil
}
return value
}
nonmutating set {
storage.setValue(newValue, forKey: key)
}
}
init(key: String, storage: UserDefaults = .standard) {
self.key = key
self.storage = storage
}
}
I am now declaring an object that would hold all my values stored in UserDefaults.
struct UserDefaultsStorage {
#DefaultsStorage(key: "userName")
var userName: String?
}
Now when I want to use it somewhere, let's say in a view model, I would have something like this.
final class ViewModel {
func getUserName() -> String? {
UserDefaultsStorage().userName
}
}
Few questions arise here.
It seems that I am obliged to use .standard user defaults in this case. How to test that view model using other/mocked instance of UserDefaults?
How to test that property wrapper using other/mocked instance of UserDefaults? Do I have to create a new type that is a clean copy of the above's DefaultsStorage, pass mocked UserDefaults and test that object?
struct TestUserDefaultsStorage {
#DefaultsStorage(key: "userName", storage: UserDefaults(suiteName: #file)!)
var userName: String?
}
As #mat already mentioned in the comments, you need a protocol to mock UserDefaults dependency. Something like this will do:
protocol UserDefaultsStorage {
func value(forKey key: String) -> Any?
func setValue(_ value: Any?, forKey key: String)
}
extension UserDefaults: UserDefaultsStorage {}
Then you can change your DefaultsStorage propertyWrapper to use a UserDefaultsStorage reference instead of UserDefaults:
#propertyWrapper
struct DefaultsStorage<Value> {
private let key: String
private let storage: UserDefaultsStorage
var wrappedValue: Value? {
get {
return storage.value(forKey: key) as? Value
}
nonmutating set {
storage.setValue(newValue, forKey: key)
}
}
init(key: String, storage: UserDefaultsStorage = UserDefaults.standard) {
self.key = key
self.storage = storage
}
}
After that a mock UserDefaultsStorage might look like this:
class UserDefaultsStorageMock: UserDefaultsStorage {
var values: [String: Any]
init(values: [String: Any] = [:]) {
self.values = values
}
func value(forKey key: String) -> Any? {
return values[key]
}
func setValue(_ value: Any?, forKey key: String) {
values[key] = value
}
}
And to test DefaultsStorage, pass an instance of UserDefaultsStorageMock as its storage parameter:
import XCTest
class DefaultsStorageTests: XCTestCase {
class TestUserDefaultsStorage {
#DefaultsStorage(
key: "userName",
storage: UserDefaultsStorageMock(values: ["userName": "TestUsername"])
)
var userName: String?
}
func test_userName() {
let testUserDefaultsStorage = TestUserDefaultsStorage()
XCTAssertEqual(testUserDefaultsStorage.userName, "TestUsername")
}
}
This might not be the best solution, however, I haven't figured out a way to inject UserDefaults that use property wrappers into a ViewModel. If there is such an option, then gcharita's proposal to use another protocol would be a good one to implement.
I used the same UserDefaults in the test class as in the ViewModel. I save the original values before each test and restore them after each test.
class ViewModelTests: XCTestCase {
private lazy var userDefaults = newUserDefaults()
private var preTestsInitialValues: PreTestsInitialValues!
override func setUpWithError() throws {
savePreTestUserDefaults()
}
override func tearDownWithError() throws {
restoreUserDefaults()
}
private func newUserDefaults() -> UserDefaults.Type {
return UserDefaults.self
}
private func savePreTestUserDefaults() {
preTestsInitialValues = PreTestsInitialValues(userName: userDefaults.userName)
}
private func restoreUserDefaults() {
userDefaults.userName = preTestsInitialValues.userName
}
func testUsername() throws {
//"inject" User Defaults with the desired values
let username = "No one"
userDefaults.userName = username
let viewModel = ViewModel()
let usernameFromViewModel = viewModel.getUserName()
XCTAssertEqual(username, usernameFromViewModel)
}
}
struct PreTestsInitialValues {
let userName: String?
}

Assign value related objects core data

I have two entities
#objc(Movies)
public class Movies: NSManagedObject {
}
extension Movies {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Movies> {
return NSFetchRequest<Movies>(entityName: "Movies")
}
#NSManaged public var id: Int64
#NSManaged public var isFav: Bool
#NSManaged public var overview: String?
#NSManaged public var poster: String?
#NSManaged public var release_date: String?
#NSManaged public var title: String?
#NSManaged public var genresID: NSSet?
}
// MARK: Generated accessors for genresID
extension Movies {
#objc(addGenresIDObject:)
#NSManaged public func addToGenresID(_ value: GenresID)
#objc(removeGenresIDObject:)
#NSManaged public func removeFromGenresID(_ value: GenresID)
#objc(addGenresID:)
#NSManaged public func addToGenresID(_ values: NSSet)
#objc(removeGenresID:)
#NSManaged public func removeFromGenresID(_ values: NSSet)
}
#objc(GenresID)
public class GenresID: NSManagedObject {
}
extension GenresID {
#nonobjc public class func fetchRequest() -> NSFetchRequest<GenresID> {
return NSFetchRequest<GenresID>(entityName: "GenresID")
}
#NSManaged public var id: Int64
#NSManaged public var name: String?
#NSManaged public var movieID: Movies?
}
When I click a button an action is triggered to save a movie, and then I would like to save that movie that has been "favored". A movie can have multiples genres (one-to-many relationship).
Method action:
func saveMoviesDB (movie: Movie) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let entityMovie = NSEntityDescription.entity(forEntityName: "Movies", in: managedContext)!
let entityGenres = NSEntityDescription.entity(forEntityName: "GenresID", in: managedContext)!
let movieDB = NSManagedObject(entity: entityMovie, insertInto: managedContext)
let genresDB = NSManagedObject(entity: entityGenres, insertInto: managedContext)
movieDB.setValue(movie.id, forKey: "id")
movieDB.setValue(movie.title, forKey: "title")
movieDB.setValue(movie.isFav, forKey: "isFav")
movieDB.setValue(movie.poster, forKey: "poster")
movieDB.setValue(movie.overview, forKey: "overview")
movieDB.setValue(movie.releaseDate, forKey: "release_date")
do{
try managedContext.save()
moviesDB.append(movieDB)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
I don't know how to assign multiple genresID to the Movie. For each movie, there is an Int array containing the Ids of the genres.
EDIT: I decided to remove the genre entity and create a transformable type property in Movies to store the arrays of Int. It went like this:
extension Movies {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Movies> {
return NSFetchRequest<Movies>(entityName: "Movies")
}
#NSManaged public var id: Int64
#NSManaged public var isFav: Bool
#NSManaged public var overview: String?
#NSManaged public var poster: String?
#NSManaged public var release_date: String?
#NSManaged public var title: String?
#NSManaged public var genresID: [NSNumber]?
}
And then I cast it before saving it to the database
let genresID = movie.genre as [NSNumber]

Swift Realm migration create reference from old type to new one

Initially I had the following classes:
#objcMembers public class NormalObjectRealm: Object {
// Shared
dynamic public var id: String?
dynamic public var title: String?
dynamic public var subTitle: String?
dynamic public var imageInfo: ImageInfoRealm?
dynamic public var descriptionString: String?
public var categories = List<String>()
public var count = RealmOptional<Int>()
public var episodes = List<String>()
public static let realmPrimaryKey: String = "id"
public override class func primaryKey() -> String? {
return NormalObjectRealm.realmPrimaryKey
}
}
#objcMembers public class ImageInfoRealm: Object {
dynamic public var id: String?
dynamic public var url: String?
public static let realmPrimaryKey: String = "id"
public override class func primaryKey() -> String? {
return ImageInfoRealm.realmPrimaryKey
}
}
but now NormalObjectRealm is kind of incorporated into a new class like so:
#objcMembers public class MediaObjectRealm: Object {
// Shared
dynamic public var id: String?
dynamic public var title: String?
dynamic public var subTitle: String?
dynamic public var imageInfo: ImageInfoRealm?
dynamic public var descriptionString: String?
public var categories = List<String>()
dynamic public var type: String?
// NormalObjectRealm
public var episodeCount = RealmOptional<Int>()
public var episodes = List<String>()
// OtherObjectRealm
dynamic public var urlOne: String?
dynamic public var urlTwo: String?
dynamic public var urlThree: String?
public var isExplicit = RealmOptional<Bool>()
public static let realmPrimaryKey: String = "id"
public override class func primaryKey() -> String? {
return MediaObjectRealm.realmPrimaryKey
}
}
I'm currently trying to write the migration for the transition here where the idea basically is to transfer most of the fields over from NormalObjectRealm to MediaObjectRealm.
This is what my migration-block currently looks like
Realm.Configuration(schemaVersion: schemaVersion, migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < temp {
print("RealmMigration: Applying migration from \(oldSchemaVersion) to \(temp)")
migration.enumerateObjects(ofType: "NormalObjectRealm") { oldObject, newObject in
guard let oldObject = oldObject else {
return
}
guard let id = oldObject["id"] as? String else {
return
}
guard let title = oldObject["title"] as? String else {
return
}
guard let subTitle = oldObject["subTitle"] as? String else {
return
}
guard let imgInfo = oldObject["imageInfo"] else {
return
}
guard let count = oldObject["count"] as? RealmOptional<Int>? else {
return
}
guard let descriptionString = oldObject["descriptionString"] as? String? else {
return
}
let item = migration.create("MediaObjectRealm")
item["id"] = id
item["title"] = title
item["subTitle"] = subTitle
item["descriptionString"] = descriptionString
item["type"] = "myType"
item["episodeCount"] = episodeCount // Doesn't work either...
migration.enumerateObjects(ofType: "ImageInfoRealm") { oldImg, newImg in
guard let oldImg = oldImg else {
return
}
let inf = oldObject.value(forKey: "imageInfo")
print(inf)
let t = migration.create("ImageInfoRealm", value: inf)
print("doing it")
// print(t)
item.setValue(t, forKey: "imageInfo")
}
}
}
})
id, title, subTitle etc. (String? and Date? variables) are set fine and appear inside the newly created MediaObjectRealm DB-Entries. However imageInfo of type ImageInfoRealm does not... setting it directly like so: item.setValue(oldObject.value(forKey: "imageInfo"), forKey: "imageInfo") (or item["imageInfo"] = oldObject.value(forKey: "imageInfo")) results in realm crashing and telling me that this object is from another realm and I have to copy it over.
'Object is already managed by another Realm. Use create instead to
copy it into this Realm.'
Creating it like in the code above results in not even having any items of type MediaObjectRealm at all i.e. loosing all the data (as NormalObjectRealm is also not present anymore).
Am I missing something? What I basically want is to to take the link/reference from the NormalObjectRealm and copy it to the new MediaObjectRealm.
After long testing and trying different possibilities I managed to migrate the data.
Here is what I did to accomplish this.
I used this as a base:
class RealmMigrationObject {
let migration: () -> ()
init(migration: #escaping () -> ()) {
self.migration = migration
}
}
and derived classes from that. Something like:
class MigrationObjectToThree: RealmMigrationObject {
init() {
super.init(migration: MigrationObjectToThree.migration)
}
private static func migration() {
print("Migration to three | migration")
var imageInfos: [ImageInfo] = []
let config = Realm.Configuration(schemaVersion: 3, migrationBlock: { migration, oldSchemaVersion in
print("Migration to three | migrationBlock")
print("RealmMigration: Applying migration from \(oldSchemaVersion) to 3")
migration.deleteData(forType: "ExploreSectionObjectRealm")
migration.enumerateObjects(ofType: "ImageInfoRealm") { oldInfo, newObject in
guard let oldInfo = oldInfo else {
return
}
guard let id = oldInfo["id"] as? String,
let url = oldInfo["url"] as? String,
let url500 = oldInfo["url500"] as? String,
let url400 = oldInfo["url400"] as? String,
let url300 = oldInfo["url300"] as? String,
let url200 = oldInfo["url200"] as? String,
let url100 = oldInfo["url100"] as? String,
let colorString = oldInfo["color"] as? String,
let color = UIColor(hexString: colorString) else {
return
}
imageInfos.append(ImageInfo(id: id,
url: url,
url500: url500,
url400: url400,
url300: url300,
url200: url200,
url100: url100,
color: color))
}
})
Realm.Configuration.defaultConfiguration = config
do {
let realm = try Realm(configuration: config)
print("Realm is located at: \(realm.configuration.fileURL?.description ?? "")")
print(realm.configuration.fileURL?.description ?? "") // Printing here on purpose as it's easier to copy
} catch {
print("Realm Error: \(error), trying to rebuild realm from scratch")
let deleteMigrationConfig = Realm.Configuration(schemaVersion: RealmHelper.schemaVersion,
deleteRealmIfMigrationNeeded: true)
do {
_ = try Realm(configuration: deleteMigrationConfig)
} catch {
print("Failed to instantiate: \(error.localizedDescription)")
}
}
RealmHelper.removeRealmFiles()
Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 3)
imageInfos.forEach({ $0.save() })
}
}
From that I just created all migration for the difference between the current schema version and target schema version on looped over all migrations simply executing the migration function of that given object.

Can't save custom class array to UserDefaults

I'm trying to save a custom class array to UserDefaults but it doesn't work. I get nil back on if let. I looked everywhere online. I'm using Swift 4.2
extension UserDefaults {
func saveReciters(_ reciters: [Reciter]) {
do {
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: reciters, requiringSecureCoding: false)
self.set(encodedData, forKey: UD_RECITERS)
} catch {
debugPrint(error)
return
}
}
func getReciters() -> [Reciter] {
if let reciters = self.object(forKey: UD_RECITERS) as? Data {
return NSKeyedUnarchiver.unarchiveObject(with: reciters) as! [Reciter]
} else {
print("EMPTY RECITERS")
return [Reciter]()
}
}
}
UserInfo={NSDebugDescription=Caught exception during archival: -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x600001babcc0
Thats my class:
class Reciter: NSCoding {
private(set) public var name: String
private(set) public var image: UIImage?
private(set) public var surahs: [Surah]
private(set) public var documentID: String
private let quranData = QuranData()
init(name: String, image: UIImage?, surahCount: Int?, documentID: String) {
self.name = name
self.image = image
self.documentID = documentID
if let surahCount = surahCount {
surahs = Array(quranData.getAllSurahs().prefix(surahCount))
} else {
surahs = quranData.getAllSurahs()
}
}
func encode(with aCoder: NSCoder) {
}
required init?(coder aDecoder: NSCoder) {
}
}
On my Surah class i get nil back. All other properties i get back succesfully
Most often I see developer's use codeable, here I am using user as an example:
YourDataModel.swift
struct User: Codable {
var userId: String = ""
var name: String = ""
var profileImageData: Data? }
UserDefaults.swift
import Foundation
extension UserDefaults {
/// The current user of the application, see `./Models/User.swift`
var currentUser: User? {
get {
guard let userData = self.object(forKey: #function) as? Data else { return nil }
return try? JSONDecoder().decode(User.self, from: userData)
}
set {
guard let newuser = newValue else { return }
if let userData = try? JSONEncoder().encode(newuser) {
self.set(userData, forKey: #function)
}
}
}
}
Transform the data into json data... #function is the function or value name i.e.
// For the case the user doesn't yet exist.
if ( UserDefaults.standard.currentUser == nil ) {
// Create a new user
user = User()
// Generate an id for the user, using a uuid.
user?.userId = UUID().uuidString
} else {
// otherwise, fetch the user from user defaults.
user = UserDefaults.standard.currentUser
}

Parse PFSubclassing in Swift of Object type

I'm pretty new to iOS/Swift/Parse and I'm trying to build a model of a class using PFSubclassing.
The data I'm trying to represent should look something like this
{
text: ""
location : {
name: "",
longitude: "",
latitude: ""
}
}
So fare the model I'm have is
class LocationModel {
var name: String?
var longitude: Float?
var latitude: Float?
}
class PostModel: PFObject, PFSubclassing {
class func parseClassName() -> String! {
return "Post"
}
#NSManaged var text: String?
var location: LocationModel?
}
The test property is being saved successfully but I'm unable to get the location properties to save.
The code I'm using to save a record to parse is
var test = PostModel()
test.location?.name = "ESB"
test.location?.latitude = 1
test.location?.longitude = 1
test.text = "This is a test post to see if this works!"
test.saveEventually { (success: Bool, error: NSError!) -> Void in
println(error)
println(success)
}
I did a lot of digging online but I'm unable to find a solution on how to represent an Object datatype in Swift using Parse PFSubclassing
Any help would be greatly appreciated.
Thank you
Here's my solution:
I will create a Hero object for example.
class Hero: PFObject, PFSubclassing {
#NSManaged var strengthPoint: Double
#NSManaged var name: String
static func parseClassName() -> String {
return "Hero"
}
init(strengthPoint: Double, name: String) {
super.init()
self.strengthPoint = strengthPoint
self.name = name
}
init(pfObject: PFObject) {
super.init()
self.strengthPoint = pfObject.object(forKey: "strengthPoint") as! Double
self.name = pfObject.object(forKey: "name") as! String
}
override init() {
super.init()
}
override class func query() -> PFQuery<PFObject>? {
let query = PFQuery(className: self.parseClassName())
query.order(byDescending: "createdAt")
query.cachePolicy = .networkOnly
return query
}
}
Now, after defining your model, you can use these methods to store and retrieve
Create your object in server
func createHero() {
let hero = Hero(strengthPoint: 2.5, name: "Superman")
hero.saveInBackground { (isSuccessful, error) in
print(error?.localizedDescription ?? "Success")
}
}
Retrieve object from server
func retrieveHero() {
let query = Hero.query()
query?.getFirstObjectInBackground(block: { (object, error) in
if error != nil {
print(error?.localizedDescription ?? "Something's wrong here")
} else if let object = object {
let hero = Hero(pfObject: object)
print(hero.strengthPoint) // 2.5
print(hero.name) // Superman
}
})
}
I have seen several different methods for PFSubclassing in Swift 1.2, but the following works best for me:
To begin with, make sure that you have the following in your Objective-C Bridging Header:
#import <Parse/PFObject+Subclass.h>
Here is a very basic example of subclassing PFObject:
import Foundation
import Parse
class Car: PFObject, PFSubclassing {
override class func initialize() {
self.registerSubclass()
}
static func parseClassName() -> String {
return "Car"
}
#NSManaged var model: String
#NSManaged var color: String
#NSManaged var yearManufactured: Int
}
So in your case, this would be:
class PostModel: PFObject, PFSubclassing {
override class func initialize() {
self.registerSubclass()
}
static func parseClassName() -> String {
return "Post"
}
#NSManaged var text: String?
}
Concerning your LocationModel...I'm a bit confused as to what exactly you are trying to accomplish with that. I hope this helps.

Resources