Loading and unloading of NSBundle - objective-c

I'm building pluggable interface for my application. I stuck with an updates mechanism for the plugins.
Here is the code that confuses me.
- (void) unloadBundle{
[_pluginInstance release], _pluginInstance = nil;
[[self bundle] unload];
[_bundle release], _bundle = nil;
}
- (void) loadBundleWithURL:(NSURL *)bundleURL{
NSBundle *newBundle = [NSBundle bundleWithURL:bundleURL];
if (newBundle){
[self setBundle:newBundle];
[self setPluginInstance:[[[[self.bundle principalClass] alloc] init] autorelease]];
NSLog(#"New bundle: %#", self.bundle);
NSLog(#"New bundle's principal class %#", [self.bundle principalClass]);
NSLog(#"Principal class' bundle is %#", [NSBundle bundleForClass:[self.bundle principalClass]]);
NSLog(#"Plugin's class %#", [self.pluginInstance class]);
}
}
These are the methods of my wrapper around the plugin principal class. I just call unloadBundle and then loadBundleWithURL with the URL to the new version of the bundle. When executes it logs the following into console:
New bundle: NSBundle </Users/prudnikov/Work/Projects/***/Name.pluginextension> (loaded)
New bundle's principal class MyPluginClass
Principal class' bundle is NSBundle </Users/prudnikov/Library/Application Support/MyApp/PlugIns/Name.pluginextension> (not yet loaded)
Plugin's class MyPluginClass
Means that I take principal class from the new bundle, get its bundle with [NSBundle bundleForClass:] and it is old bundle.
Any ideas what I'm doing wrong?

The problem in this case was that I forgot to unload bundle in different place. I had method that was verifying that bundle is a valid plugin's bundle.
Calling principalClass loads the bundle automatically. So, calling unload is required.

Related

Communication between Main Application and loadable bundle

OK, it's rather self-explanatory.
Let's say we've got our cocoa application.
Let's all assume that you've already got some "plugins", packaged as independent loadable bundles.
This is how bundles are currently being loaded (given that the plugin's "principal class" is actually an NSWindowController subclass :
// Load the bundle
NSString* bundlePath = [[NSBundle mainBundle] pathForResource:#"BundlePlugin"
ofType:#"bundle"]
NSBundle* b = [NSBundle bundleWithPath:bundlePath];
NSError* err = nil;
[b loadAndReturnError:&err];
if (!err)
{
// if everything goes fine, initialise the main controller
Class mainWindowControllerClass = [b principalClass];
id mainWindowController = [[mainWindowControllerClass alloc] initWithWindowNibName:#"PluginWindow"];
}
Now, here's the catch :
How do I "publish" some of my main app's objects to the plugin?
How do I make my plugin "know" about my main app's classes?
Is it even possible to actually establish some sort of communication between them, so that e.g. the plugin can interact with my main app? And if so, how?
SIDENOTES :
Could using Protocols eliminate the "unknown selector" errors and the need for extensive use of performSelector:withObject:?
Obviously I can set and get value to-and-from my newly created mainWindowController as long as they're defined as properties. But is this the most Cocoa-friendly way?

How do I load a nib from a secondary process?

I have a process spawned with NSTask that needs to display a window. I started out just writing all the UI code by hand, but this has become a pain.
So, I created a new class with a xib, MyWindowController. I want to load up an instance of this controller in secondary process and have all the IBOutlets and whatnot work properly.
Here's what I've got so far:
// Get the bundle for the main application (not the subprocess).The executable lives in Contents/Helpers, so look two dirs up from its path for the main app bundle root.
NSArray *executablePathComponents = [[[NSBundle mainBundle] executableURL] pathComponents];
NSIndexSet *indexOfEveryComponentExceptLastTwo = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [executablePathComponents count] - 2)];
NSBundle *myBundle = [NSBundle bundleWithURL:[NSURL fileURLWithPathComponents:[executablePathComponents objectsAtIndexes:indexOfEveryComponentExceptLastTwo]]];
// Load the controller nib.
NSNib *windowControllerNib = [[NSNib alloc] initWithNibNamed:#"MyWindowController" bundle:myBundle];
MyWindowController *windowController = [[MyWindowController alloc] init];
NSArray *topLevelObjects = nil;
[windowControllerNib instantiateNibWithOwner:windowController topLevelObjects:topLevelObjects];
This gives me an instance of the window controller and it displays the window from the nib on screen, so this appears to work. BUT, instantiateNibWithOwner:topLevelObjects is deprecated in favor of instantiateNibWithOwner:topLevelObjects.
Using the non-deprecated method results in an exception: "-[NSNib instantiateWithOwner:topLevelObjects:]: unrecognized selector sent to instance 0x10291ab1"
At the very least I'd like to not use the deprecated method. But maybe there is a better way to approach this whole thing?

Calling -[NSFileManager setUbiquitous:itemAtURL:destinationURL:error:] never returns

I have a straightforward NSDocument-based Mac OS X app in which I am trying to implement iCloud Document storage. I'm building with the 10.7 SDK.
I have provisioned my app for iCloud document storage and have included the necessary entitlements (AFAICT). The app builds, runs, and creates the local ubiquity container Documents directory correctly (this took a while, but that all seems to be working). I am using the NSFileCoordinator API as Apple recommended. I'm fairly certain I am using the correct UbiquityIdentifier as recommended by Apple (it's redacted below tho).
I have followed Apple's iCloud Document storage demo instructions in this WWDC 2011 video closely:
Session 107 AutoSave and Versions in Lion
My code looks almost identical to the code from that demo.
However, when I call my action to move the current document to the cloud, I experience liveness problems when calling the -[NSFileManager setUbiquitous:itemAtURL:destinationURL:error:] method. It never returns.
Here is the relevant code from my NSDocument subclass. It is almost identical to Apple's WWDC demo code. Since this is an action, this is called on the main thread (as Apple's demo code showed). The deadlock occurs toward the end when the -setUbiquitous:itemAtURL:destinationURL:error: method is called. I have tried moving to a background thread, but it still never returns.
It appears that a semaphore is blocking while waiting for a signal that never arrives.
When running this code in the debugger, my source and destination URLs look correct, so I'm fairly certain they are correctly calculated and I have confirmed the directories exist on disk.
Am I doing anything obviously wrong which would lead to -setUbiquitous never returning?
- (IBAction)moveToOrFromCloud:(id)sender {
NSURL *fileURL = [self fileURL];
if (!fileURL) return;
NSString *bundleID = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleIdentifier"];
NSString *appID = [NSString stringWithFormat:#"XXXXXXX.%#.macosx", bundleID];
BOOL makeUbiquitous = 1 == [sender tag];
NSURL *destURL = nil;
NSFileManager *mgr = [NSFileManager defaultManager];
if (makeUbiquitous) {
// get path to local ubiquity container Documents dir
NSURL *dirURL = [[mgr URLForUbiquityContainerIdentifier:appID] URLByAppendingPathComponent:#"Documents"];
if (!dirURL) {
NSLog(#"cannot find URLForUbiquityContainerIdentifier %#", appID);
return;
}
// create it if necessary
[mgr createDirectoryAtURL:dirURL withIntermediateDirectories:NO attributes:nil error:nil];
// ensure it exists
BOOL exists, isDir;
exists = [mgr fileExistsAtPath:[dirURL relativePath] isDirectory:&isDir];
if (!(exists && isDir)) {
NSLog(#"can't create local icloud dir");
return;
}
// append this doc's filename
destURL = [dirURL URLByAppendingPathComponent:[fileURL lastPathComponent]];
} else {
// get path to local Documents folder
NSArray *dirs = [mgr URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
if (![dirs count]) return;
// append this doc's filename
destURL = [[dirs objectAtIndex:0] URLByAppendingPathComponent:[fileURL lastPathComponent]];
}
NSFileCoordinator *fc = [[[NSFileCoordinator alloc] initWithFilePresenter:self] autorelease];
[fc coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForMoving writingItemAtURL:destURL options:NSFileCoordinatorWritingForReplacing error:nil byAccessor:^(NSURL *fileURL, NSURL *destURL) {
NSError *err = nil;
if ([mgr setUbiquitous:makeUbiquitous itemAtURL:fileURL destinationURL:destURL error:&err]) {
[self setFileURL:destURL];
[self setFileModificationDate:nil];
[fc itemAtURL:fileURL didMoveToURL:destURL];
} else {
NSWindow *win = ... // get my window
[self presentError:err modalForWindow:win delegate:nil didPresentSelector:nil contextInfo:NULL];
}
}];
}
I don't know if these are the source of your problems, but here are some things I'm seeing:
-[NSFileManager URLForUbiquityContainerIdentifier:] may take a while, so you shouldn't invoke it on the main thread. see the "Locating the Ubiquity Container" section of this blog post
Doing this on the global queue means you should probably use an allocated NSFileManager and not the +defaultManager.
The block passed to the byAccessor portion of the coordinated write is not guaranteed to be called on any particular thread, so you shouldn't be manipulating NSWindows or presenting modal dialogs or anything from within that block (unless you've dispatched it back to the main queue).
I think pretty much all of the iCloud methods on NSFileManager will block until things complete. It's possible that what you're seeing is the method blocking and never returning because things aren't configured properly. I'd double and triple check your settings, maybe try to simplify the reproduction case. If it still isn't working, try filing a bug or contacting DTS.
Just shared this on Twitter with you, but I believe when using NSDocument you don't need to do any of the NSFileCoordinator stuff - just make the document ubiquitous and save.
Hmm,
did you try not using a ubiquity container identifier in code (sorry - ripped out of a project so I've pseudo-coded some of this):
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *iCloudDocumentsURL = [[fm URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:#"Documents"];
NSURL *iCloudFileURL = [iCloudDocumentsURL URLByAppendingPathComponent:[doc.fileURL lastPathComponent]];
ok = [fm setUbiquitous:YES itemAtURL:doc.fileURL destinationURL:iCloudRecipeURL error:&err];
NSLog(#"doc moved to iCloud, result: %d (%#)",ok,doc.fileURL.fileURL);
And then in your entitlements file:
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>[devID].com.yourcompany.appname</string>
</array>
Other than that, your code looks almost identical to mine (which works - except I'm not using NSDocument but rolling it all myself).
If this is the first place in your code that you are accessing iCloud look in Console.app for a message like this:
taskgated: killed yourAppID [pid 13532] because its use of the com.apple.developer.ubiquity-container-identifiers entitlement is not allowed
Anytime you see this message delete your apps container ~/Library/Containers/<yourAppID>
There may also be other useful messages in Console.app that will help you solve this issue.
I have found that deleting the app container is the new Clean Project when working with iCloud.
Ok, So I was finally able to solve the problem using Dunk's advice. I'm pretty sure the issue I was having is as follows:
Sometime after the WWDC video I was using as a guide was made, Apple completed the ubiquity APIs and removed the need to use an NSFileCoordinator object while saving from within an NSDocument subclass.
So the key was to remove both the creation of the NSFileCoordinator and the call to -[NSFileCoordinator coordinateWritingItemAtURL:options:writingItemAtURL:options:error:byAccessor:]
I also moved this work onto a background thread, although I'm fairly certain that was not absolutely required to fix the issue (although it was certainly a good idea).
I shall now submit my completed code to Google's web crawlers in hopes of assisting future intrepid Xcoders.
Here's my complete solution which works:
- (IBAction)moveToOrFromCloud:(id)sender {
NSURL *fileURL = [self fileURL];
if (!fileURL) {
NSBeep();
return;
}
BOOL makeUbiquitous = 1 == [sender tag];
if (makeUbiquitous) {
[self displayMoveToCloudDialog];
} else {
[self displayMoveFromCloudDialog];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doMoveToOrFromCloud:makeUbiquitous];
});
}
- (void)doMoveToOrFromCloud:(BOOL)makeUbiquitous {
NSURL *fileURL = [self fileURL];
if (!fileURL) return;
NSURL *destURL = nil;
NSFileManager *mgr = [[[NSFileManager alloc] init] autorelease];
if (makeUbiquitous) {
NSURL *dirURL = [[MyDocumentController instance] ubiquitousDocumentsDirURL];
if (!dirURL) return;
destURL = [dirURL URLByAppendingPathComponent:[fileURL lastPathComponent]];
} else {
// move to local Documentss folder
NSArray *dirs = [mgr URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
if (![dirs count]) return;
destURL = [[dirs firstObject] URLByAppendingPathComponent:[fileURL lastPathComponent]];
}
NSError *err = nil;
void (^completion)(void) = nil;
if ([mgr setUbiquitous:makeUbiquitous itemAtURL:fileURL destinationURL:destURL error:&err]) {
[self setFileURL:destURL];
[self setFileModificationDate:nil];
completion = ^{
[self hideMoveToFromCloudDialog];
};
} else {
completion = ^{
[self hideMoveToFromCloudDialog];
NSWindow *win = [[self canvasWindowController] window];
[self presentError:err modalForWindow:win delegate:nil didPresentSelector:nil contextInfo:NULL];
};
}
dispatch_async(dispatch_get_main_queue(), completion);
}

Nib files cannot be loaded by name from main bundle

It seems as though my nib files are included in my test target, they don't exist in the main bundle, so my app crashes on me when I am loding a nib by its name from the main bundle. I either need to find the correct bundle that includes my nib file, or I need to load a nib using a path.
Does anyone have a solution for either one? [NSBundle bundleForClass:[self class]] doesn't work. I think the nib and class files are not in the same bundle
It might help to enumerate the bundles
for (NSBundle *bundle in [NSBundle allBundles])
{
// can look for resources in bundle
locatedPath = [bundle pathForResource:resourcePath ofType:type];
// or maybe trying and load the nib from it?
UINib *nib = [UINib nibWithName:#"Blah" bundle:bundle];
// check for !nil ...
}

How to Determine/Control Where Managed Object Model (.mom) File is Stored?

Ey guys, so I have this preloader (Xcode commandline utility project) that preloads my iPhone app's sqlite store and it has been working just fine until now as it is always giving me an incompatible store error. So instead of referencing the .mom file within the preloader I am going to create it within the preloader to hopefully bypass this issue. This is where my question comes in, how do I determine where the preloader is storing the .mom file I am having it create? And is there any way I can tell it where to place it?
Below are some relevant snippets of code.
From within the iPhone app:
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created by merging all of the models found in the application bundle.
*/
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
return managedObjectModel;
}
//managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
NSURL *modelURL = [NSURL fileURLWithPath:#"path/to/Parking.mom"];
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
#pragma mark TODO change below line to search for your project's mom file
NSString *path = [[NSBundle mainBundle] pathForResource:#"Parking" ofType:#"mom"];
return managedObjectModel;
}
From within the preloader (Xcode commandline utility project):
NSManagedObjectModel *managedObjectModel(NSString* momPath) {
static NSManagedObjectModel *model = nil;
if (model != nil) {
return model;
}
model = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
//NSURL *modelURL = [NSURL fileURLWithPath:momPath];
//model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return model;
}
If you look closely to the code you can see where I swapped what was in each file.
Thanks in advance!
Use -initWithContentsOfURL: to read a managed object model file from wherever you like. In fact, just doing that much might resolve your error... it may be that you've got an old model file in your bundle, and using +mergedModelFromBundles: is adding it to your other model files.