FinderSync Extension - requestBadgeIdentifierForURL is never called - objective-c

I have tested the template provided in Xcode for making a FinderSync Extension. Everything works well except two things:
a) The method requestBadgeIdentifierForURL is never called by the system when a folder is monitored so that badges are not set. What is going wrong here? I am right assuming that this method should be called when I e.g. move or scroll a monitored folder in Finder? By the way the methods beginObservingDirectoryAtURL and endObservingDirectoryAtURL are called properly in this context.
#import "FinderSync.h"
#interface FinderSync ()
#property NSURL *myFolderURL;
#end
#implementation FinderSync
- (instancetype)init {
self = [super init];
NSLog(#"%s launched from %# ; compiled at %s", __PRETTY_FUNCTION__, [[NSBundle mainBundle] bundlePath], __TIME__);
// Set up the directory we are syncing.
self.myFolderURL = [NSURL fileURLWithPath:#"/Users/hmaass/Downloads"];
[FIFinderSyncController defaultController].directoryURLs = [NSSet setWithObject:self.myFolderURL];
// Set up images for our badge identifiers. For demonstration purposes, this uses off-the-shelf images.
[[FIFinderSyncController defaultController] setBadgeImage:[NSImage imageNamed: NSImageNameColorPanel] label:#"Status One" forBadgeIdentifier:#"One"];
[[FIFinderSyncController defaultController] setBadgeImage:[NSImage imageNamed: NSImageNameCaution] label:#"Status Two" forBadgeIdentifier:#"Two"];
return self;
}
#pragma mark - Primary Finder Sync protocol methods
- (void)beginObservingDirectoryAtURL:(NSURL *)url {
// The user is now seeing the container's contents.
// If they see it in more than one view at a time, we're only told once.
NSLog(#"beginObservingDirectoryAtURL:%#", url.filePathURL);
}
- (void)endObservingDirectoryAtURL:(NSURL *)url {
// The user is no longer seeing the container's contents.
NSLog(#"endObservingDirectoryAtURL:%#", url.filePathURL);
}
- (void)requestBadgeIdentifierForURL:(NSURL *)url {
NSLog(#"requestBadgeIdentifierForURL:%#", url.filePathURL);
// For demonstration purposes, this picks one of our two badges, or no badge at all, based on the filename.
NSInteger whichBadge = [url.filePathURL hash] % 3;
NSString* badgeIdentifier = #[#"", #"One", #"Two"][whichBadge];
[[FIFinderSyncController defaultController] setBadgeIdentifier:badgeIdentifier forURL:url];
}
#pragma mark - Menu and toolbar item support
- (NSString *)toolbarItemName {
return #"testfifi";
}
- (NSString *)toolbarItemToolTip {
return #"testfifi: Click the toolbar item for a menu.";
}
- (NSImage *)toolbarItemImage {
return [NSImage imageNamed:NSImageNameCaution];
}
- (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu {
// Produce a menu for the extension.
NSMenu *menu = [[NSMenu alloc] initWithTitle:#""];
[menu addItemWithTitle:#"Example Menu Item" action:#selector(sampleAction:) keyEquivalent:#""];
return menu;
}
- (IBAction)sampleAction:(id)sender {
NSURL* target = [[FIFinderSyncController defaultController] targetedURL];
NSArray* items = [[FIFinderSyncController defaultController] selectedItemURLs];
NSLog(#"sampleAction: menu item: %#, target = %#, items = ", [sender title], [target filePathURL]);
[items enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#" %#", [obj filePathURL]);
}];
}
#end
b) I get the following message in the log console of Xcode when running the template above:
2015-08-25 15:33:00.300 testfifi[855:8134] Failed to connect
(colorGridView) outlet from (NSApplication) to
(NSColorPickerGridView): missing setter or instance variable
2015-08-25 15:33:00.300 testfifi[855:8134] Failed to connect (view)
outlet from (NSApplication) to (NSColorPickerGridView): missing setter
or instance variable 2015-08-25 15:33:00.321 testfifi[855:8134]
-[FinderSync init] launched from /Users/hmaass/Library/Developer/Xcode/DerivedData/testtest-egudnxkifjxirpbrjkohnatmjuro/Build/Products/Debug/testtest.app/Contents/PlugIns/testfifi.appex
; compiled at 20:38:18
Can someone help me to get rid of this message?
Thanks!

I already commented on your question but figured I should post a more complete answer.
It sounds like the issue you're having is another Finder Sync extension is "greedily" observing all folders, most likely the Dropbox Finder Integration. Try disabling all other Finder Sync extensions (under System Preferences -> Extensions -> Finder) and re-run your test.
If this resolves the issue, the problem is that Dropbox (or another app) has already called beginObservingDirectoryAtURL for the folder you're trying to monitor. Unfortunately, Apple's API is lacking in that there is no intelligent logic to who gets to monitor a folder when there are conflicting extensions. Currently, whichever Finder Sync extension starts first will "win".
Dropbox greedily monitors all folders under the user's home directory. I've written to both Apple and Dropbox to address this, but haven't heard any response. Currently, the (ugly) workaround I've implemented is to shutdown known "greedy" extensions, start my own extension, then restart the greedy extension.

Here's the workaround sample code for disabling "greedy" Finder Sync extensions. Nothing fancy, but it works.
(Adding this as a separate answer since it's really just a workaround and isn't necessarily the "correct" answer).
public static void main(String[] args) throws Exception {
String[] greedyFSProcessNames =
new String[] { "com.getdropbox.dropbox.garcon" };
List<String> disabledGreedyFSProcessNames = new ArrayList<>();
for (String greedyFSProcessName : greedyFSProcessNames) {
if (!_isFSProcessRunning(greedyFSProcessName)) {
continue;
}
_enableFSProcess(greedyFSProcessName, false);
disabledGreedyFSProcessNames.add(greedyFSProcessName);
}
_enableFSProcess("com.dejuknow.myfindersync", true);
for (String disabledGreedyFSProcessName :
disabledGreedyFSProcessNames) {
_enableFSProcess(disabledGreedyFSProcessName, true);
}
}
private static boolean _isFSProcessRunning(String processName)
throws Exception {
BufferedReader bufferedReader = null;
try {
Process process = Runtime.getRuntime().exec(
"pluginkit -m -i" + processName);
bufferedReader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
if (line.startsWith("+")) {
return true;
}
else {
return false;
}
}
}
finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
return false;
}
private static void _enableFSProcess(String processName, boolean enable)
throws Exception {
String electionArgument = null;
if (enable) {
electionArgument = "use";
}
else {
electionArgument = "ignore";
}
String[] arguments = new String[] {
"pluginkit", "-e", electionArgument, "-i", processName
};
while (_isFSProcessRunning(processName) != enable) {
Process process = Runtime.getRuntime().exec(arguments);
process.waitFor();
Thread.sleep(100);
}
}

The folders I know Dropbox uses: ~/Dropbox, ~/Documents and ~/Desktop.
I have a FinderSync app too and I can display badges on all of my folders except those. Fortunately the context menu does not seem to suffer any conflict, both extension's menu items are shown.

Related

app broke after Xcode update

I have a Swift app, with some Objective-C code mixed in. It was working yesterday, but this morning I updated XCode and now everything had gone to hell!
Firstly, after updating, I clicked the XCode popup box to allow it to upgrade my app to Swift4. This is where the problems started.
I have a Swift class called RestClient with the following 4 functions (among others):
class func getInstance() -> RestClient {
if (gRestClient == nil) {
let prefs:UserDefaults = UserDefaults.standard
return RestClient(username: prefs.string(forKey: "username")!, password: prefs.string(forKey: "password")!)
}
return gRestClient!
}
class func getUsername() -> String {
if (gUsername == nil) {
let prefs:UserDefaults = UserDefaults.standard
gUsername = prefs.string(forKey: "username")!
}
return gUsername!
}
class func getPassword() -> String {
if (gPassword == nil) {
let prefs:UserDefaults = UserDefaults.standard
gPassword = prefs.string(forKey: "password")!
}
return gPassword!
}
public func getServer() -> String {
return MAIN_SERVER;
}
Then in my /Objective-C/ folder, I have some more files, once of which is called RestClientObj.m. In here, I have this lines of code:
NSString* url = [NSString stringWithFormat:#"%#/receipt/email/%#/%#/", [[RestClient getInstance] getServer], rrn, emailAddress];
NSString *authStr = [NSString stringWithFormat:#"%#:%#", [RestClient getUsername], [RestClient getPassword]];
So as you can see, I'm calling the RestClient.swift from here. The RestClientObj.h is as follows:
#ifndef RestClientObj_h
#define RestClientObj_h
#endif /* ResClientObj_h */
#interface RestClientObj : NSObject {
}
+(BOOL) sendSMS:(NSString *)rrn mn:(NSString *)mobileNumber;
+(BOOL) sendEmail:(NSString *)rrn mn:(NSString *)emailAddress;
#end
This whole upgrade is causing other problems too. I have another class with the following error:
No visible #interface for 'AppDelegate' declares the selector 'managedObjectContext'
on the line:
appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext]; <-ERROR
Can anyone shed any light on this?
EDIT: Here's some code from the AppDelegate class:
lazy var managedObjectContext: NSManagedObjectContext? = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if coordinator == nil {
return nil
}
var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
Just to close this off.
The issue was missing the #obj identifier before the variable declaration to make it visible to my objective-c code, in combination with the XCode Swift Upgrade wizard renaming some functions.

Support NSDocument changes in an external editor?

I have an NSDocument with some simple code:
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
self.string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return YES;
}
If I change the file in an external editor, how do I get notified of this so I can handle it? I assume there is something built in for this, but I can't find it.
I'm looking for something built into NSDocument. I'm aware of FSEvent, but that seems too low level to do something very common for most document-based apps.
Since OS X v10.7, NSDocument provides a far simpler mechanism you can override in subclasses: -presentedItemDidChange.
Handling -presentedItemDidChange, Ignoring Metadata Changes
Just relying on this callback can produce false positives, though, when metadata change. That got on my nerves quickly for files stored in Dropbox, for example.
My approach to deal with this in general, in Swift, is like this:
class MyDocument: NSDocument {
// ...
var canonicalModificationDate: Date!
override func presentedItemDidChange() {
guard fileContentsDidChange() else { return }
guard isDocumentEdited else {
DispatchQueue.main.async { self.reloadFromFile() }
return
}
DispatchQueue.main.async { self.showReloadDialog() }
}
fileprivate func showReloadDialog() {
// present alert "do you want to replace your stuff?"
}
/// - returns: `true` if the contents did change, not just the metadata.
fileprivate func fileContentsDidChange() -> Bool {
guard let fileModificationDate = fileModificationDateOnDisk()
else { return false }
return fileModificationDate > canonicalModificationDate
}
fileprivate func fileModificationDateOnDisk() -> Date? {
guard let fileURL = self.fileURL else { return nil }
let fileManager = FileManager.default
return fileManager.fileModificationDate(fileURL: fileURL)
}
}
Now you have to update the canonicalModificationDate in your subclass, too:
In a callback from the "do you want to replace contents?" alert which I call -ignoreLatestFileChanges so you don't nag your user ad infitium;
In -readFromURL:ofType:error: or however you end up reading in contents for the initial value;
In -dataOfType:error: or however you produce contents to write to disk.
You want to register with the FSEvents API. Since 10.7, you can watch arbitrary files.
Potential duplicate of this question.
When I open a document in my document-based app, edit in in another application, and switch back to my app, the same method that you mentioned (readFromData:ofType:error:) is called with the new data. This method is called when you restore a previous version from the Versions browser, too.
You could then add a boolean instance variable to check whether it's being called because of an external update (in my case, I check whether one of my IBOutlets is initialized: if it's not, the document is being loaded for the first time). You might want to move your code that makes use of the string instance variable into some method that you can call if the document is already initialized, like this:
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
self.string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (self.isLoaded)
[self documentChanged];
return YES;
}
- (void)windowControllerDidLoadNib:(FCWindowController *)windowController {
self.isLoaded = YES;
[self documentChanged];
}
- (void)documentChanged {
// use self.string as you like
]
NSMetadataQuery seems to be the best way to monitor file and folder changes without polling and with a low cpu overhead.
Some basic code for watching a folder, you'd just want to set the filePattern to the filename and not the wildcard *
NSString* filePattern = [NSString stringWithFormat:#"*"];
NSString *watchedFolder = #"not/fake/path";
NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
[query setSearchScopes:#[watchedFolder]];
NSString *itemName = (NSString*)kMDItemFSName;
[query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE %#", NSMetadataItemDisplayNameKey, filePattern]];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(queryFoundStuff:) name:NSMetadataQueryDidFinishGatheringNotification object:query];
[nc addObserver:self selector:#selector(queryFoundStuff:) name:NSMetadataQueryDidUpdateNotification object:query];
[query setNotificationBatchingInterval:0.5];
[query startQuery];
- (void)queryFoundStuff:(NSNotification *)notification {
[query disableUpdates];
NSLog(#"Notification: %#", notification.name);
NSMutableArray *results = [NSMutableArray arrayWithCapacity:query.resultCount];
for (NSUInteger i=0; i<query.resultCount; i++) {
[results addObject:[[query resultAtIndex:i] valueForAttribute:NSMetadataItemPathKey]];
}
// file has updated, do something
[query enableUpdates];
}
I've never been able to find an ideal solution to watching files for updates, NSFilePresenter sounds like it should be the appropriate high level solution, but from what I can tell it only works if the file is being edited by another App using NSFilePresenter also. I've also tried VDKQueue and SCEvents which wrap low level kernel events but have a cpu overhead.

Changing cocoa bindings value programmatically

I have an NSButton in my preferences to interact with adding the application to the LoginItems. If the adding of the login item fails, I want to uncheck the box so the user doesn't get a false sense that it was added to the login items. However, after doing this, when I click the checkbox again, the bindings is not triggered.
- (void)addLoginItem:(BOOL)status
{
NSURL *url = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:
#"Contents/Library/LoginItems/HelperApp.app"];
// Registering helper app
if (LSRegisterURL((__bridge CFURLRef)url, true) != noErr) {
NSLog(#"LSRegisterURL failed!");
}
if (!SMLoginItemSetEnabled((__bridge CFStringRef)[[NSBundle mainBundle] bundleIdentifier], (status) ? true : false)) {
NSLog(#"SMLoginItemSetEnabled failed!");
[self willChangeValueForKey:#"startAtLogin"];
[self.startAtLogin setValue:[NSNumber numberWithBool:[self automaticStartup]] forKey:#"state"];
[self didChangeValueForKey:#"startAtLogin"];
}
}
- (void)setAutomaticStartup:(BOOL)state
{
NSLog(#"Set automatic startup: %d", state);
if ([self respondsToSelector:#selector(addLoginItem:)]) {
[self addLoginItem:state];
}
}
- (BOOL)automaticStartup
{
BOOL isEnabled = NO;
// the easy and sane method (SMJobCopyDictionary) can pose problems when sandboxed. -_-
CFArrayRef cfJobDicts = SMCopyAllJobDictionaries(kSMDomainUserLaunchd);
NSArray* jobDicts = CFBridgingRelease(cfJobDicts);
if (jobDicts && [jobDicts count] > 0) {
for (NSDictionary* job in jobDicts) {
if ([[[NSBundle mainBundle] bundleIdentifier] isEqualToString:[job objectForKey:#"Label"]]) {
isEnabled = [[job objectForKey:#"OnDemand"] boolValue];
break;
}
}
}
NSLog(#"Is Enabled: %d", isEnabled);
// if (isEnabled != _enabled) {
[self willChangeValueForKey:#"startupEnabled"];
startupEnabled = isEnabled;
[self didChangeValueForKey:#"startupEnabled"];
// }
return isEnabled;
}
I have my databinding for the checkbox bound to self.automaticStartup. If I remove the line [self.startAtLogin setValue:[NSNumber numberWithBool:[self automaticStartup]] forKey:#"state"]; then the bindings work fine, but it doesn't uncheck, if the adding of the item fails.
How can I change this binding value programmatically so that every other binding event is not ignored?
From your explanation, your bound value is automaticStartup, but you are sending willChangeValueForKey: for startAtLogin. In order for bindings to work correctly, you need to alert on the change to the bound variable at some point. However, since you are in the midst of setAutomaticStartup: at the time, it's not really safe to do that here.
In this case, I would not use bindings to perform the change itself, I would consider the old-style IBAction mechanism and then set the checkbox value manually through an IBOutlet when you can confirm the status.

NSDocument saveDocumentWithDelegate deadlocked during App termination

NSDocument continues to be a software maintenance nightmare.
Anyone else having a problem where they want certain blocking dialogs to be handled SYNCHRONOUSLY?
BEGIN EDIT: I may have found a solution that allows me to wait synchronously
Can anyone verify that this would be an "Apple approved" solution?
static BOOL sWaitingForDidSaveModally = NO;
BOOL gWaitingForDidSaveCallback = NO; // NSDocument dialog calls didSave: when done
...
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
if ( gWaitingForDidSaveCallback )
{
// first, dispatch any other potential alerts synchronously
while ( gWaitingForDidSaveCallback && [NSApp modalWindow] )
[NSApp runModalForWindow: [NSApp modalWindow]];
if ( gWaitingForDidSaveCallback )
{
sWaitingForDidSaveModally = YES;
[NSApp runModalForWindow: [NSApp mbWindow]]; // mbWindow is our big (singleton) window
sWaitingForDidSaveModally = NO;
}
}
...
- (void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
[self recordLastSaveURL];
gWaitingForDidSaveCallback = NO;
if ( sWaitingForDidSaveModally )
[NSApp stopModal];
}
END EDIT
I have to support Snow Leopard/Lion/ML
App termination is an ugly process.
When the user decides to quit, and the document has changes that need saving, I call this:
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
I really really really want this call to be synchronous, but in latest Lion, this hangs my app:
while ( gWaitingForDidSaveCallback )
{
// didSave: callback clears sWaitingForDidSaveCallback
// do my own synchronous wait for now
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0.05]];
}
My best guess for the hang is that the mouseDown: of a window close button
is confusing the NSDocument.
So now, I have to return, and pepper my apps main loop with unmaintainable state machine logic to prevent user from executing various dangerous hotkeys.
Ok, so I grin and bear it, and run into yet another roadblock!
In previous OS versions/SDKs, [NSApp modalWindow] would return a window when it
was in this state. Now it doesn't! Grrrrr...
NSDocument has no API to test when it is in this state!
So, now there is no mechanism to globally check this state!
I have to add yet another state variable to my state machine.
Anyone have a cleaner solution for this problem that works in all OS versions and all present (and future) SDKs?
The better way is to save unsaved documents in chain. It is very easy:
// Catch application terminate event
-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
for (NSInteger i = 0; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:i];
if ([doc isDocumentEdited])
{
// Save first unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:i + 1])]; // Next document
return NSTerminateLater; // Wait until last document in chain will be saved
}
}
return NSTerminateNow; // All documents are saved or there are no open documents. Terminate.
}
...
// Document saving finished
-(void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
if (didSave) // Save button pressed
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
NSInteger nextIndex = [(__bridge NSNumber *)contextInfo integerValue];
for (NSInteger i = nextIndex; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:nextIndex];
if ([doc isDocumentEdited])
{
// Save next unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:nextIndex + 1])]; // Next document
return;
}
}
[NSApp replyToApplicationShouldTerminate:YES]; // All documents saved. Terminate.
}
else [NSApp replyToApplicationShouldTerminate:NO]; // Saving canceled. Terminate canceled.
}
Maybe this answer is too late to be useful but... In one of my apps I implemented -(IBAction)terminate:(id)sender in my NSApplication derived class which would conditionally call [super terminate] to actually close the application only if all open documents were cleanly saved. I may have found some of this in the Apple docs or other examples.
The terminate override will go through each document and either close it (because it's saved), or call the document's canCloseDocumentWithDelegate method in the NSDocument derived class passing 'self' and 'terminate' as the didSaveSelector. Since the terminate method falls through and does nothing except make the document present an NSAlert, the alert in the document class will callback and re-run the terminate routine if the user clicks YES or NO. If all documents are clean, the app will terminate since [super terminate] will get called. If any more dirty documents exist, the process repeats.
For example:
#interface MyApplication : NSApplication
#end
#implementation MyApplication
- (IBAction)terminate:(id)sender
{
//Loop through and find any unsaved document to warn the user about.
//Close any saved documents along the way.
NSDocument *docWarn = NULL;
NSArray *documents = [[NSDocumentController sharedDocumentController] documents];
for(int i = 0; i < [documents count]; i++)
{
NSDocument *doc = [documents objectAtIndex:i];
if([doc isDocumentEdited])
{
if(docWarn == NULL || [[doc windowForSheet] isKeyWindow])
docWarn = doc;
}
else
{
//close any document that doesn't need saving. this will
//also close anything that was dirty that the user answered
//NO to on the previous call to this routine which triggered
//a save prompt.
[doc close];
}
}
if(docWarn != NULL)
{
[[docWarn windowForSheet] orderFront:self];
[[docWarn windowForSheet] becomeFirstResponder];
[docWarn canCloseDocumentWithDelegate:self shouldCloseSelector:#selector(terminate:) contextInfo:NULL];
}
else
{
[super terminate:sender];
}
}
#end
Later in the document derived class:
typedef struct {
void * delegate;
SEL shouldCloseSelector;
void *contextInfo;
} CanCloseAlertContext;
#interface MyDocument : NSDocument
#end
#implementation MyDocument
- (void)canCloseDocumentWithDelegate:(id)inDelegate shouldCloseSelector:(SEL)inShouldCloseSelector contextInfo:(void *)inContextInfo
{
// This method may or may not have to actually present the alert sheet.
if (![self isDocumentEdited])
{
// There's nothing to do. Tell the delegate to continue with the close.
if (inShouldCloseSelector)
{
void (*callback)(id, SEL, NSDocument *, BOOL, void *) = (void (*)(id, SEL, NSDocument *, BOOL, void *))objc_msgSend;
(callback)(inDelegate, inShouldCloseSelector, self, YES, inContextInfo);
}
}
else
{
NSWindow *documentWindow = [self windowForSheet];
// Create a record of the context in which the panel is being
// shown, so we can finish up when it's dismissed.
CanCloseAlertContext *closeAlertContext = malloc(sizeof(CanCloseAlertContext));
closeAlertContext->delegate = (__bridge void *)inDelegate;
closeAlertContext->shouldCloseSelector = inShouldCloseSelector;
closeAlertContext->contextInfo = inContextInfo;
// Present a "save changes?" alert as a document-modal sheet.
[documentWindow makeKeyAndOrderFront:nil];
NSBeginAlertSheet(#"Would you like to save your changes?", #"Yes", #"Cancel", #"No", documentWindow, self,
#selector(canCloseAlertSheet:didEndAndReturn:withContextInfo:), NULL, closeAlertContext, #"%");
}
}
- (void)canCloseAlertSheet:(NSWindow *)inAlertSheet didEndAndReturn:(int)inReturnCode withContextInfo:(void *)inContextInfo
{
CanCloseAlertContext *canCloseAlertContext = inContextInfo;
void (*callback)(id, SEL, NSDocument *, BOOL, void* ) = (void (*)(id, SEL, NSDocument *, BOOL, void* ))objc_msgSend;
if (inAlertSheet) [inAlertSheet orderOut:self];
// The user's dismissed our "save changes?" alert sheet. What happens next depends on how the dismissal was done.
if (inReturnCode==NSAlertAlternateReturn)
{
//Cancel - do nothing.
}
else if (inReturnCode==NSAlertDefaultReturn)
{
//Yes - save the current document
[self saveDocumentWithDelegate:(__bridge id)canCloseAlertContext->delegate
didSaveSelector:canCloseAlertContext->shouldCloseSelector contextInfo:canCloseAlertContext->contextInfo];
}
else
{
// No - just clear the dirty flag and post a message to
// re-call the shouldCloseSelector. This should be
// the app:terminate routine.
[self clearDirtyFlag];
if (canCloseAlertContext->shouldCloseSelector)
{
(callback)((__bridge id)canCloseAlertContext->delegate,
canCloseAlertContext->shouldCloseSelector, self, YES, canCloseAlertContext->contextInfo);
}
}
// Free up the memory that was allocated in -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:.
free(canCloseAlertContext);
}
#end
And that should do it - No loops... no waiting...

How to pass arguments to app built on Phonegap

I'm writing an app using JQM and Phonegap to deploy on iOS and I need it to read input arguments like a url arguments of a common website does in javascript by handling the object 'window.location.search'
In my situation, the app will be launched from a website, like this:
My App
This is working right now, I can already call my app, what I need now is to read the arguments arg1, arg2, etc. I've tried reading window.location.search but with no luck.
How can I do this? Do I need to write some Objective C code?
Any suggestions would be appreciated.
Thanks.
My problem was solved using the content of this link: https://gist.github.com/859540
The code is:
Objective-c part:
In MainViewController.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// perform any custom startup stuff you need to ...
// process your launch options
NSArray *keyArray = [launchOptions allKeys];
if ([launchOptions objectForKey:[keyArray objectAtIndex:0]]!=nil)
{
// we store the string, so we can use it later, after the webView loads
NSURL *url = [launchOptions objectForKey:[keyArray objectAtIndex:0]];
self.invokeString = [url absoluteString];
NSLog(#amp;" launchOptions = %#",url); // if you want to see what is happening
}
// call super, because it is super important ( 99% of phonegap functionality starts here )
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void) webViewDidFinishLoad:(UIWebView*) theWebView
{
// only valid if ___PROJECTNAME__-Info.plist specifies a protocol to handle
if (self.invokeString)
{
// this is passed before the deviceready event is fired, so you can access it in js when you receive deviceready
NSString* jsString = [NSString stringWithFormat:#"var invokeString = \"%#\";", self.invokeString];
[theWebView stringByEvaluatingJavaScriptFromString:jsString];
}
// Black base color for background matches the native apps
theWebView.backgroundColor = [UIColor blackColor];
return [super webViewDidFinishLoad:theWebView];
}
In the index.html file using cordova-1.7.0:
function onDeviceReady()
{
alert(invokeString);
}
alert returned: myapp://?arg1=1&arg2=2
just the same string used to call it ... :)
I had the same issue, everything here in these answers is hella confusing and extra information.
Understanding and solving the problem in 2 easy steps:
Informative (you can skip if you don't care what happens in the background): Go to AppDelegate.m in Clases folder in the project and search for "handleOpenUrl", you should notice some code there with comments explaining what's up. I don't know objective-c, but intuitively that code there looks for window.handleOpenURL function and calls it giving it the parameter of the url called (e.g. 'myapp:///?parameter=value')
Basically all you have to do is globally(in window object) define the function handleOpenURL
function handleOpenURL (url) {
alert(url);
}
Note that this only gets executed when your app is opened with an
..
window.location will be the location of your phonegap index.html file, not the URL that was used to launch your app.
Some web searches suggested that a function called:
function handleOpenUrl(url) {
alert("opened from url " + url);
}
.. might automatically be called . I don't have my dev machine here to test though, Sorry!
If this doesn't work in Objective-C check out the handleOpenUrl method of the AppDelegate.m This gets called when your app is opened with a URL Scheme.
you should do it in obj-c and then with a plugin pass it to javascript code :
for doing it in obj-c first you should implement
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURL *urlToParse = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey];
if (urlToParse) {
[self application:application handleOpenURL:urlToParse];
}
return YES;
}
and then you could access parameters like this :
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
if ([[url scheme] isEqualToString:#"myapp"]) {
//in here you do whatever you need the app to do
// e.g decode JSON string from base64 to plain text & parse JSON string
}
return YES; //if everything went well
}
function getParameterByName(name)
{
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(window.location.href);
if(results == null)
return "";
else
return results[1];
}
Call this function like var para1 = getParameterByName("para1");
on pageshow event in jquery mobile.
$('#page').on('pageshow',function(event){
var para1 = getParameterByName("para1");
});