How to export package files in document based application on OSX? - objective-c

in my app, I want to load certain image user picks, make some modification, after that, save a copy in my own file format. Since I want to keep track of user modification, I decided to export my file as a package(bundle).
So, in my document class, there's a NSImage object that holds the image file. In the fileWrapperOfType:error: method, I've setup a NSFileWrapper object, and put the object in it. Here's the code:
NSDictionary *fileWrappers = [self.documentFileWrapper fileWrappers];
if (image != nil) {
NSBitmapImageRep *imageRep = [image bitmapImageRepresentation];
NSData *imageData = [imageRep representationUsingType:NSPNGFileType properties:nil];
NSFileWrapper *imageFileWrapper = [[NSFileWrapper alloc]
initRegularFileWithContents:imageData];
[imageFileWrapper setPreferredFilename:#"image"];
[[self documentFileWrapper] addFileWrapper:imageFileWrapper];
}
return self.documentFileWrapper;
In my project plist file, I have two document types, first is the type of public.image since I need to load images in my app:
The other one is my own document type. To make the file a package, I've checked the bundle checkbox in xcode:
If I simply run this now, the code compains that finding extension from type identifier is deprecated, so, I managed to add an entry in Exported UTIs:
At this moment, everything seems working, except that the outputed folder with mdc extension, is indeed a folder instead of a package, what am I missing here? Thanks in advance!

Finally I solved the problem, the key here is to change Conforms To field to com.apple.package.

In order for a package on OS X to be a pkg, it must have a .pkg extension. I hope it is as simple as having the wrong extension.
Note: Until recently, .pkg files were merely directories. Now, we often see package.pkg packages that are obscured cannot be browsed causually without some tricks like using Pacifist, pkgutil or xar:
pkgutil --extract file.pkg folder/
or
xar -xf file.pkg

Related

UIDocumentPickerViewController initForExportingURLs asCopy creates zero length file on network volume

I'm using the following code snippet in a Mac Catalyst app to "export" (copy) a file from an URL to a user-selected URL:
UIDocumentPickerViewController* documentPicker = [[UIDocumentPickerViewController alloc] initForExportingURLs:#[ [[NSURL alloc] initFileURLWithPath:path] ] asCopy:YES];
This works as expected if a local folder is picked (the file in path is copied there). However, if a network folder is picked (in this case, SMB), a zero-length file is generated and no error is shown.
Does anyone know what might cause this?

Documents not Uploading to iCloud when using For Loops - iOS

I am attempting to upload a local folder full of documents to a remote iCloud folder. I wrote this method to loop through the array of files in the local folder, check if they already exist, and if they don't exist upload them to iCloud. Note- this code is being executed on the background thread not the main thread.
//Get the array of files in the local documents directory
NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSArray *localDocuments = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:nil];
//Compare the arrays then upload documents not already existent in iCloud
for (int item = 0; item < [localDocuments count]; item++) {
//If the file does not exist in iCloud, upload it
if (![[iCloud previousQueryResults] containsObject:[localDocuments objectAtIndex:item]]) {
NSLog(#"Uploading %# to iCloud...", [localDocuments objectAtIndex:item]);
//Move the file to iCloud
NSURL *destinationURL = [[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:[NSString stringWithFormat:#"Documents/%#",[localDocuments objectAtIndex:item]]];
NSError *error;
NSURL *directoryURL = [[NSURL alloc] initWithString:[documentsDirectory stringByAppendingPathComponent:[localDocuments objectAtIndex:item]]];
BOOL success = [[NSFileManager defaultManager] setUbiquitous:YES itemAtURL:directoryURL destinationURL:destinationURL error:&error];
if (success == NO) {
//Determine Error
NSLog(#"%#",error);
}
} else {
...
} }
When I run this code the For Loop works fine - I use NSLog statements to find out which file it is uploading - and every file stored locally that isn't already in iCloud is supposed to start uploading. After the For Loop is finished I check which documents are now in iCloud using developer.icloud.com. Only one file (an sqlite file that my app keeps making but never uses) out of the many stored locally uploads to iCloud. Why would only one file upload when using for loops?
When I use the same code to upload individual files (without a For Loop) they upload to iCloud perfectly. Why would a For Loop hinder the uploading of files? Does this have to do with the For Loop continuing to the next line of code without waiting for the last process / line to finish executing? What's going on here?
EDIT: Usually when I upload large files to iCloud (without using a For Loop) I can see on the iCloud dev site that the file is Pending Upload almost instantly. I'm testing everything over WiFi and I've been waiting a while but nothing appears (except for the sqlite file).
EDIT: I also check for iCloud availability in a different method which then allows calls to this method if iCloud is available. I've also made sure to read over the documentation and watched the WWDC videos on Apple's website, however they are complex and don't provide much explanation for what I'm trying to do.
EDIT: After revising the code above (adding the error functionality), I now get this error message in the log:
Error Domain=NSCocoaErrorDomain Code=512 "The operation couldn’t be completed. (Cocoa error 512.)" UserInfo=0x1f5dd070 {NSUnderlyingError=0x208ad9b0 "The operation couldn’t be completed. (LibrarianErrorDomain error 2 - No source URL specified.)"}
This makes things even more confusing because one of the files uploads successfully, whereas the others do not.
OK, that last edit is useful. The error complains that the source URL (directoryURL in your code) is missing-- probably because it's nil. Why would it be nil? Because you're creating it wrong. You're using -[NSURL initWithString:], and the docs say that:
This string must conform to URL format as described in RFC 2396. This method parses URLString according to RFCs 1738 and 1808.
You're passing in a file path, not a URL. If you pass NSURL something it doesn't recognize as a valid URL, it normally returns nil. It looks like NSURL will frequently produce a file URL anyway, but failure is the expected behavior here. From what I can tell it seems to work if the filename contains no spaces, but that's not documented and not something you could rely on.
What you should do is change the line where you initialize directoryURL to use something that accepts a file path. Something like:
NSURL *directoryURL = [NSURL fileURLWithPath:[documentsDirectory stringByAppendingPathComponent:[localDocuments objectAtIndex:item]]];
Also, make sure to verify that directoryURL is not nil, just in case.

Core Data adding entities at runtime

I am rewriting the question to help clarify and get rid of a lot of code I wrote that really doesn't help.
I am using a .xcdatamodel for my initial schema, but I need to add entities to my schema at runtime and therefore I need to add a new NSManagedObjectModel and copy over the existing entities and then add the new entities.
If I create a new NSPersistantStore first and then ask my NSMigrationManager to migrate, I get an error about how it can't move the source model to destination path because file already exists.
If I simply ask my NSMigrationManager to migrate, then it just crashes without any error codes or anything in the debugger.
NSMappingModel *mappingModel = [NSMappingModel inferredMappingModelForSourceModel:originalModel destinationModel:newModel error:&error];
NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:originalModel destinationModel:newModel];
if (![manager migrateStoreFromURL:[originalStore URL]
type:NSSQLiteStoreType
options:[self autoMigrationOptions]
withMappingModel:mappingModel
toDestinationURL:[NSPersistentStore MR_urlForStoreName:[self nextStoreName]]
destinationType:NSSQLiteStoreType
destinationOptions:[self autoMigrationOptions]
error:&error])
{
return NO;
}
The URL's are all good, the mapping model looks good when I log it to the console, the manager exists, etc. In this case I did not create the NSPersistantStore yet, but according to the NSMigrationManager class reference if a store does not exist at the destination URL then one is automatically created.
Anyone have a clue?

Can't find mapping model for migration - UIManagedDocument Core Data Migration

I have two versions of my model Model001.xcdatamodel and Model002.xcdatamodel. These two are in the Model.xcdatamodeld bundle.
I also have a Model001to002.xcmappingmodel which is not part of the Model.xcdatamodeld. I checked: both the xcmappingmodel and the xcdatamodeld get copied into the .app bundle.
My managed object context is initialized like this:
NSURL *documentModel = [bundle URLForResource:#"Model"
withExtension:#"momd"]; managedObjectModel = [[NSManagedObjectModel alloc]
initWithContentsOfURL:documentModel]; return managedObjectModel;
I also set these properties on my overridden initWithFileURL: in my UIManagedObject subclass.
NSMutableDictionary *options = [NSMutableDictionary dictionaryWithDictionary:self.persistentStoreOptions];
[options setObject:#YES forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:#YES forKey:NSInferMappingModelAutomaticallyOption];
self.persistentStoreOptions = [options copy];
But when I try to open a documet, I get the following error:
Can't find mapping model for migration
-- UPDATE --
Even if I do a manual migration
[NSMappingModel mappingModelFromBundles:#[[NSBundle mainBundle]]
forSourceModel:sourceObjectModel
destinationModel:self.managedObjectModel];
this returns nil. Although I double checked that the Model001to002.cdm is in the app bundle. It has to be in the app bundle right?
A "gotcha" with mapping models is that you are not allowed to make any changes to the models after you created the mapping. If you do, you will also get this error.
OK, solved the problem by removing all core data files from Xcode, reading them and setting the source and destination of the mapping model again.
Damn you Xcode!
You are not allowed to make any changes to the source/destination model after you have created the mapping models.
If you do make some changes,
mappingModelFromBundles:forSourceModel:destinationModel: will not be able to find the mapping model file
addPersistentStoreWithType:configuration:URL:options:error: with {NSInferMappingModelAutomaticallyOption: #NO} will report an error "Can't find mapping model for migration"
migrateStoreFromURL:type:options:withMappingModel:toDestinationURL:destinationType:destinationOptions:error: will report an error "Mismatch between mapping and source/destination models"
So, just recreate the mapping model and copy every change you made in the old one.
TL;DR
At least as of Xcode 8/9, open the mapping model then from the Editor menu select Refresh data models. Usually it seems you need to restart Xcode. If that doesn't do it you might try re-selecting the destination at the bottom of the model editor.
More Tips
Definitely NEVER change a model after it has been distributed in an app build.
For this example, let's say you have published Data Model 1 (DM1) and are making a migration to DM2. If you set DM2 as the active version then run your app, a migration will run on your persistent store. If you then make another change to DM2, run your app... Boom!
The issue is that your store has already been migrated to "DM2" but the data in the store doesn't fit into the model anymore. And, we can't migrate from DM2 to DM2 again.
It may seem like an obvious solution to go ahead and create DM3. It is
usually a good idea though to minimize the number of models and
migrations while you are developing.
So... now you have a persistent store that has been migrated to a defunct DM2. How do you test the migration again? You could revert your app and generate some data with DM1 but I prefer to use backups
Creating a backup
Before you run your app with DM2 you can copy the existing store (with DM1) to use for later test migrations. On macOS you can easily do this manually. The code below should do the trick as well. Typically you wouldn't want to ship this, rather you could just put it somewhere before your normal CD stack opens, run the app, then stop the app (maybe place a breakpoint just after then end the run via Xcode).
let fm = FileManager.default
let url = // The store URL you would use in ↓
// try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
let dir = url.deleteLastPathComponent().appendingPathComponent("Backup", isDirectory: true).appendingPathComponent("DM1", isDirectory: true)
print("Saving DB backup for DM1")
if !fm.fileExists(atPath: dir.path) {
do {
// Create a directory
try fm.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
let backupURL = dir.appendingPathComponent(url.lastPathComponent)
try fm.copyItem(at: url, to: backupURL)
}
catch {
print("Failed to save DB backup")
}
}
Oops, I need to make another change...
If you run your migration to DM2 then realize you need to make another change, you'll want to re-test your migration from DM1 -> DM2. This is where the backup comes in.
Same way you made the backup, run this code.
let fm = FileManager.default
let url = // The store URL you would use to add the store
let dir = url.deleteLastPathComponent().appendingPathComponent("Backup", isDirectory: true).appendingPathComponent("DM1", isDirectory: true)
let backupURL = dir.appendingPathComponent(url.lastPathComponent)
if fm.fileExists(atPath: backupURL.path) {
do {
fm.removeItem(at: url.path)
try fm.copyItem(at: backupURL, to: url)
}
catch {
print("Failed to restore DB backup")
}
}
You now have a restored DM1 store and have made changes to DM2. If you run the app the migration might succeed but it won't use your custom mapping model.
Remember if you are using a custom mapping, you will still need to use the Refresh Data Models technique before the mapping model will work.
This can happen if your test device's store is from a version of the data model that no longer exists.
For example I had Data Model Version 7, then I made Data Model Version 8. I made a mapping model to go from 7 to 8. Then I ran it on my test device and everything was happy.
Then I made some more changes to 8.
The thing to realize is that in Core Data, every model has a hash identifier that the system creates by taking a checksum of the xcdatamodel file. So if you make even a slight change, even if you didn't create a new version, it sees it as a different version. These versions' identifiers are NSStoreModelVersionHashes (see documentation here).
So in other words, I ended up with:
Data Model 7 (release) - 0plcXXRN7XHKl5CcF+fwriFmUpON3ZtcI/AfK748aWc=
Data Model 8 (beta) - qeN1Ym3TkWN1G6dU9RfX6Kd2ccEvcDVWHpd3LpLgboI=
Data Model 8 (release) - EqtMzvRnVZWkXwBHu4VeVGy8UyoOe+bi67KC79kphlQ=
Instead of making a version 9, and saving the original version 8 in the data model history, I just updated 8, figuring automatic migration could take care of me. Well, it couldn't, and I couldn't make a mapping between the two, because the old (beta) version of 8 was gone.
I did it that way because it was an intermediary internal build (not a release) so it wasn't a big deal, but it did throw me for a loop!
If it wasn't an internal build and I needed to make this work, I could go back to the (beta) commit and pull out that xcdatamodel file for 8 (beta), rename the (release) version to 9, then stick it into the release build and make a mapping model between 8 and 9.
However since it was just an internal beta build, we just erased and reinstalled the app on test devices. We did verify that, when going from 7 (release) to 8 (release), the migration went smoothly.
Removing Coredata files from its path an re - run project is worked for me

Modifying a file on FTP Server

I have finished the majority of my application that I am working on, but I have run into a problem. Basically, what I wrote streams audio from my server to the iPod and plays it without having to save the file. It has a GUI that allows the user to choose the file they want to listen to, and it works flawlessly.
My problem is that I want to be able to have some sort of a counter that updates every time a user listens to a file. I have a file in the project, "TotalDownloads.txt," that I had planned on using to store the new value and upload back to the server, but when the code executes, nothing changes within that file. I had also tried just declaring the file #"TotalDownloads.txt," but that created a new file in the Macintosh HD named "TotalDownloads.txt."
The two identifiers "//-" and "-//" are to parse the file for the number, when I get this figured out, specifically to isolate the numeric value, should there be any formatting involved. Eventually, I want to have counts for each and every one of the files.
I have no problem with downloading the text file off of the server and reading it into the iPhone, but the problem arises when sending it back. What is the easiest way to 1, make a temporary file text file on the iPhone, and 2, upload it back to the server to replace the existing file?
Is there a way to just modify the file on the server?
Any help is appreciated.
Below is what I have so far:
- (IBAction)action
{
NSURL *textURL = [NSURL URLWithString:#"http://www.faithlifefellowship.us/Audio/TotalDownloads.txt"];
NSString *textFromFile = [NSString stringWithContentsOfURL:textURL encoding:NSUTF8StringEncoding error:false];
int click = [textFromFile intValue];
click += 1;
NSString *replace = #"//-";
NSString *clicks = [NSString stringWithFormat:#"%d",click];
replace = [replace stringByAppendingString:clicks];
replace = [replace stringByAppendingString:#"-//"];
//test is a label I used to check to make sure the download count was being read properly.
test.text = clicks;
[replace writeToFile:[[NSBundle mainBundle] pathForResource:#"TotalDownloads" ofType:#"txt"]atomically:YES encoding:NSUTF8StringEncoding error:NULL];
}
Here is the best way I suggest you do this.
Set up a page on your server that accepts a get request.
Code the page to take the value from that get request (which should be the name of the file you are trying to update) and update the file at that path.
From your app, use UIWebView this way:
UIWebView* web = [UIWebView new];
[web loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.mydomain.com/myPage.php"]]];
web = nil;
Presto!