I have one app with multiple targets (each target is for another client as separate application with different name, bundle identifier etc).
I have method:
fileprivate static func loadSessionFromKeychain() -> UserSession? {
if let sessionData = KeychainWrapper.standard.data(forKey: UserSession.sessionDefaultsKey) {
print("sessionData:")
print(sessionData.debugDescription)
if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession {
_current = session
return session
} else {
print("ERROR: Could not parse UserSession from Keychain")
}
return nil
}
return nil
}
The line if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession { throws error:
* Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*
-[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_test__MyApp.UserSession) for key (root); the class may be
defined in source code or a library that is not linked'
I've tried to catch do {} catch {} but it didn't catch and throws still the same error + xCode says
catch' block is unreachable because no errors are thrown in 'do' block`
Any ideas how to fix this?
UserSessionSwift.swift
import UIKit
import SwiftKeychainWrapper
class UserSession: NSObject, NSCoding {
// Static vars
fileprivate static var _current: UserSession?
static var current: UserSession? {
get {
// If there is already a session return it
if _current != nil {
return _current
}
// If there is no session yet but one is persistently stored return it
if let session = self.persistentLoadCurrentSession() {
_current = session
self.persistentLoadCookies()
return session
}
// Otherwise return nil
return nil
}
set(value) {
// Store the actual value
_current = value
// Perform hooks after changing the current session
if value == nil {
self.afterLogout()
} else {
self.afterLogin()
}
}
}
// Constants
fileprivate static let cookiesDefaultsKey: String = "NSUserDefaultsKeyCookieStorage"
fileprivate static let sessionDefaultsKey: String = "NSUserDefaultsKeyUserSessionStorage"
// Instance properties
let client: Client
// -------------------------------------------------------------------------------
// MARK: - Lifecycle
// -------------------------------------------------------------------------------
required init(client: Client) {
// set local properties
self.client = client
// call super init
super.init()
// Store cookies after a session was initialized
UserSession.persistentStoreCookies()
}
required init?(coder aDecoder: NSCoder) {
self.client = aDecoder.decodeObject(forKey: "client") as! Client
super.init()
}
// -------------------------------------------------------------------------------
// MARK: - Public
// -------------------------------------------------------------------------------
func encode(with aCoder: NSCoder) {
aCoder.encode(self.client, forKey: "client")
}
/**
Performs all necessary operations after user logs in: stores current cookies and user session for the case user stops and reruns the application later
*/
static func afterLogin() {
// Persistently store session data
self.persistentStoreCookies()
self.persistentStoreCurrentSession()
// Register user & device for PUSH notifications
NotificationsManager.registerForNotifications()
}
/**
Performs all necessary operations after user logs out: deletes stored cookies and user session so that the next time the user runs this application he gets the login prompt
*/
static func afterLogout() {
// Erase user session data
self.persistentEraseCookies()
self.persistentEraseCurrentSession()
// Delete all offers from local database
CoreDataHelper.deleteEntitiesInContext(CoreDataHelper.mainContext, entityName: UsedOffer.entityName)
CoreDataHelper.saveContext()
}
static func requestPopup() {
// Get popup from server
print("INFO: Checking for popups on the server...")
ClientPopupRequest.send({ (popup) -> Void in
if let popup = popup {
// If there is one, show it
popup.showAlertAndPerform(in: RootVC.sharedInstance) {
// After the popup performs its action, ask for another one
self.requestPopup()
}
} else {
// If none, exit
print("INFO: No new popups found.")
}
}) { (error) -> Void in
}
}
// -------------------------------------------------------------------------------
// MARK: - Private
// -------------------------------------------------------------------------------
/**
Saves current user session to persistent store (currently NSUserDefaults)
*/
static func persistentStoreCurrentSession() {
if let session = _current {
// Archive session
let sessionData = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: sessionData)
archiver.encode(session)
archiver.finishEncoding()
// Session encoded
KeychainWrapper.standard.set(session, forKey: UserSession.sessionDefaultsKey)
// UserDefaults.standard.set(sessionData, forKey: UserSession.sessionDefaultsKey)
// UserDefaults.standard.synchronize()
} else {
print("WARNING: No session to store")
}
}
/**
Tries to load an user session from persistent store (currently NSUserDefaults) and store it as current session in UserSession class. Returns the loaded instance of user session if it succeeds, otherwise returns nil
*/
fileprivate static func persistentLoadCurrentSession() -> UserSession? {
if let keychainData = loadSessionFromKeychain() {
persistentEraseUserDataSession()
return keychainData
} else if let userData = loadSessionFromStore() {
return userData
}
return nil
}
fileprivate static func loadSessionFromKeychain() -> UserSession? {
if let sessionData = KeychainWrapper.standard.data(forKey: UserSession.sessionDefaultsKey) {
print("sessionData:")
print(sessionData.debugDescription)
if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession {
_current = session
return session
} else {
print("ERROR: Could not parse UserSession from Keychain")
}
return nil
}
return nil
}
fileprivate static func loadSessionFromStore() -> UserSession? {
if let sessionData = UserDefaults.standard.object(forKey: UserSession.sessionDefaultsKey) as? Data {
let unarchiver = NSKeyedUnarchiver(forReadingWith: sessionData)
if let session = unarchiver.decodeObject() as? UserSession {
unarchiver.finishDecoding()
// Session decoded
_current = session
return session
} else {
print("ERROR: Could not parse UserSession from Store")
}
return nil
}
return nil
}
fileprivate static func persistentEraseCurrentSession() {
// Remove the current session object
_current = nil
// Remove the persisted session object
UserDefaults.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
KeychainWrapper.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
}
fileprivate static func persistentEraseUserDataSession() {
// Remove the persisted session object
UserDefaults.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
}
fileprivate static func persistentStoreCookies() {
if let cookies = HTTPCookieStorage.shared.cookies {
let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookies)
UserDefaults.standard.set(cookieData, forKey: UserSession.sessionDefaultsKey)
KeychainWrapper.standard.set(cookieData, forKey: UserSession.cookiesDefaultsKey)
} else {
print("WARNING: No cookies to store")
}
}
fileprivate static func persistentLoadCookies() {
var cookieData: Data?
if let keychainData = KeychainWrapper.standard.data(forKey: UserSession.cookiesDefaultsKey) {
cookieData = keychainData
} else if let userData = UserDefaults.standard.object(forKey: UserSession.cookiesDefaultsKey) as? Data {
cookieData = userData
}
if (cookieData != nil) {
if let cookies = NSKeyedUnarchiver.unarchiveObject(with: cookieData!) as? [HTTPCookie] {
cookies.forEach { HTTPCookieStorage.shared.setCookie($0) }
} else {
print("ERROR: Could not parse [NSHTTPCookie] from unarchived data")
}
} else {
print("WARNING: No cookies to load")
}
}
fileprivate static func persistentEraseCookies() {
UserDefaults.standard.removeObject(forKey: UserSession.cookiesDefaultsKey)
KeychainWrapper.standard.removeObject(forKey: UserSession.cookiesDefaultsKey)
}
}
// EDIT: added UserSession.swift class
What you're getting here is an exception; exceptions cannot be caught or handled in Swift, and are different from errors, which is why you can't wrap the call in a do {} catch {}.
The issue here is that your archive contains the name of a class which is then not available at runtime, which can happen for several reasons:
You encoded the archive in an app that contains the class, and are attempting to decode in a different app which does not contain the class. This can happen if you forget to link the class implementation with the target you're working with, but this is much less likely in Swift because you can't import the header and forget to link the implementation
The class name has changed. This can happen for a few reasons itself, but in Swift, the most likely reason is due to your app/module name changing. Classes in Swift have runtime names which include the full path to the class. If you've got an app named "MyApp", a class called "Foo" has a qualified name of "MyApp.Foo". Similarly, a class "Bar" nested in "Foo" would have a qualified name of "MyApp.Foo.Bar". Importantly, if you change the name of your app (which is the name of your main module), the name of the class changes!
What's likely happening here is that you've either renamed the target since the archive was written (which would change the class name), or you wrote the archive with the class in one target, but are decoding in another. Even though you include the same class in both, they have different names ("MyTarget1.UserSession" vs. "MyTarget2.UserSession").
You can remedy this with a few steps:
Give the class a stable name which won't change with #objc, e.g. #objc(UserSession) class UserSession { ... }. This will give the class an Objective-C name that is constant and does not depend on the module name in any way
Use NSKeyedUnarchiver.setClass(_:forClassName:) to migrate the old archives to use the new, stable class
See NSKeyedArchiver and sharing a custom class between targets for the full details on how to migrate the archive forward.
Itai's answer is fantastic and I cannot match their explanation. In my case, the main target was looking for a class that existed only in the test target. The solution was to run all tests to completion and the build the main target again. I guess some cleanup hadn't been done the last time I ran my tests.
Related
I have implemented sharing of items from my Core Data store, using the NSPersistentCloudKitContainer api, with limited success. The objects I share (a TripsLog) have child objects (Trip), which appear on participants' devices. I've implemented more or less a direct copy of Apple's example code, except using my own core data entities.
The problem is that any new trips I create on participants' devices don't appear in the owner's database, whereas any I create on the owner's device appear for all participants, updating their displays pretty much instantly. If I edit any of the original trips on a participant's device, the changes are successfully shared, it's just the new ones which are problematic.
I'm getting a lot of noise in the console -
"Attempt to map database failed: permission was denied. This attempt will not be retried"
"Permission Failure" (10/2007); server message = "Invalid bundle ID for container"
etc
which really aren't helpful. The bundle ID & containers are set up properly and it all works fine for syncing across a single user's devices.
Although my code is more-or-less just the same as Apple's, except for the container identifier & the Core Data entities, here's most of the relevant stuff -
// sharing protocol
protocol CloudKitSharable: NSManagedObject {
static var entityName: String { get }
var identifier: String { get }
var sharedTitle: String { get }
var sharedSubject: String? { get }
var thumbnailImage: UIImage? { get }
}
class PersistenceController: NSObject {
enum CoreDataError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
static let shared = PersistenceController()
private static let containerIdentifier = "iCloud.com.containerIdentifier"
private var _privatePersistentStore: NSPersistentStore?
var privatePersistentStore: NSPersistentStore {
return _privatePersistentStore!
}
private var _sharedPersistentStore: NSPersistentStore?
var sharedPersistentStore: NSPersistentStore {
return _sharedPersistentStore!
}
lazy var cloudKitContainer: CKContainer = {
return CKContainer(identifier: Self.containerIdentifier)
}()
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container: NSPersistentCloudKitContainer = try! mainDatabaseContainer()
guard let localDatabaseURL = localDatabaseURL, let cloudDatabaseURL = cloudDatabaseURL else {
fatalError("#\(#function): Failed to get local database URLs")
}
// Set up the database which will sync over the cloud
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudDatabaseURL)
cloudStoreDescription.configuration = "Cloud"
cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
setupStoreDescription(cloudStoreDescription)
let cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
cloudKitContainerOptions.databaseScope = .private
cloudStoreDescription.cloudKitContainerOptions = cloudKitContainerOptions
// Setting up for sharing
let sharedStoreDescription = cloudStoreDescription.copy() as! NSPersistentStoreDescription
sharedStoreDescription.url = sharedDatabaseURL
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
// Set up the stuff which we don't want to sync to the cloud
let localStoreDescription = NSPersistentStoreDescription(url: localDatabaseURL)
localStoreDescription.configuration = "Local"
setupStoreDescription(localStoreDescription)
// finish setting up the container
container.persistentStoreDescriptions = [
cloudStoreDescription,
localStoreDescription,
sharedStoreDescription
]
loadPersistentStores(for: container)
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = TransactionAuthor.app
/**
Automatically merge the changes from other contexts.
*/
container.viewContext.automaticallyMergesChangesFromParent = true
/**
Pin the viewContext to the current generation token and set it to keep itself up-to-date with local changes.
*/
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("#\(#function): Failed to pin viewContext to the current generation:\(error)")
}
/**
Observe the following notifications:
- The remote change notifications from container.persistentStoreCoordinator.
- The .NSManagedObjectContextDidSave notifications from any context.
- The event change notifications from the container.
*/
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
NotificationCenter.default.addObserver(self, selector: #selector(containerEventChanged(_:)),
name: NSPersistentCloudKitContainer.eventChangedNotification,
object: container)
return container
}()
func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction], to context: NSManagedObjectContext) {
context.perform {
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
}
}
private var cloudDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("CloudDatabase.sqlite")
}
private var localDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Database.sqlite")
}
private var sharedDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Shared.sqlite")
}
private func setupStoreDescription(_ description: NSPersistentStoreDescription) {
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.shouldInferMappingModelAutomatically = true
}
private func model(name: String) throws -> NSManagedObjectModel {
return try loadModel(name: name, bundle: Bundle.main)
}
private func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataError.modelLoadingFailed(forURL: modelURL)
}
return model
}
private func mainDatabaseContainer() throws -> NSPersistentCloudKitContainer {
return NSPersistentCloudKitContainer(name: "Database", managedObjectModel: try model(name: "Database"))
}
private func locationsContainer() throws -> NSPersistentContainer {
return NSPersistentContainer(name: "Locations", managedObjectModel: try model(name: "Locations"))
}
private func loadPersistentStores(for container: NSPersistentContainer) {
container.loadPersistentStores { [unowned self] storeDescription, error in
if let error = error {
fatalError("#\(#function): Failed to load persistent stores:\(error)")
} else {
print("Database store ok: ", storeDescription)
if let containerOptions = storeDescription.cloudKitContainerOptions, let url = self.sharedDatabaseURL {
if containerOptions.databaseScope == .shared {
let sharedStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._sharedPersistentStore = sharedStore
}
} else if let url = self.cloudDatabaseURL {
let privateStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._privatePersistentStore = privateStore
}
}
}
}
/**
An operation queue for handling history-processing tasks: watching changes, deduplicating tags, and triggering UI updates, if needed.
*/
lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
}
// Sharing extensions
extension PersistenceController {
func presentCloudSharingController<T: CloudKitSharable>(for item: T) {
/**
Grab the share if the item is already shared.
*/
var itemToShare: CKShare?
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]), let (_, share) = shareSet.first {
itemToShare = share
}
let sharingController: UICloudSharingController
if let itemToShare = itemToShare {
sharingController = UICloudSharingController(share: itemToShare, container: cloudKitContainer)
} else {
sharingController = newSharingController(for: item)
}
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
func presentCloudSharingController(share: CKShare) {
let sharingController = UICloudSharingController(share: share, container: cloudKitContainer)
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
private func newSharingController<T: CloudKitSharable>(for unsharedItem: T) -> UICloudSharingController {
return UICloudSharingController { (_, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
/**
The app doesn't specify a share intentionally, so Core Data creates a new share (zone).
CloudKit has a limit on how many zones a database can have, so this app provides an option for users to use an existing share.
If the share's publicPermission is CKShareParticipantPermissionNone, only private participants can accept the share.
Private participants mean the participants an app adds to a share by calling CKShare.addParticipant.
If the share is more permissive, and is, therefore, a public share, anyone with the shareURL can accept it,
or self-add themselves to it.
The default value of publicPermission is CKShare.ParticipantPermission.none.
*/
self.persistentContainer.share([unsharedItem], to: nil) { objectIDs, share, container, error in
if let share = share {
self.configure(share: share, with: unsharedItem)
}
completion(share, container, error)
}
}
}
private var rootViewController: UIViewController? {
for scene in UIApplication.shared.connectedScenes {
if scene.activationState == .foregroundActive,
let sceneDelegate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
let window = sceneDelegate.window {
return window?.rootViewController
}
}
print("\(#function): Failed to retrieve the window's root view controller.")
return nil
}
}
extension PersistenceController: UICloudSharingControllerDelegate {
/**
CloudKit triggers the delegate method in two cases:
- An owner stops sharing a share.
- A participant removes themselves from a share by tapping the Remove Me button in UICloudSharingController.
After stopping the sharing, purge the zone or just wait for an import to update the local store.
This sample chooses to purge the zone to avoid stale UI. That triggers a "zone not found" error because UICloudSharingController
deletes the zone, but the error doesn't really matter in this context.
Purging the zone has a caveat:
- When sharing an object from the owner side, Core Data moves the object to the shared zone.
- When calling purgeObjectsAndRecordsInZone, Core Data removes all the objects and records in the zone.
To keep the objects, deep copy the object graph you want to keep and make sure no object in the new graph is associated with any share.
The purge API posts an NSPersistentStoreRemoteChange notification after finishing its job, so observe the notification to update
the UI, if necessary.
*/
func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
if let share = csc.share {
purgeObjectsAndRecords(with: share)
}
}
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
if let share = csc.share, let persistentStore = share.persistentStore {
persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
}
}
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("\(#function): Failed to save a share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return csc.share?.title ?? "Airframe Logbook"
}
}
extension PersistenceController {
func shareObject<T: CloudKitSharable>(_ unsharedObject: T, to existingShare: CKShare?, completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)? = nil) {
persistentContainer.share([unsharedObject], to: existingShare) { (objectIDs, share, container, error) in
guard error == nil, let share = share else {
print("\(#function): Failed to share an object: \(error!))")
completionHandler?(share, error)
return
}
/**
Deduplicate tags, if necessary, because adding a photo to an existing share moves the whole object graph to the associated
record zone, which can lead to duplicated tags.
*/
if existingShare != nil {
/*
if let tagObjectIDs = objectIDs?.filter({ $0.entity.name == "Tag" }), !tagObjectIDs.isEmpty {
self.deduplicateAndWait(tagObjectIDs: Array(tagObjectIDs))
}
*/
} else {
self.configure(share: share, with: unsharedObject)
}
/**
Synchronize the changes on the share to the private persistent store.
*/
self.persistentContainer.persistUpdatedShare(share, in: self.privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
/**
Delete the Core Data objects and the records in the CloudKit record zone associated with the share.
*/
func purgeObjectsAndRecords(with share: CKShare, in persistentStore: NSPersistentStore? = nil) {
guard let store = (persistentStore ?? share.persistentStore) else {
print("\(#function): Failed to find the persistent store for share. \(share))")
return
}
persistentContainer.purgeObjectsAndRecordsInZone(with: share.recordID.zoneID, in: store) { (zoneID, error) in
if let error = error {
print("\(#function): Failed to purge objects and records: \(error)")
}
}
}
func existingShare(for item: NSManagedObject) -> CKShare? {
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]),
let (_, share) = shareSet.first {
return share
}
return nil
}
func share(with title: String) -> CKShare? {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
let share = shares?.first(where: { $0.title == title })
return share
}
func shareTitles() -> [String] {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
return shares?.map { $0.title } ?? []
}
private func configure<T: CloudKitSharable>(share: CKShare, with item: T) {
share[CKShare.SystemFieldKey.title] = item.sharedTitle
}
}
extension PersistenceController {
func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
/**
Use the email address to look up the participant from the private store. Return if the participant doesn't exist.
Use privatePersistentStore directly because only the owner may add participants to a share.
*/
let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress)
let persistentStore = privatePersistentStore //share.persistentStore!
persistentContainer.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in
guard let participants = results, let participant = participants.first, error == nil else {
completionHandler?(share, error)
return
}
participant.permission = permission
participant.role = .privateUser
share.addParticipant(participant)
self.persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
func deleteParticipant(_ participants: [CKShare.Participant], share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
for participant in participants {
share.removeParticipant(participant)
}
/**
Use privatePersistentStore directly because only the owner may delete participants to a share.
*/
persistentContainer.persistUpdatedShare(share, in: privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
// Core Data models
extension TripsLog {
#NSManaged var name: String
#NSManaged var identifier: String
#NSManaged var entries: NSSet
}
extension Trip {
#NSManaged var identifier: String
#NSManaged var name: String
#NSManaged var date: Date
#NSManaged var comments: String?
#NSManaged var leaderName: String
#NSManaged var images: NSSet?
}
If anyone is able to shed any light on this I'd really appreciate it, as Apple's own documentation is somewhat lacking. Many thanks!
I'm trying to save player game data using GKSavedGame from GameKit, but it doesn't work, and I don't get any error. (Is the data not saving? Is the data not loading back?)
One of the problem is that GKSavedGame is not well documented (there are no examples like we can find for other things), so we don't really know how to implement it correctly (e.g. what are the best practices?)
I'm using a simple struct GameData: Codable to store the game data, that I encode using JSONEncoder/JSONDecoder. Here is my GameService class with the part that doesn't work:
class GameService {
// Shared instance
static let shared = GameService()
// Properties
private(set) var isGameLoaded = false
private(set) var gameData: GameData?
// Methods
func loadSavedGame(completionHandler: #escaping () -> Void) {
// Check game is not loaded yet
if isGameLoaded {
completionHandler()
return
}
// Get player
let localPlayer = GKLocalPlayer.local
if localPlayer.isAuthenticated {
localPlayer.fetchSavedGames { games, error in
// Iterate saved games
var game: GKSavedGame?
for currentGame in games ?? [] {
if game == nil || game?.modificationDate ?? Date() < currentGame.modificationDate ?? Date() {
game = currentGame
}
}
// If one found, load its data
if let game = game {
game.loadData { data, error in
if let data = data {
self.gameData = try? JSONDecoder().decode(GameData.self, from: data)
}
self.isGameLoaded = true
self.initGameData()
completionHandler()
}
} else {
self.isGameLoaded = true
self.initGameData()
completionHandler()
}
}
}
}
func saveGame() {
// Get player
let localPlayer = GKLocalPlayer.local
if localPlayer.isAuthenticated, let gameData = gameData, let data = try? JSONEncoder().encode(gameData) {
// Save its game data
localPlayer.saveGameData(data, withName: "data") { savedGame, error in
if let error = error {
print(error.localizedDescription)
}
}
}
}
func initGameData() {
// If game data is undefined, define it
if gameData == nil {
gameData = GameData()
}
}
func gameEnded(level: Level, score: Int64) {
// Here I edit my `gameData` object before saving the changes
// I known that this method is called because the data is up to date in the UI
// ...
saveGame()
}
}
Also, GameCenter is enabled in the app capabilities (and workout for other things like leaderboards)
After restarting the app, and calling loadSavedGame, the gameData property is not restored to its previous state.
What is wrong with this code?
Note: I tested it on both Simulator and on my own iPhone (real daily device where GameCenter and iCloud are working with other apps) and it never saves anything.
After enabling iCloud in Capabilities, iCloud Documents, and creating a container for the app, it now works. There is nothing in the documentation about this.
I am facing this issue once or twice a day for the last week when I open my app & the app tries any save operation on the context, I still can't find a way to reproduce it.
I have searched many question on SO for fix, but most of them point 2 issues
Core Data Migration issue(which I don't have as I am on the same Model version no.)
failure of loading the persistent store (which is also doesn't happen in my case as my Core Data Stack doesn't initialise the main UI if the loadPersistentStores method on the persistentContainer fails)
I am using the Core Data stack setup mentioned in the below link:
https://williamboles.me/progressive-core-data-migration/
Here is my CoreData Setup class:
lazy var persistentContainer: NSPersistentContainer = {
let persistentContainer = NSPersistentContainer(name: "ABC")
let description = persistentContainer.persistentStoreDescriptions.first
description?.shouldInferMappingModelAutomatically = false //inferred mapping will be handled else where
description?.shouldMigrateStoreAutomatically = false
description?.type = storeType
return persistentContainer
}()
lazy var managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var _managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
return context
}()
// MARK: - Singleton
private static var privateShared : CoreDataManager?
class func shared() -> CoreDataManager { // change class to final to prevent override
guard let uwShared = privateShared else {
privateShared = CoreDataManager()
return privateShared!
}
return uwShared
}
class func destroy() {
privateShared = nil
}
// MARK: - Init
init(storeType: String = NSSQLiteStoreType, migrator: CoreDataMigratorProtocol = CoreDataMigrator()) {
self.storeType = storeType
self.migrator = migrator
}
// MARK: - SetUp
func setup(completion: #escaping () -> Void) {
loadPersistentStore {
completion()
}
}
// MARK: - Loading
private func loadPersistentStore(completion: #escaping () -> Void) {
migrateStoreIfNeeded {
self.persistentContainer.loadPersistentStores { description, error in
guard error == nil else {
fatalError("was unable to load store \(error!)")
}
completion()
}
}
}
private func migrateStoreIfNeeded(completion: #escaping () -> Void) {
guard let storeURL = persistentContainer.persistentStoreDescriptions.first?.url else {
fatalError("persistentContainer was not set up properly")
}
if migrator.requiresMigration(at: storeURL, toVersion: CoreDataMigrationVersion.current) {
DispatchQueue.global(qos: .userInitiated).async {
self.migrator.migrateStore(at: storeURL, toVersion: CoreDataMigrationVersion.current)
DispatchQueue.main.async {
completion()
}
}
} else {
completion()
}
}
And I initialise the Core Data stack using the following code in the App Delegate:
CoreDataManager.shared().setup {[unowned self] in
self.showMainUI()
}
My App crashes after the Home Controller is loaded & some part of my code does a save operation on certain NSManagedObject Model
This how I save to Context:
let context = CoreDataManager.shared().managedObjectContext // background context
context.performAndWait {
if let entityDescription = NSEntityDescription.entity(forEntityName: Entity_Name, in: context) {
if let runEntityObject = NSManagedObject(entity: entityDescription, insertInto: context) as? MY_Model {
// Create the Object
guard context.hasChanges else { return }
do {
try context.save() // Crashes here once or twice a day :(
}
catch {
print(error.localizedDescription)
}
}
}
}
Some SO answers also mention of threading issues but I am using the performAndWait Block so the save happen on the same queue
Would be really helpful If someone pointed me in the right direction regarding this issue
After going through my AppDelegate file many times, I found that I was doing a Core Data save operation in the applicationDidBecomeActive method which is also called when the app starts from a suspended state.
So if my Core Data stack setup closure didn't finish before the applicationDidBecomeActive is called the app would crash.
After removing it, the app was working fine without any crashes
My goal is to show a user list of history logins ( such as username ) if there are any. In order to do that, I am doing
1. Create an custom object named User like below
class User: NSObject
{
var login: String
init(login: String)
{
self.login = login
}
required init(coder aDecoder: NSCoder) {
login = aDecoder.decodeObjectForKey("login") as! String
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(login, forKey: "login")
}
}
// This conform to make sure that I compare the `login` of 2 Users
func ==(lhs: User, rhs: User) -> Bool
{
return lhs.login == rhs.login
}
At UserManager, Im doing save and retrieve an User. Before saving, I'm doing a check if the the list of history logins contains a User, I wont add it in, otherwise.
class UserManager : NSObject
{
static let sharedInstance = UserManager()
var userDefaults = NSUserDefaults.standardUserDefaults()
func saveUser(user:User)
{
var users = retrieveAllUsers()
// Check before adding
if !(users.contains(user))
{
users.append(user)
}
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(users)
userDefaults.setObject(encodedData, forKey: "users")
userDefaults.synchronize()
}
func retrieveAllUsers() -> [User]
{
guard let data = userDefaults.objectForKey("users") as? NSData else
{
return [User]()
}
let users = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [User]
// Testing purpose
for user in users
{
print(user.login)
}
return users
}
}
At first time trying, I do
UserManager.sharedInstance.saveUser(User(login: "1234"))
Now it saves the first login. At second time, I also do
UserManager.sharedInstance.saveUser(User(login: "1234"))
UserManager still adds the second login into nsuserdefault. That means the function contains fails and it leads to
func ==(lhs: User, rhs: User) -> Bool
{
return lhs.login == rhs.login
}
does not work properly.
Does anyone know why or have any ideas about this.
The problem is that User derives from NSObject. This means that (as you rightly say) your == implementation is never being consulted. Swift's behavior is different for objects that derive from NSObject; it does things the Objective-C way. To implement equatability on an object that derives from NSObject, override isEqual:. That is what makes an NSObject-derived object equatable in a custom way, in both Objective-C and Swift.
Just paste this code right into your User class declaration, and contains will start working as you wish:
override func isEqual(object: AnyObject?) -> Bool {
if let other = object as? User {
if other.login == self.login {
return true
}
}
return false
}
What's going on?
As #matt already said, the problem is about equality.
Look
var users = [User]()
users.append(User(login: "1234"))
users.contains(User(login: "1234")) // false
Look again
var users = [User]()
let user = User(login: "1234")
users.append(user)
users.contains(user) // true <---- THIS HAS CHANGED
contains
The contains function is NOT using the logic you defined here
func ==(lhs: User, rhs: User) -> Bool {
return lhs.login == rhs.login
}
Infact it is simply comparing the memory addresses of the objects.
Solution
You can solve the issue passing your own logic to contains, just replace this
if !(users.contains(user)) {
users.append(user)
}
with this
if !(users.contains { $0.login == user.login }) {
users.append(user)
}
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.