Support NSDocument changes in an external editor? - objective-c

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.

Related

NSPersistentDocument fails when saving a previously locked document

When opening a locked file using my NSPersistentDocument subclass I get the following message in the console:
Attempt to add read-only file at path [URL] read/write. Adding
it read-only instead. This will be a hard error in the future; you
must specify the NSReadOnlyPersistentStoreOption.
The document window title is '(document name) - Locked'. After the user unlocks it, makes a change and then attempts to save, the save fails with the error
An error occurred while saving.
It seems that NSPersistentDocument fails to recognize that the user has unlocked the document and doesn't reopen it in read/write mode. Is this a bug in NSPersistentDocument or am I missing something here?
I am not overriding any of the file I/O methods in NSPersistentDocument.
Ah, ok automatic file locking.
That happens for auto-save documents not accessed in a while.
The typical approach is to notice the lock before creating the core data stack and put up a dialog asking the user to unlock the file.
If they agree to unlock the file, you simply unlock it and run as normal.
If they don't agree to unlock it, you copy it or open it readonly. Of course, you could simply bypass the user's preference and automatically unlock the file anyway, but that's probably not very nice.
Here is a category that should help you determine if a file is locked, and also lock/unlock the file.
Note, that this is entirely separate from the files mode being changed to read-only, but you can handle it in a similar manner.
Category interface
#interface NSFileManager (MyFileLocking)
- (BOOL)isFileLockedAtPath:(NSString *)path;
- (BOOL)unlockFileAtPath:(NSString*)path error:(NSError**)error;
- (BOOL)lockFileAtPath:(NSString*)path error:(NSError**)error;
#end
Category implementation
#implementation NSFileManager (MyFileLocking)
- (BOOL)isFileLockedAtPath:(NSString *)path {
return [[[self attributesOfItemAtPath:path error:NULL]
objectForKey:NSFileImmutable] boolValue];
}
- (BOOL)unlockFileAtPath:(NSString*)path error:(NSError**)error {
return [self setAttributes:#{NSFileImmutable:#NO}
ofItemAtPath:path
error:error];
}
- (BOOL)lockFileAtPath:(NSString*)path error:(NSError**)error {
return [self setAttributes:#{NSFileImmutable:#YES}
ofItemAtPath:path
error:error];
}
#end
Then, you can call [[NSFileManager defaultManager] isFileLockedAtPath:path] to determine if it is locked, and if it is, throw up a dialog asking the user what to do about it. You can then unlock it and open the stack as normal, or leave it locked and open the stack read-only, which will prevent saves from changing the file store.
Note that you can also monitor the file, and know when it changes from locked/unlocked and respond accordingly.
For Apple's guidelines on this, see https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/StandardBehaviors/StandardBehaviors.html
EDIT
Ok. I would have liked for NSPersistentDocument to replicate the
behavior in NSDocument - where the prompt to unlock comes only when an
edit is attempted. What you're saying is that there is no such feature
in NSPersistentDocument? – Aderstedt
OK. I thought you were wanting to ask the user to unlock it so that it could be opened read/write.
If you want to "go with the flow" and open it read-only when necessary, then you should add a little customization to your NSPersistentDocument subclass.
First, you want to add a little state to keep track of whether or not the original options specified a read-only file.
#implementation MyDocument {
BOOL explicitReadOnly;
}
Then, you will want a couple of utility methods...
- (NSDictionary*)addReadOnlyOption:(NSDictionary*)options {
NSMutableDictionary *mutable = options ? [options mutableCopy]
: [NSMutableDictionary dictionary];
mutable[NSReadOnlyPersistentStoreOption] = #YES;
return [mutable copy];
}
- (NSDictionary*)removeReadOnlyOption:(NSDictionary*)options {
NSMutableDictionary *mutable = options ? [options mutableCopy]
: [NSMutableDictionary dictionary];
[mutable removeObjectForKey:NSReadOnlyPersistentStoreOption];
return [mutable copy];
}
Next, you want to provide your own persistent store coordinator configuration code. This allows you to provide the read-only option to the store when you create it. This method is automatically called when you build your document, all you need to do is provide an override implementation.
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url
ofType:(NSString *)fileType
modelConfiguration:(NSString *)configuration
storeOptions:(NSDictionary<NSString *,id> *)storeOptions
error:(NSError * _Nullable __autoreleasing *)error {
explicitReadOnly = [storeOptions[NSReadOnlyPersistentStoreOption] boolValue];
if (![[NSFileManager defaultManager] isWritableFileAtPath:url.path]) {
storeOptions = [self addReadOnlyOption:storeOptions];
}
return [super configurePersistentStoreCoordinatorForURL:url
ofType:fileType
modelConfiguration:configuration
storeOptions:storeOptions
error:error];
}
Also, notice that NSPersistentDocument implements the NSFilePresenter protocol. Thus, you can override a method and be notified whenever the file content or attributes are changed. This will notify you for any change to the file, including lock/unlock from within your application, the Finder, or any other mechanism.
- (void)presentedItemDidChange {
[self ensureReadOnlyConsistency];
[super presentedItemDidChange];
}
We then want to ensure that our persistent store remains consistent with the read-only properties of the file.
Here is one implementation, that just changes the store's readOnly property.
- (void)ensureReadOnlyConsistency {
NSURL *url = [self presentedItemURL];
BOOL fileIsReadOnly = ![[NSFileManager defaultManager] isWritableFileAtPath:url.path];
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
[psc performBlock:^{
NSPersistentStore *store = [psc persistentStoreForURL:url];
if (store) {
if (fileIsReadOnly) {
if (!store.isReadOnly) {
store.readOnly = YES;
}
} else if (!explicitReadOnly) {
if (store.isReadOnly) {
store.readOnly = NO;
}
}
}
}];
}
This works, but has one little hangup. If the store is originally opened with read-only options, then the very first time the readOnly attribute is set to NO, that first save throws (actually, it's the obtainPermanentIDsForObjects:error: call. Core data appears to catch the exception, but it is logged to the console.
The save continues, and nothing seems amiss. All the objects get saved, and the object IDs are properly obtained and recorded as well.
So, there is nothing that does not work that I can tell.
However, there is another more draconian option, but it avoids the aforementioned "issue." You can replace the store.
- (void)ensureReadOnlyConsistency {
NSURL *url = [self presentedItemURL];
BOOL fileIsReadOnly = ![[NSFileManager defaultManager] isWritableFileAtPath:url.path];
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
[psc performBlock:^{
NSPersistentStore *store = [psc persistentStoreForURL:url];
if (store) {
if (fileIsReadOnly != store.isReadOnly) {
NSString *type = store.type;
NSString *configuration = store.configurationName;
NSDictionary *options = store.options;
if (fileIsReadOnly) {
options = [self addReadOnlyOption:options];
} else if (!explicitReadOnly) {
options = [self removeReadOnlyOption:options];
}
NSError *error;
if (![psc removePersistentStore:store error:&error] ||
![psc addPersistentStoreWithType:type
configuration:configuration
URL:url
options:options
error:&error]) {
// Handle the error
}
}
}
}];
}
Finally, note that the notification happens when the operating system notices that the file has changed. When the file is locked/unlocked from within your application, you can get a faster notification.
You can override these two methods to get a little quicker response to the change...
- (void)lockWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler {
[super lockWithCompletionHandler:^(NSError * _Nullable error) {
if (completionHandler) completionHandler(error);
if (!error) [self ensureReadOnlyConsistency];
}];
}
- (void)unlockWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler {
[super unlockWithCompletionHandler:^(NSError * _Nullable error) {
if (completionHandler) completionHandler(error);
if (!error) [self ensureReadOnlyConsistency];
}];
}
I hope that's what you are looking for.

NSFilePresenter methods never get called

I'm trying to write a simple (toy) program that uses the NSFilePresenter and NSFileCoordinator methods to watch a file for changes.
The program consists of a text view that loads a (hardcoded) text file and a button that will save the file with any changes. The idea is that I have two instances running and saving in one instance will cause the other instance to reload the changed file.
Loading and saving the file works fine but the NSFilePresenter methods are never called. It is all based around a class called FileManager which implements the NSFilePresenter protocol. The code is as follows:
Interface:
#interface FileManager : NSObject <NSFilePresenter>
#property (unsafe_unretained) IBOutlet NSTextView *textView;
- (void) saveFile;
- (void) reloadFile;
#end
Implementation:
#implementation FileManager
{
NSOperationQueue* queue;
NSURL* fileURL;
}
- (id) init {
self = [super init];
if (self) {
self->queue = [NSOperationQueue new];
self->fileURL = [NSURL URLWithString:#"/Users/Jonathan/file.txt"];
[NSFileCoordinator addFilePresenter:self];
}
return self;
}
- (NSURL*) presentedItemURL {
NSLog(#"presentedItemURL");
return self->fileURL;
}
- (NSOperationQueue*) presentedItemOperationQueue {
NSLog(#"presentedItemOperationQueue");
return self->queue;
}
- (void) saveFile {
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
NSError* error;
[coordinator coordinateWritingItemAtURL:self->fileURL options:NSFileCoordinatorWritingForMerging error:&error byAccessor:^(NSURL* url) {
NSString* content = [self.textView string];
[content writeToFile:[url path] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
}];
}
- (void) reloadFile {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
NSError* error;
__block NSData* content;
[coordinator coordinateReadingItemAtURL:self->fileURL options:0 error:&error byAccessor:^(NSURL* url) {
if ([fileManager fileExistsAtPath:[url path]]) {
content = [fileManager contentsAtPath:[url path]];
}
}];
dispatch_async(dispatch_get_main_queue(), ^{
[self.textView setString:[[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding]];
});
}
// After this I implement *every* method in the NSFilePresenter protocol. Each one
// simply logs its method name (so I can see it has been called) and calls reloadFile
// (not the correct implementation for all of them I know, but good enough for now).
#end
Note, reloadFile is called in applicationDidFinishLaunching and saveFile gets called every time the save button is click (via the app delegate).
The only NSFilePresenter method that ever gets called (going by the logs) is presentedItemURL (which gets called four times when the program starts and loads the file and three times whenever save is clicked. Clicking save in a second instance has no noticeable effect on the first instance.
Can anyone tell me what I'm doing wrong here?
I was struggling with this exact issue for quite a while. For me, the only method that would be called was -presentedSubitemDidChangeAtURL: (I was monitoring a directory rather than a file). I opened a technical support issue with Apple, and their response was that this is a bug, and the only thing we can do right now is to do everything through -presentedSubitemDidChangeAtURL: if you're monitoring a directory. Not sure what can be done when monitoring a file.
I would encourage anyone encountering this issue to file a bug (https://bugreport.apple.com) to encourage Apple to get this problem fixed as soon as possible.
(I realize that this is an old question, but... :) )
First of all, I notice you don't have [NSFileCoordinator removeFilePresenter:self]; anywhere (it should be in dealloc).
Secondly, you wrote:
// After this I implement *every* method in the NSFilePresenter protocol. Each one
// simply logs its method name (so I can see it has been called) and calls reloadFile
// (not the correct implementation for all of them I know, but good enough for now).
You're right: it's the incorrect implementation! And you're wrong: it's not good enough, because it's essential for methods like accommodatePresentedItemDeletionWithCompletionHandler: which take a completion block as a parameter, that you actually call this completion block whenever you implement them, e.g.
- (void) savePresentedItemChangesWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler
{
// implement your save routine here, but only if you need to!
if ( dataHasChanged ) [self save]; // <-- meta code
//
NSError * err = nil; // <-- = no error, in this simple implementation
completionHandler(err); // <-- essential!
}
I don't know whether this is the reason your protocol methods are not being called, but it's certainly a place to start. Well, assuming you haven't already worked out what was wrong in the past three years! :-)

How to subclass NSDocumentController to only allow one doc at a time

I'm trying to create a Core Data, document based app but with the limitation that only one document can be viewed at a time (it's an audio app and wouldn't make sense for a lot of docs to be making noise at once).
My plan was to subclass NSDocumentController in a way that doesn't require linking it up to any of the menu's actions. This has been going reasonably but I've run into a problem that's making me question my approach a little.
The below code works for the most part except if a user does the following:
- Tries to open a doc with an existing 'dirty' doc open
- Clicks cancel on the save/dont save/cancel alert (this works ok)
- Then tries to open a doc again. For some reason now the openDocumentWithContentsOfURL method never gets called again, even though the open dialog appears.
Can anyone help me work out why? Or perhaps point me to an example of how to do this right? It feels like something that must have been implemented by a few people but I've not been able to find a 10.7+ example.
- (BOOL)presentError:(NSError *)error
{
if([error.domain isEqualToString:DOCS_ERROR_DOMAIN] && error.code == MULTIPLE_DOCS_ERROR_CODE)
return NO;
else
return [super presentError:error];
}
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
if(self.currentDocument) {
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openUntitledDocumentAndDisplayIfClosedAll: didCloseAll: contextInfo:)
contextInfo:nil];
NSMutableDictionary* details = [NSMutableDictionary dictionary];
[details setValue:#"Suppressed multiple documents" forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:DOCS_ERROR_DOMAIN code:MULTIPLE_DOCS_ERROR_CODE userInfo:details];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
- (void)openUntitledDocumentAndDisplayIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
if(self.currentDocument == nil)
[super openUntitledDocumentAndDisplay:YES error:nil];
}
- (void)openDocumentWithContentsOfURL:(NSURL *)url
display:(BOOL)displayDocument
completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler NS_AVAILABLE_MAC(10_7)
{
NSLog(#"%s", __func__);
if(self.currentDocument) {
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[url copy], #"url",
[completionHandler copy], #"completionHandler",
nil];
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocumentWithContentsOfURLIfClosedAll:didCloseAll:contextInfo:)
contextInfo:(__bridge_retained void *)(info)];
} else {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
}
}
- (void)openDocumentWithContentsOfURLIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
NSDictionary *info = (__bridge NSDictionary *)contextInfo;
if(self.currentDocument == nil)
[super openDocumentWithContentsOfURL:[info objectForKey:#"url"] display:YES completionHandler:[info objectForKey:#"completionHandler"]];
}
There's a very informative exchange on Apple's cocoa-dev mailing list that describes what you have to do in order to subclass NSDocumentController for your purposes. The result is that an existing document is closed when a new one is opened.
Something else you might consider is to mute or stop playing a document when its window resigns main (i.e., sends NSWindowDidResignMainNotification to the window's delegate), if only to avoid forcing what might seem to be an artificial restriction on the user.
I know it's been a while, but in case it helps others....
I had what I think is a similar problem, and the solution was to call the completion handler when my custom DocumentController did not open the document, e.g.:
- (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument * _Nullable, BOOL, NSError * _Nullable))completionHandler {
if (doOpenDocument) {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
} else {
completionHandler(NULL, NO, NULL);
}
}
When I added the completionHandler(NULL, NO, NULL); it started working for more than a single shot.

How to get localized Cancel, Done and etc?

UIBarButtonItem have identifiers like Cancel, Done and some others. They are shown as text to user. If user changes language then for example Cancel button will be translated automatically. And as developer you do not need to provide localization string for this buttons. It means that Cancel, Done and other strings already localized and comes together with OS.
Is here a way to get this strings programmatically?
I do not want to add additional strings to localization files. And if it is possible to access then it would be very good.
Here's a little macro I created to get the System UIKit Strings:
#define UIKitLocalizedString(key) [[NSBundle bundleWithIdentifier:#"com.apple.UIKit"] localizedStringForKey:key value:#"" table:nil]
Use it like this:
UIKitLocalizedString(#"Search");
UIKitLocalizedString(#"Done");
UIKitLocalizedString(#"Cancel");
...
One (admittedly questionable) way of accomplishing this easily is use Apple's framework bundle localizations directly:
To see what they have to offer, open the following directory via the Finder:
/Applications/Xcode/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk/System/Library/Frameworks
And in your case, you'll subsequently open ./UIKit.framework/English.lproj/Localizable.strings (in TextMate). Here you see a wide variety of translations that Apple uses for things like "Print", "OK", "On", "Off", etc. The real magic is that there are about 35 different language translations that you can copy into your own Localizable.strings files, for free.
If you are incredibly brazen and don't mind putting your app's future stability in question, you could skip the whole "copy into your own Localizable.strings" process and go straight to the source programmatically:
NSBundle *uiKitBundle = [NSBundle bundleWithIdentifier:#"com.apple.UIKit"];
NSString *onText = uiKitBundle ? [uiKitBundle localizedStringForKey:#"Yes" value:nil table:nil] : #"YES";
NSString *offText = uiKitBundle ? [uiKitBundle localizedStringForKey:#"No" value:nil table:nil] : #"NO";
Caveat: In no way do I recommend that you actually access these localized resources programmatically in an app that you intend to submit to the App Store. I'm merely illustrating a particular implementation that I've seen which addresses your original question.
Encouraged by Answer to this Question (by Stephan Heilner)
and Answer (by bdunagan) for iPhone/iOS: How can I get a list of localized strings in all the languages my app is localized in?
Objective-C
NSString * LocalizedString(NSString *key) {
return [[NSBundle mainBundle] localizedStringForKey:key];
}
#interface NSBundle(Localization)
- (NSString *)localizedStringForKey:(NSString *)key;
- (NSDictionary<NSString *, NSString *> *)localizationTable;
#end
#implementation NSBundle(Localization)
- (NSDictionary<NSString *, NSString *> *)localizationTable {
NSString *path = [self pathForResource:#"Localizable" ofType:#"strings"];
NSData *data = [NSData dataWithContentsOfFile:path];
NSError *error = nil;
id obj = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:&error];
if (error && obj == nil) {
#throw error;
return nil;
}
if ([obj isKindOfClass:NSDictionary.class]) {
return obj;
}
#throw NSInternalInconsistencyException;
return nil;
}
- (NSString *)localizedStringForKey:(NSString *)key {
return [self localizedStringForKey:key value:nil table:nil];
}
#end
Swift 5
public extension String {
func localized(_ bundle: Bundle = .main) -> String {
bundle.localize(self)
}
var localized: String {
return localized()
}
}
extension Bundle {
static var UIKit: Bundle {
Self(for: UIApplication.self)
}
func localize(_ key: String, table: String? = nil) -> String {
self.localizedString(forKey: key, value: nil, table: nil)
}
var localizableStrings: [String: String]? {
guard let fileURL = url(forResource: "Localizable", withExtension: "strings") else {
return nil
}
do {
let data = try Data(contentsOf: fileURL)
let plist = try PropertyListSerialization.propertyList(from: data, format: .none)
return plist as? [String: String]
} catch {
print(error)
}
return nil
}
}
Usage:
"Photo Library".localized(.UIKit)
To get all keys of localized strings of UIKit:
Bundle.UIKit.localizableStrings?.keys//.map { $0 }
While perhaps not exactly what you were seeking, there is a commercial app in the Mac App Store called "System Strings" claiming that it provides a collection of more than 53000 standard localized strings. It was released November 2, 2012. I am in no way affiliated with this app or the author. The URL is https://itunes.apple.com/us/app/systemstrings/id570467776.
Sounds like what you are asking is if Apple provides a way to access a dictionary of pre-translated strings. I would think that anything provided by Apple for something like this would be located in their Docs: Internationalization Programming Topics
To answer your question I do not believe they provide a dictionary/list of known translations. Either you will have to define them in your Localizable.strings resource file or do as others have stated in the comments and pull the title from the UIBarButtonItem (I would go with the resource file personally).
Why not use base localization with your storyboard? It will localize it for you.
You could use
NSLocalizedString(#"Cancel", #"Cancel")

Calling Obj-C Code from JavaScript via Console: Arguments get dropped?

Having a heck of a time with this one.
I've got a super-simple Cocoa app containing one WebView, a WebScripting API defined in the page, and a single NSObject defined on that API. When I turn on the debugger tools (in the embedded WebView), I can see the API on the JavaScript window object, and I can see my "api" property defined on that -- but when I call the API's "get" method, the arguments aren't being serialized -- when the Obj-C method gets called, the arguments are missing. See below, which hopefully illustrates:
I've combed through the docs, I've (apparently) set the appropriate methods to expose everything that needs to be exposed, and I can see the method being called. There has to be something stupid I'm missing, but as a relative newbie to this environment, I'm not seeing it.
Thanks in advance for your help!
Have you set WebKitDeveloperExtras to YES in your default user defaults when you send -[NSUserDefaults registerDefaults:]?
Depending on what version of Xcode you're using you could be getting a known error. If you're using LLDB on anything but the most recent version, it might not be giving you the right variables in the debugger. The solution has been to use GDB instead of LLDB until Apple fixes the problem. But I think they fixed the problem in the latest version. I'd change the debugger to use GDB and see if you're getting the right variables in Xcode. (Product-> Edit Scheme...-> Run -> Debugger). I came across this problem in iOS, though, so I don't know its applicability to OSX. Worth a try anyway.
I originally came across the problem here: https://stackoverflow.com/a/9485349/1147934
I process javascript in the main thread of my app from a local file stored in the apps directory. I check for beginning and ending tokens for the js functions I am executing and whether the function contains a variable.
Hopefully this can give you some good ideas for your issue. You could also do alerts in the js to see if the values post correctly as you run the app (I am sure you thought of that already, but it's worth mentioning.) Happy coding! I hope this helps!
in the .h file define:
NSMutableString *processedCommand;
NSArray *commandArguments;
In the .m file:
// tokens
#define kOpenToken #"<%%"
#define kCloseToken #"%%>"
// this will throw
-(void)executeJScriptCommand:(NSString *)aCommand {
[self performSelectorOnMainThread:#selector(executeThisCommand:) withObject:aCommand waitUntilDone:YES];
}
// this will throw
-(NSString *)executeCommand:(NSString *)command {
NSString *aCommand = [[[command stringByReplacingOccurrencesOfString:kOpenToken withString:#""]
stringByReplacingOccurrencesOfString:kCloseToken withString:#""]
stringByTrimmingLeadingAndTrailingWhitespaces];
if ([aCommand hasPrefix:#"="])
{
// variable. get value
[self getVariableFromCommand:aCommand];
}
else {
[self executeThisCommand:aCommand];
}
NSString *returnValue = [NSString stringWithString:processedCommand];
self.processedCommand = nil;
self.commandArguments = nil;
return returnValue;
}
-(void)executeThisCommand:(NSString *)aCommand {
BOOL hasError = NO;
// clear result
self.processedCommand = nil;
self.commandArguments = nil;
BOOL isFromJS = NO;
NSString *function = nil;
NSMutableArray *commandParts = nil;
#try {
// first, break the command into its parts and extract the function that needs to be called, and the (optional) arguments
commandParts = [[NSMutableArray alloc] initWithArray:[aCommand componentsSeparatedByString:#":"]];
if ([[[commandParts objectAtIndex:0] lowercaseString] isEqualToString:#"js-call"]) {
isFromJS = YES;
[commandParts removeObjectAtIndex:0];
}
// get our function, arguments
function = [[commandParts objectAtIndex:0] retain];
[commandParts removeObjectAtIndex:0];
if ([commandParts count] > 0){
if (isFromJS == YES) {
NSString *arguments = [[commandParts objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
if ([arguments length] > 0) {
self.commandArguments = [arguments JSONValue];
}
}
else {
self.commandArguments = [NSArray arrayWithArray:commandParts];
}
}
// build invoke
SEL sel = NSSelectorFromString(function);
if ([self respondsToSelector:sel]) {
[self performSelectorOnMainThread:sel withObject:nil waitUntilDone:YES];
// using invocation causes a SIGABORT because the try/catch block was not catching the exception.
// using perform selector fixed the problem (i.e., the try/catch block now correctly catches the exception, as expected)
}
else {
[appDelegate buildNewExceptionWithName:#"" andMessage:[NSString stringWithFormat:#"Object does not respond to selector %#", function]];
}
}
#catch (NSException * e) {
hasError = YES;
[self updateErrorMessage:[NSString stringWithFormat:#"Error processing command %#: %#", aCommand, [e reason]]];
}
#finally {
[function release];
[commandParts release];
}
if (hasError == YES) {
[appDelegate buildNewExceptionWithName:#"executeThisCommand" andMessage:self.errorMessage];
}
}
// this can return nil
-(NSString *)getQueryStringValue:(NSString *)name {
NSString *returnValue = nil;
if (queryString != nil) {
returnValue = [queryString objectForKey:[name lowercaseString]];
}
return returnValue;
}