I am trying to sync CoreData between iOS 9.0 and watchOS 2.0. My database is pretty small, so I wanted to just send the entire file over using the file transfer in watch connectivity. I know I need to find the URL of the CoreData stack and send that along with the metadata. I think I am getting the URL correctly, but I am getting a null error for the metadata. This is what I have tried.
let bundle = NSBundle.mainBundle()
let modelURL = bundle.URLForResource("Sunday", withExtension: "momd")!
do {
let myStore = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(nil, URL: modelURL)
let fileTransfer = WCSession.defaultSession().transferFile(modelURL, metadata:myStore)
catch {
}
The metadata argument is not required and can be nil, which in your case I think will be fine.
Give that a shot
Related
I have a Core Data+CloudKit configuration where I need to provide the user with some initial data as a starting point. It's only a few records/objects so at first I tried hardcoding the data and writing to Core Data on first launch. Users are then able to add to and change the starting point data.
When I enabled CloudKit syncing, I discovered that when the second device on the same account was launched, the newly written default data for that device was 'newer' from CloudKit's point of view. That meant that the default data on the second device would sync and overwrite the user's own data on the first device. That's bad.
I believe the right solution is to provide pre-populated Core Data file (.sqlite) files in the app, so that on first launch the default data is already older than anything the user would have created on other devices. Most of the tutorials and sample code for doing this are very old, referencing Swift 3, Xcode 7, etc. I'm looking for a solution that works when using NSPersistentCloudKitContainer.
I tried the code in the answer to this SO question, changing the type for persistentContainer to NSPersistentCloudKitContainer. This compiles and runs, but the result is that my app launches with no pre-loaded data, and I'm not sure why. I have included the three .sqlite files in my Xcode project, and added them to the 'Copy Bundle Resources' list in 'Build Phases'.
Here is my code:
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let appName = Bundle.main.infoDictionary!["CFBundleName"] as! String
let container = NSPersistentCloudKitContainer(name: appName)
//Pre-load the default Core Data (Category names) on first launch
var persistentStoreDescriptions: NSPersistentStoreDescription
let storeUrl = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask).first!.appendingPathComponent(appName + ".sqlite")
let storeUrlFolder = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask).first!
if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
let seededDataUrl = Bundle.main.url(forResource: appName, withExtension: "sqlite")
let seededDataUrl2 = Bundle.main.url(forResource: appName, withExtension: "sqlite-shm")
let seededDataUrl3 = Bundle.main.url(forResource: appName, withExtension: "sqlite-wal")
try! FileManager.default.copyItem(at: seededDataUrl!, to: storeUrl)
try! FileManager.default.copyItem(at: seededDataUrl2!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-shm"))
try! FileManager.default.copyItem(at: seededDataUrl3!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-wal"))
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
I believe you are not seeing any data because your code looks into the .documentDirectory, and PersistentCloudKitContainer looks for databases in the Application Support directory by default.
I am trying load SCNParticleSystem from download bundle which i am not able to load.
Path for the resource.
file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/
let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)
This object is nil.
This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).
Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
return system
}
}
If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.
Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).
So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
system.particleImage = particleImageURL
}
}
return system
}
}
You can just set the URL of the image file and SceneKit will handle it from there.
As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.
Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.
(from File System Basics)
Don't have enough reps to add the comment so adding it as the answer.
The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.
All particles were appearing square which meant,
If the value is nil (the default), SceneKit renders each particle as a
small white square (colorized by the particleColor property).
SCNParticleSystem particleImage
In the documentation it says You may specify an image using an
NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or
NSURL instance containing the path or URL to an image file.
Instead of using the NSURL, ended up using the UIImage and it loaded up fine.
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
// load up the NSURL contents in UIImage
let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
system.particleImage = particleUIImage
}
}
return system
}
}
I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.
Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)
Why do I get this error when I run the following code? :
"Internal Error" (1/1000); "No authToken received for asset"
I think it has something to do with the setObject code in the last line.
let documentsDirectoryPath:NSString = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
var imageURL: URL!
let imageData = UIImageJPEGRepresentation(self.newImage, 1.0)
let path:String = documentsDirectoryPath.appendingPathComponent(self.newImage.description)
try? UIImageJPEGRepresentation(self.newImage, 1.0)!.write(to: URL(fileURLWithPath: path), options: [.atomicWrite])
imageURL = URL(fileURLWithPath: path)
try? imageData?.write(to: imageURL, options: [.atomicWrite])
let imageAsset:CKAsset? = CKAsset(fileURL: URL(fileURLWithPath: path))
curImages = record["Images"] as! [CKAsset]
curImages.append(imageAsset!)
print("saving image")
record.setObject(curImages as CKRecordValue?, forKey: "Images")
I've encountered this, too. It appears to be a bug in cloudkit, and--from what I can tell--it happens when you try to re-use any part of the "asset creation chain."
In other words, you have some initial data, you create an image from that data, you write it to a file, you load that file into a CKAsset, then you load the CKAsset into the CKRecrod. In my experiments, if you re-use any of those components... or if they just happen to be the same (that is, you create an image, then you happen to create a new-but-identical image later) you'll see this error.
For example, the following code reliably recreates the "no auth token" error when saving a record. All it does is create an array of assets and places it into the record:
for (int i = 0; i <= maxPlayers; i++)
{
int tempVal = 0xf;
NSData *tempData = [[NSData alloc] initWithBytes:&tempVal length:sizeof(tempVal)];
NSString *tempDataFilepath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"temp%d.dat",i]];
[tempData writeToFile:tempDataFilepath atomically:YES];
NSURL *tempDataURL = [NSURL fileURLWithPath:tempDataFilepath];
someArray[i] = [[CKAsset alloc] initWithFileURL:tempDataURL ];
}
someRecord[SOME_FIELD_NAME] = someArray;
Simply changing the third line to:
int tempVal = i; //force the temp value to be different every time
Completely solves the error.
Furthermore, this error occurs even when I tried to use a value in a different CKAsset **that was already used in a prior CKAsset For example, using int tempVal = 0xf in the first asset, then using int secondTempVal = 0xf in another CKAsset also produces the "no auth token" error.
In my case, I was able to force the asset value to always be a unique value, and completely solved the problem. In your case, I suggest the following possible work arounds:
Check if you're using identical images for your assets. If you are, try slightly modifying the images for each new CKAsset.
If you must re-use identical images, try saving the record after you set each asset. I do not know if that will solve the issue, and it certainly increases your network traffic. But it's worth an experiment to see if it helps.
In this question Saving CKAsset to CKRecord in CloudKit produces error: "No authToken received for asset" the OP was able to create separate copies of the image file that ultimately solved the problem.
Open a bug with Apple. I didn't bother doing this, as I've grown jaded watching similar bug reports sit open for years without attention. But who knows, you might have better luck.
This is not an answer to the specific problem (which as been solved by the accepted answer), but it solves another problem that creates the same error message, so it might be useful for somebody else:
I have an app that uses CoreData+Cloudkit with a .public database, i.e. my description for the NSPersistentCloudKitContainer uses
description.cloudKitContainerOptions!.databaseScope = .public
Whenever I change my CloudKit schema, I have to re-initialize it using
do {
try self.initializeCloudKitSchema()
} catch {
print("Could not initialize schema, error \(error)")
}
This creates the error
"Internal Error" (1/1000); "No authToken received for asset"
although I do not use any asset in my model.
I now realized that it has something to do with the .public database:
As soon as I out-comment the instruction that sets the database scope to .public, the re-initialization works without problems.
Now the CloudKit schema is independent of the database type (.private or .public). Thus, a re-initialization of the schema requires the following:
Set the database to .private (the default)
Execute the init code
Set the database to .public
Disable the init code
PS: I know I should write now a bug report, but I stopped doing so: Nearly none of my bug reports (about 15) have ever been answered or processed, so it is not worth the effort.
I need to Display and modify my data structure from both Apple Watch and iPhone.
The Database:
I am currently using a simple Realm Structure where I have an Object A and an Object B which can hold lots of A's.
So on iPhone the user can create a B and add A's and view of course all A's and B's.
I want the Apple watch to show all A's of the current B and give the users the chance to add new A's to their current B.
The way I have tried to do it:
I wanted to move the hole Realm file from iPhone to the watch or the other way. (That was a tip from the Internet)
iPhone Code:
override func viewDidLoad() {
super.viewDidLoad()
if WCSession.isSupported() { //makes sure it's not an iPad or iPod
let watchSession = WCSession.default()
watchSession.delegate = self
watchSession.activate()
transferRealmFile()
if watchSession.isWatchAppInstalled {
do {
try watchSession.updateApplicationContext(["foo": "bar"])
} catch let error as NSError {
print(error.description)
}
}
}
}
func transferRealmFile(){
if let path = Realm.Configuration().fileURL {
WCSession.default().transferFile(path, metadata: nil)
}
}
WathcKit Extension:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
//set the recieved file to default Realm file
var config = Realm.Configuration()
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
let realmURL = documentsDirectory.appendingPathComponent("data.realm")
if FileManager.default.fileExists(atPath: realmURL.path){
try! FileManager.default.removeItem(at: realmURL)
}
try! FileManager.default.copyItem(at: file.fileURL, to: realmURL)
config.fileURL = realmURL
Realm.Configuration.defaultConfiguration = config
}
Then I call transferRealmFile() every time I write to Realm. This works but I can't solve this Problems:
Problems:
It doesn't work if only watchKit App is started.
Apple Watch to iPhone doesn't work the same way. (I think I need to change the didRecived code, but I don't know what)
Question:
Do you know who to solve this 2 Problems or do you maybe know a better way to handle the situation or will the way we interact between iPhone an Watch change in WathcOS 3?
With watchOS1 it was possible to use AppGroups to share resources (even your Realm database) between an iOS app and its Watch extension. However, Apple removed this in watchOS 2, so now the only way to share data between your iOS and watchOS apps is via WatchConnectivity. Have a look at this answer.
Sadly the WatchConnectivity framework requires the WCSession to be active on both devices for transferring data, so you can't really get around problem 1.
In my opinion it is a better solution to only communicate the changes between the two apps and not send the whole Realm file, since your Realm file can get quite big and hence sending it forward and backward can take a lot of time and resources, while just sending the changes should be way faster.
I'm transferring a file from iOS to WatchOS. applicationContext and userInfo transfers all work fine. When I run the following code, nothing is transferred. fileTransfer.transferred == false.
let fileTransfer = WCSession.defaultSession().transferFile(fileurl, metadata: dict)
I can see all of my attempts queued up when I look at:
let transfers = WCSession.defaultSession().outstandingFileTransfers
Is there something I can check that will provide feedback as to why nothing is getting transferred? I'm using simulators. The file is a plist file. I'm accessing it like this:
let fileurl = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("myfile", ofType: "plist")!)
fileurl does return the file path.