I would like to know how can I open a file with my OS X application, which I wrote in Objective-C. I registered the file types in Info.plist and I have application:openFile: in my code. I did everything by this post, which was marked as solved.
The problem is that this works only if I drag and drop my file on my application while it is running. But it doesn't work if I just double click on my file. It starts my application, but not as it would start if I would drag and drop. So the code which is in application:openFile: doesn't run when double-clicked, but only when I drag and drop my file.
EDIT:
Some more detail about my code, and what I am trying to achieve.
I created a wrapper application for an other app. Let's call the other app the "HelperApp.app". This HelperApp is inside the /Contents/ folder of my wrapper app. With the wrapper app I specified a new file type, let's call it ".ha" in the Info.plist file. This file contains some argument commands. What I try to achieve, that when a user clicks on a file which is a ".ha" file, then my wrapper app reads in the argument from this file and sets it for the HelperApp, then starts the HelperApp. This HelperApp is opening different things depending on the argument it gets. Below you can check my code.
I have an AppDelegate.h and an AppDelegate.mm by default how the newest Xcode creates it. I added this line to my AppDelegate.h, just before the "#end":
- (BOOL)processFile:(NSString *)file;
I have these functions in my AppDelegate.mm:
#import "AppDelegate.h"
#import "ArgumentParser.h"
#implementation AppDelegate
- (void)dealloc
{
[super dealloc];
}
- (BOOL)application:(NSApplication *)WrapperApp openFile:(NSString *)filename
{
return [self processFile:filename];
}
- (BOOL)processFile:(NSString *)file
{
NSLog(#"The following file has been dropped or selected: %#",file);
std::string path = [file cStringUsingEncoding:[NSString defaultCStringEncoding]];
ArgumentParser parser = ArgumentParser();
parser.getArgumentfromFile(path);
parser.setArgumentinFile(); // <== This is the "corrupted" function
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *helperAppPath = [[mainBundle bundlePath]
stringByAppendingString:#"/Contents/HelperApp.app"];
[[NSWorkspace sharedWorkspace] launchApplication:helperAppPath];
return YES;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
}
The corrupted function - setArgumentinFile():
void ArgumentParser::setArgumentinFile() {
std::string content = ""; // The file content
std::fstream file;
file.open("HelperApp.app/Contents/Wrapper/HelperApp.app/Contents/Info.plist");
// Open the file and modify the arguments
if(file.is_open()) {
std::stringstream stream;
stream << file.rdbuf();
std::string line = "";
bool isIt = false;
while(getline(stream, line)) {
// This line is the argument list, which needs to be modifyed
if(isIt) {
int index = (int)line.find_last_of("<");
std::string start = line.substr(0, index);
std::string end = line.substr(index, std::string::npos);
std::string argument_list = start + " " + _argument + end;
content += argument_list + '\n';
isIt = false;
}
// Save the rest of the file so we can overwrite it
else {
content += line + '\n';
}
// Next line is the argument list
if(line.find("WineProgramArguments") != std::string::npos) {
isIt = true;
}
}
file.flush();
file.close();
}
else {
file.flush();
file.close();
throw std::runtime_error("File isn't opened");
}
file.open("HelperApp.app/Contents/Wrapper/HelperApp.app/Contents/Info.plist", std::ios::out);
// Open the file and overwrite it with the modifyed argument
if(file.is_open()) {
file << content;
file.flush();
file.close();
}
else {
file.flush();
file.close();
throw std::runtime_error("File isn't opened");
}
}
If I comment out the above function from the processFile function in AppDelegate, then everything works "smoothly". I mean the wrapper app starts and it starts the HelperApp with default arguments. So here should be the error...
If you've implemented -application:openFile:, it should be called when you double-click a file of the type that you've registered. You say that the app launches, so the OS is trying to use your app to open the file. Here's a useful note from the documentation:
If the user started up the application by double-clicking a file, the
delegate receives the application:openFile: message before receiving
applicationDidFinishLaunching:. (applicationWillFinishLaunching: is
sent before application:openFile:.)
So, if you're doing anything in -applicationDidFinishLaunching: that has to be done before you open any files, that could be your problem. Consider moving your app initialization code to -applicationWillFinishLaunching:.
I've figured it out. When you double-click on a file icon, the application will launch itself, other things done correctly. But the application that responds to your action is not necessarily the one that you built for the last time. Probably, an old copy of your application is responding. Take a look at Library > Developer > Xcode > DrivedData. You should see many folders for your application. You can locate your application folders by right-clicking and choosing Shown In Finder after build one. Trash them all, and build a new application. Then double-click and see what happens now.
The problem was, that I gave the wrong path in my function. This path worked if I started the app from Xcode, but did not if I started the app by itself.
Here is the post which solved my problem!
right-click vs. double-click to open a file behave differently!
Apple Docs:
If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message before receiving applicationDidFinishLaunching:. (applicationWillFinishLaunching: is sent before application:openFile:.)
The Apple Docs leave out a vital piece of info...
I had assumed that a right-click -> 'Open With'
operation in Finder would be the same as a double-click.
Its NOT!
application:openFile: happens AFTER applicationDidFinishLaunching: in this case!
Was scratching my head for an hour on this one.
Related
The desired output looks like this,
how to show this on click event programmatically using flutter(even with native code if possible), it's really appreciated if anyone could show an example.
If there is no direct approach to this then a platform specific example using MethodChannel is also very welcome. Native code example must be in Objective C.
Additionally I have tried to use flutter_to_airplay but project fails to run and also has other functionalities that are not needed in this context, what is needed is showing Airplay panel only.
(Answer by M123 native code completely not working)
Here is a example how to open the AirPlayPanel in objective c.
Setup flutter
First you have to create a channel. To start communicating to the native code.
All channel names used in a single app must be unique; prefix the
channel name with a unique ‘domain prefix
static const platform = const MethodChannel('stack.M123.dev/airplay');
Then you have to invoke a method on the method channel.
Future<void> _openAirPlay() async {
try {
await platform.invokeMethod('openAirPlay');
} on PlatformException catch (e) {
print('error');
}
}
Native part
Now the flutter part is done.
First you have to add suppport for swift. For this open the ios folder in the flutter root with XCode.
Expand Runner > Runner in the Project navigator.
Open the AppDelegate.m located under Runner > Runner in the Project navigator.
Create a FlutterMethodChannel and add a handler inside the application didFinishLaunchingWithOptions: method. Make sure to use the same channel name as was used on the Flutter client side.
#import <Flutter/Flutter.h>
#import "GeneratedPluginRegistrant.h"
#implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* airPlayChannel= [FlutterMethodChannel
methodChannelWithName:#"stack.M123.dev/airplay"
binaryMessenger:controller.binaryMessenger];
[airPlayChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// Note: this method is invoked on the UI thread.
if ([#"getBatteryLevel" isEqualToString:call.method]) {
int returnValue = [weakSelf openAirPlay];
result(#(returnValue));
}];
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
Add the method in the AppDelegate class, just before #end
- (int)openAirPlay {
//Open air play
return (int)(100);
}
Discalimer: I am not a IOS developer so the steps are mostly theoretical.
I am following the official guide from Flutter. It can be found in full length here.
I've been working on a new Document-based app, and was super glad about the new UIDocumentBrowserController...trying to roll my own solution for the document browser UI was tricky!
I'm having some trouble getting the browser to open documents after they've been created.
What happens now is that when I choose to create a new document in the document browser, the document is created and opened as expected, although an error message is logged. However, after the doc is closed, I cannot reopen the file, either immediately or upon subsequent launches, even though the document is displayed. However, a weird clue here is that if I stop running the app after creating the document, but without adding new information to it (triggering the save cycle), and run the project again, I can open the file correctly. Whuch makes me think that there's something in the way the files are being saved that is the issue.
(Note: At this phase, I'm working on getting the local, non/icloud implentation working, before I move on to the icloud implementation.)
Here is the error message at any point in the code whenthe document is saved to disk (or at least most of the time!):
2017-06-20 13:21:58.254938-0500 Sermon Design 2 iOS[22454:5000138] [default] [ERROR] Could not get attribute values for item file:///Users/stevenhovater/Library/Developer/CoreSimulator/Devices/9A4364F2-B3A1-4AD9-B680-FB4BC876C707/data/Containers/Data/Application/DD534ED8-C4A3-40FE-9777-AED961976878/Documents/Untitled-9.sermon. Error: Error Domain=NSFileProviderInternalErrorDomain Code=1 "The reader is not permitted to access the URL." UserInfo={NSLocalizedDescription=The reader is not permitted to access the URL.}
I suspect that the issue lies somewher in my document types plists, which I've tried to set up by imitating the setup in the video for wwdc 2017 session 229.
My docs are encapuslated by an NSData object, using what I take to be a pretty standard subclass implentation of UIDocument. (I'm omitting the code to generate the thumbnails)
override func contents(forType typeName: String) throws -> Any {
print("Saving Document Changes")
if sermon != nil {
let newData = NSKeyedArchiver.archivedData(withRootObject: sermon!)
return newData
} else {
let newData = NSKeyedArchiver.archivedData(withRootObject: Sermon())
return newData
}
}
override func fileAttributesToWrite(to url: URL, for saveOperation: UIDocumentSaveOperation) throws -> [AnyHashable : Any] {
let thumbnail:UIImage = self.createThumbnail()
let thumbnaildict = [URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey : thumbnail]
let dict = [URLResourceKey.thumbnailDictionaryKey:thumbnaildict]
return dict
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let newSermon:Sermon = NSKeyedUnarchiver.unarchiveObject(with: contents as! Data) as? Sermon else{
throw documentErrors.invalidFile
}
self.sermon = newSermon
}
In my subclass of UIDocumentBrowserViewController, Here is my code for getting a local filename and for creating the new document.
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
var newDocumentURL: URL? = nil
print("creating new local document")
guard let target = self.newLocalFilename() else {
return
}
let targetSuffix = target.lastPathComponent
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory() + targetSuffix)
let newDocument:SDDocument = SDDocument(fileURL: tempURL)
newDocument.sermon = Sermon()
/
newDocument.save(to: tempURL, for: .forCreating) { (saveSuccess) in
/
guard saveSuccess else {
/
importHandler(nil, .none)
return
}
/
newDocument.close(completionHandler: { (closeSuccess) in
/
guard closeSuccess else {
/
importHandler(nil, .none)
return
}
/
importHandler(tempURL, .move)
})
}
}
func newLocalFilename() -> URL? {
let fileManager = FileManager()
guard let baseURL = self.localDocumentsDirectoryURL.appendingPathComponent("Untitled")
else {return nil}
var target = baseURL.appendingPathExtension(DocumentBrowserViewController.documentExtension)
var nameSuffix = 2
while fileManager.fileExists(atPath: target.path) {
target = URL(fileURLWithPath: baseURL.path + "-\(nameSuffix).\(DocumentBrowserViewController.documentExtension)")
nameSuffix += 1
}
let targetSuffix = target.lastPathComponent
print("Target name: \(targetSuffix)")
print("new url: \(target)")
return target
}
After four or five hours of work banging my head against this problem, I discovered a simple solution: don't test in the Simulator. I switched to testing on my device and instantly everything started working as advertised.
[I can't speak from experience here, but it may be that the "doesn't work in the Simulator" problem is confined to Sierra, but that the Simulator does work in High Sierra. This would explain why some users see this issue and others don't, and especially why Apple seems blissfully unaware of it in the WWDC video.]
I had exactly the same issue when I was trying to save to NSTemporaryDirectory().
If you instead save to the documents directory ([[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject]), it appears to work fine!
Update: it looks like this issue is fixed in iOS 11 beta 3, and you can now save newly created documents to NSTemporaryDirectory() correctly.
Here is my current theory.
This error
Error Domain=NSFileProviderInternalErrorDomain Code=1 "The reader is not permitted to access the URL."
shows up when first creating a UIDocument at a new URL using -initWithFileURL. It's basically saying "this URL doesn't exist yet," but in a way that makes it sound more like a permissions issue.
As far as I can tell, it doesn't prevent you from saving, opening, editing, or closing the file. So I think it's just a superfluous error that Apple should have taken out.
I found that the error happens on simulator when LSSupportsOpeningDocumentsInPlace property is set to YES in the info.plist.
Turn this property to NO, then it starts working, in my case.
On the real device, it works anyway without having error.
Had similar problem, and tried the method for providing a default Core Location in the Schemes settings and it works now. The method was mentioned in this answer: IOS 9 Error Domain=kCLErrorDomain Code=0 "(null)"
An always annoying problem i had with lessphp (and also less compiling in rails or python/django) is that it is only watching the file which to compile but NOT the imported files.
For example my less structure looks something like this:
// main.less
// (compiled to styles.css)
#import "variables"
#import "objects"
#import "theme"
.
// theme.less
// (actual styles)
body { background:#efefef }
So the actual compiled file is only the root to import the styles and files i work on. Everytime i make a change on my styles(theme.less) i have to edit the main.less so it gets recompiled.
Is there any option to check ALL files for changes like it does on client-side compile(less.js)?
When you inspect the source of the php lessc compiler, which can be found at https://raw.githubusercontent.com/oyejorge/less.php/master/bin/lessc, you will found that the script only evaluate the modification time of the Less file that you have passed as argument.
When you less files are in /home/project/less you can add for instance the following code inside the while(1) loop in bin/less, around line 125:
while (1) {
clearstatcache();
//keep original code here
$dir = '/home/project/less';
$dh = opendir($dir);
while (false !== ($filename = readdir($dh))) {
if(preg_match('/\.less$/',$filename) && filemtime($filename) > $lastAction)
{
$updated = true;
break;
}
}
if($updated) break;
}
I have a NSDocument subclass that presents a text document from disk. I’m trying to make it refresh automatically on detecting file changes on disk. I’ve overridden -presentedItemDidChange like this:
- (void)presentedItemDidChange
{
[super presentedItemDidChange];
// Ignoring bundles and error-handling for the moment.
NSData *newData = [NSData dataWithContentsOfURL:self.presentedItemURL];
self.textView.string = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
}
The UI refreshes fine when the file is changed in another application. The problem is, I get this dialog when I try to save the document in my application after it is modified by another app:
I kind of have an idea why this happens (not sure whether it’s correct): The modification time of the document is later (because it’s modified by another application) than the latest saved version in my app. But can I notify the autosaving system that I have done something with it and let it go away? Or am I doing things wrong when I refresh the document, and I should do it some other way to handle document versions correctly? I need to consider both external applications support or do not support autosave.
Thanks in advance.
#uranusjr's answer pointed me in the right direction -- only revertDocumentToSaved: wasn't exactly the right place.
override func presentedItemDidChange() {
dispatch_async(dispatch_get_main_queue()) {
self.reloadFromFile()
}
}
func reloadFromFile() {
guard let fileURL = self.fileURL else { return }
do {
try revertToContentsOfURL(fileURL, ofType: "YOUR TYPE HERE IF NECESSARY")
} catch {
// TODO handle error
print(error)
}
}
This simply reloads the file. readFromURL(url:, ofType:) (or the NSData/file wrapper based variants) is called and you can re-create your data structures from there.
Stumbled across the solution today (finally). You can “cheat” OS X into not warning about this by reverting the document (but not the file itself) before actually updating the internal data structure:
// Somehow read the updated data.
NSString *content = ...;
// Revert the document.
// This will discard any user input after the last document save,
// so you might want to present some UI here, like an NSAlert.
[self revertDocumentToSaved:self];
// Update the internal state.
self.content = content;
Now OS X will be happy when you save the document.
How do people write their comments for their NSLocalizedStrings? Is there a standard guideline that we should follow? For example if I have:
NSLocalizedString(#"Tap your account to sign in", #"");
and my comment is "Text that asks user to sign in by tapping on the account", is this a bit ambigous? Should I leave the comment out if it's pretty much self-explanatory?
Another question is, what if I have a bunch of ProgressHUD that has a text set to LoggingIn, what would be an easy way to sync across my app project that this needs to be localized into NSLocalizedString (#"Logging In", #"some description"); Is there a tool for performing such tasks?
The second parameter is a comment that will automatically appear in the strings file if you use the genstrings command-line utility, which can create the strings file for you by scanning your source code.
The comment is useful for your localizers. For example:
NSLocalizedString(#"Save",#"Title of the Save button in the theme saving dialog");
When you run genstrings, this will produce an entry in the Localizable.strings file like this:
/* Title of the Save button in the theme saving dialog */
"Save" = "Save";
In your specific example, it's fairly obvious what the comment means, but not the context. You should probably add some context like so:
NSLocalizedString(#"Tap your account to sign in", #"Instruct user to tap their account to sign in (Facebook account, main game preferences)");
That way the localizer knows exactly what button you're referring to.
This becomes even more important for buttons labelled "Share" or some other non-specific label:
NSLocalizedString(#"Share", #"Label for sharing button on main image editing screen");
(This is a modified version of my answer to this similar question).
Rob Keniger is right. I also would like to add this:
Second param can be used as .. default value!!
(NSLocalizedStringWithDefaultValue does not work properly with genstring, that's why I proposed this solution)
Here is my Custom implementation that use NSLocalizedString that use comment as default value:
1 . In your pre compiled header (.pch file) , redefine the 'NSLocalizedString' macro:
// cutom NSLocalizedString that use macro comment as default value
#import "LocalizationHandlerUtil.h"
#undef NSLocalizedString
#define NSLocalizedString(key,_comment) [[LocalizationHandlerUtil singleton] localizedString:key comment:_comment]
2. create a class to implement the localization handler
#import "LocalizationHandlerUtil.h"
#implementation LocalizationHandlerUtil
static LocalizationHandlerUtil * singleton = nil;
+ (LocalizationHandlerUtil *)singleton
{
return singleton;
}
__attribute__((constructor))
static void staticInit_singleton()
{
singleton = [[LocalizationHandlerUtil alloc] init];
}
- (NSString *)localizedString:(NSString *)key comment:(NSString *)comment
{
// default localized string loading
NSString * localizedString = [[NSBundle mainBundle] localizedStringForKey:key value:key table:nil];
// if (value == key) and comment is not nil -> returns comment
if([localizedString isEqualToString:key] && comment !=nil)
return comment;
return localizedString;
}
#end
3. Use it!
Make sure you add a Run script in your App Build Phases so you Localizable.strings file will be updated at each build, i.e., new localized string will be added in your Localized.strings file:
My build phase Script is a shell script:
Shell: /bin/sh
Shell script content: find . -name \*.m | xargs genstrings -o MyClassesFolder
So when you add this new line in your code:
self.title = NSLocalizedString(#"view_settings_title", #"Settings");
Then perform a build, your ./Localizable.scripts file will contain this new line:
/* Settings */
"view_settings_title" = "view_settings_title";
And since key == value for 'view_settings_title', the custom LocalizedStringHandler will returns the comment, i.e. 'Settings"
Voilà :-)