Compile time check for valid file references in Xcode - objective-c

Is it possible to force the Xcode complier to verify that files referenced in code are valid?
There are multiple points in Cocoa development when you naturally reference a file programmatically via an NSString:
[UINib nibWithNibName:#"MyNib" bundle:nil];
[UIImage imageNamed:#"MyImage"];
[[UIViewController alloc] initWithNibName:#"MyNib" bundle:nil];
Is there any way at compile time to check is these file references are valid?
Often times after using above methods, I end up changing the name of the referenced file but forget to change the name in code. Everything complies without a problem and it is only when you happen to go to the portion of the app that accesses this file that the bug will reveal itself.
Is there another approach or technique that people use to avoid this sort of error?
Referencing a file name via a string feels very fragile.

Warning: This answer is mostly outdated. The general idea is fine but better solutions exist now (e.g. Image assets with a SwiftGen script to generate an enum).
Nibs usually have a class with the same name as the file, e.g.
[[MyViewController alloc] initWithNibName:NSStringFromClassName([MyViewController class]) bundle:nil];
I usually hide it into the controller's init method as [self class].
For image loading, compile-time checks are difficult. Help yourself with macros, first replace the loading method by a simple macro, e.g.
#define LOAD_IMAGE(__IMAGE_NAME__) [UIImage imageNamed:__IMAGE_NAME__]
First thing you should do is to put an assert into this macro and always check that the image was successfully loaded. It's not a compile-time check but it helps to find missing resources.
The second thing is to write a ruby/python/shell/(any scripting language) script that will search your source files for LOAD_IMAGE and check if the file (between parenthesis) exists. As a shell script, it will be very simple (e.g. using grep). You can add this script into your xcode project to be run when compiling.
Don't forget to check images referenced by xibs.
However, often you have to create the image name dynamically, e.g. NSString* imageName = [NSString stringWithFormat:#"image_%i", index]. There's no way how you can check this at compile time.
Also don't forget to do the reverse check - don't include image files which are not used anywhere.

AutoComplete for [UIImage imageNamed:] by Kent Sutherland.
This provides code completion support within Xcode - a brilliant piece of code. This is working for me in Xcode 4.6:
Currently this project does not have support for strings other than imageNamed:. To support those, I will try to write a compile time script. Or maybe I will become bold and try to extend Mr. Sutherland's spectacular work.

Xcode doesn't support this, but if this problem is really biting you then you could use the following hack:
Give every in-bundle file a unique prefix (e.g. app__)
When you add a file to your project, make sure you first rename it to add this prefix.
Your compile time (pre-distribution) check then has two parts: 1) Search through all .m files and enumerate strings that begin with the prefix. You shouldn't have to check if the string is quoted since your prefix is unique. 2) grep project.pbxproj for each string to check if it is included in the bundle.
With some effort, this process can be mostly automated and also optimized, but the above recipe ought to work.

here is a bash script that we use that lists all images on disk but NOT referenced in code.
https://gist.github.com/3750087
it would likely be easy to reverse this to check for non-exting images and xibs.
Anyways, the script should be a good starting point

Related

Best way to create global god mode variables in Objective-C

I want to have some "god mode" boolean toggles to quickly change functionality inside my app.
The previous developer created #define macros for these in a file that's included (indirectly) in every other file, but there are some disadvantages:
Every change has to be commited in git
Every change forces a build of the entire project
Is an xconfig file suited for this case? I accomplished what I want by:
Creating an .xconfig file
Adding it to the project
Adding HACKS_TEST = YES to it
Adding preprocessor macro HACKS_TEST=${HACKS_TEST} to the project target
Adding static BOOL const IOHacksTest = HACKS_TEST
and upon logging it, the value is printed correctly. But, if I change the HACK_TEST's value in the .xconfig file, it still builds the entire project. Also, adding one 'hack', needs modifications in the .xconfig file, project target's preprocessor macro section and project's global file.
Is it possible to achieve this without the need of an entire project build?
Is there another solution that doesn't require these many modifications on new 'hacks' adding?
Is there another solution that is more appropriated to my needs and/or easier/better?
What is the best way to have them set to NO for the Release configuration?
Edit:
Along the selected solution, I also added this in the Globals.m file:
if #debug
BOOL IOHacksTest = YES;
#else
BOOL IOHacksTest = NO;
#endif
so for Release builds, all the "hacks" are turned off automatically.
Every change has to be commited in git
You can't, or really shouldn't try to, avoid this. If there's some setting that can affect how the whole project works, it should be archived with the project.
As you've discovered, putting the values in an .xcconfig file doesn't have much advantage over having #defines for everything - if you change the xcconfig file, yo have to rebuild everything.
To avoid that, you'll want to split the declaration and definition of the variables between a header and a source file. So, you'd have a goobals.h file that contains "extern" declarations, like this:
extern BOOL IOHacksTest;
And a goobals.m file that has:
BOOL IOHacksTest = YES;
Then you include goobals.h wherever you need the declarations. If you need to change a value, you only need to recompile a single file.
1, Is it possible to achieve this without the need of an entire
project build?
If you want code to be compiled differently, based on those changing values, then there is no way to do it. In fact, you want the project to be recompiled, because the generated code needs to change.
The best way to prevent an entire project build is to put those values into a header file, and selectively include that header file only in files that need to know about the values that could change.
Is there another solution that doesn't require these many modifications on new 'hacks' adding?
There is no way getting around it if you want compile-time detection. If you change the values, then the compiler has to generate new code. How could the compiler generate the right code if it does not know about the custom changes you want to make with your "changes?"
Is there another solution that is more appropriated to my needs and/or easier/better?
That depends on your actual needs, which you didn't state in the original question. If you must have compile-time knowledge of the changes, you can use the header files, or you can add values to .xcconfig files, or just set them in the project. However, you will still have to recompile all the code that is impacted every time the values change.
If you want the settings to be changed at run time, then this is precisely what NSUserDefaults is designed for. Set an initial value in the source code, or in the Info.plist file, and set it to user-defaults on the app's first launch. Thereafter, you can manage the values from user-defaults.
What is the best way to have them set to NO for the Release configuration?
static BOOL builtInDebugMode = !!(DEBUG);
EDIT
Also, what does !!(DEBUG) do? – Iulian Onofrei
That's a logical-not operator, employed twice. It ensures that its operand will always be either 1 or 0. It was there to guide you to how it could be done (i.e., consider DEBUG or NDEBUG at compile time).
By default, your xcode configuration will have DEBUG=1 for debug builds. You could set it to DEBUG=0 for release builds, and use the above code.
As long as your code properly handles DEBUG, this should be fine. Unfortunately, some code incorrectly uses #ifdef DEBUG rather than #if DEBUG which could cause issues.
Thus, you may be better off with something like...
#if DEBUG
static BOOL builtInDebugMode = YES;
#else
static BOOL builtInDebugMode = NO;
#endif
When compiled in debug mode, DEBUG will be defined as 1. When not in debug mode, it will not be defined at all.
Or, you could add a definition to your project file DEBUG_VALUE
You can set some value in NSUserDefault, is very easy to use, I think.
You can add buttons to your app's UI to toggle these values, or you can let app query some website for this value at first launch, depends your needs.
NSUserDefault is fast.
And you can query at launch, and use it util app quit.

Array Doesn't load PNGs Exported from After Effects: ios, xcode

Our graphic designer is sending us .PNGs named appropiately "hide_00~iphone.png", "hide_00#2X~iphone.png" etc
He is exporting the images from after effects. I add them to the project and try to load them into an array on init. An exception is thrown each time for all of his files. Now, if I go in AND RENAME the files in the finder to exactly the same name, everything compiles fine. I have no idea what's going on here. Xcode cannot find them in the file system until I rename them. But the name is EXACTLY the same as what he sent me. I checked for white space around his file naming but everything looks fine.
Does anybody know if After Effects puts weird header info in the images? Or does this sound familiar at all to anyone. There are a whole bunch of images we are working with and I would hate to have to rename them by hand.
So I just used imageNamed and the image loaded just fine. So obviously your routine to load the image by path/name is flawed. You can post that and it can be evaluated - but this has nothing to do with 'After Effects'.
EDIT: For the record, my system is Lion Xcode 4.4.1 and my project set for iOS 5.1. I took your file from dropbox, and verified that in my project I CAN load the image as you are trying to do:
for (int i = 6; i < 7; i++) {
NSString *path = [NSString stringWithFormat:#"hide_step_seq_%02d", i];
UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:path ofType:#"png"]];
NSLog(#"PATH %# image=%#", path, image);
}
2012-09-13 07:15:23.241 Searcher[58114:f803] PATH hide_step_seq_06 image=<UIImage: 0x6a4cb30>
So, where to go from here? I've tried to help several people here who get burned by the #2x or ~iphone suffixes. For some reason a few people cannot ever seem to get this to work - all I can think of is there is some flag deep in the system that gets toggled and there is no way to untoggle it.
My suggestion is to try using the actual complete file name - try appending ~iphone and see if that works.
You can also in Terminal do a 'ls *.png | od -c' before changing the name and afterwards, to verify that absolutely the characters are the same.
The last thought I have on this is that files have many attributes: creation time, last access, last modiied, extended attributes, permissions, etc. It is possible (while unlikely) that for some reason one of these values blocks the system from attempting to use the ~iphone suffix.
I really wish I could help you further. If you want to put a simple little project together that does nothing more than tries to open a few images and it fails, zip the whole project up, put on dropbox, I'd be more than willing to run it on my system to try and duplicate the problem. You can also do as I did in the code above and verify that path looks good and the image is nil.

How to intercept reading of plist values in Objective-C code?

We're using the new Urban Airship iOS plugin for PhoneGap.
In the plugin's plist file, we're supposed to enter the app-specific keys needed to enable push notifications.
The problem is we have two versions, free and paid, of the same app, but the plist file only accommodates one version.
Essentially, we need to modify the Objective-C code to read different plist values, depending on whether it's the free or premium version.
We currently manage both versions with the same code base and Xcode project. Unless we change the plugin code, it seems like we need to create a new Xcode project, which we don't want to do.
How do we adjust Urban Airship's Objective-C files to read different values from the plsit file?
Sorry to keep you waiting, I wanted to give you a very detailed answer instead of rushing last night :) So here we go.
First in your project we need to add a new target. Go to your project settings and right click your target. Click duplicate.
You'll get a new target probably named Target-copy. You'll also get a new info.plist file just for that target.
Next we're going to edit our Pro version's Built Settings. Scroll or search and find Apple LLVM compiler 4.0 Preprocessing. Add to both your Debug and Release configurations. I normally just go with the simple PRO=1. You also need to add PRO=0 to your lite version or it will be undefined when you try to build that version.
Now lets look at how to add a custom plist like I'm sure you'll need. First create two folders. Its important these are folders not groups. In each folder we can create a plist with the exact same filename.
Since Now you can add something to each of them. I just added a key property and a value pro string / lite string. Finally to the code. In the sample project I made I simple overrode viewDidLoad but obviously this will work anywhere. Since the plists have the same name you can load them with one line of code. They'll never get mixed up because they are only copied to their respective target. If you need to do code level based logic you can use the PRO preprocessor we made.
- (void)viewDidLoad
{
[super viewDidLoad];
// This will load the proper plist automatically.
NSLog(#"Plist Value: %#",[[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Property List" ofType:#"plist"]] objectForKey:#"property"]);
// Also remember we set up a preprocessor PRO. you can use it as well.
if (PRO) {
NSLog(#"Only Show for Pro");
} else {
NSLog(#"Only Show for Lite");
}
NSLog(#"This will show for both");
}
This is the method I use for all my lite/pro version apps so I can share a common codebase without copying it between projects or other complicated systems. It has worked pretty well for me so far :) Happy Coding!
Source
Figured someone may be able to use the project to look at so here it is on GitHub.

NSLocalizedString doesn't work

I want to use NSLocalizedString in my app but it always failed. What i do is:
Define 3 Localizations in Project Properties (See screenshot bellow)
Create a new file: Resource Strings File
Check in the app bundle if file.strings is there
Then I use NSLocalizedStrings as follow but it doesn't work!
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
NSString *v1 = NSLocalizedString(#"MyWindow", nil);
//NSString *v1 = [[NSBundle mainBundle] localizedStringForKey:(#"MyWindow") value:#"" table:nil];
[label setStringValue:v1];
}
In my 3 .strings files I define the below key/value:
"MyWindow" = "Ma Fenetre";
Normally, my label should display "Ma Fenetre" and not "MyWindows"
You can download an example project here and tell me where is the issue.
Bellow the content of Resources folder in my app bundle :
DerivedData om$ find test/Build/Products/Debug/test.app/Contents/Resources/
test/Build/Products/Debug/test.app/Contents/Resources/
test/Build/Products/Debug/test.app/Contents/Resources//de.lproj
test/Build/Products/Debug/test.app/Contents/Resources//de.lproj/File.strings
test/Build/Products/Debug/test.app/Contents/Resources//en.lproj
test/Build/Products/Debug/test.app/Contents/Resources//en.lproj/File.strings
test/Build/Products/Debug/test.app/Contents/Resources//fr.lproj
test/Build/Products/Debug/test.app/Contents/Resources//fr.lproj/File.strings
test/Build/Products/Debug/test.app/Contents/Resources//MainMenu.nib
Thanks
Elfoiros
NSLocalizedString uses Localizable.strings file by default. Change your File.strings name and try again.
For every time you make changes in .Strings file you need to clean your project and remove application from device and simulator. This is the only way to develop Localised application.
Have a happy coding.!
As Adam stated, NSLocalizedString uses Localizable.strings for a lookup table. To specify a custom table, use:
NSLocalizedStringFromTable(#"MyWindow", #"File");
Documentation
Need to take care over case sensitive file names: Localizable.strings not localizable.strings. Simply rename in Finder, delete reference in Xcode project and add Localizable string back to the project. Then make sure the appropriate Localization boxes are selected in the file inspector. Don't know if there's a better slicker process.
I had a similar issue as described here but the solution was different. Somehow I managed to mangle how the Localization.strings file was being referenced in the project.
Removing the Localizable.strings file and readding it fixed the problem for me.

Trouble using xib files in libraries

I'm having some trouble working with libraries and included xib files. Let me explain my situation first. I've got a project that's running pretty good. Now I need a part of my project as a library for another customer who want's to use some of the functionality in his app. I've created a library with help of that highly recommended article.
My customer should be able to initialize a main view controller like this:
LibraryMainViewController *lmvc = [[LibraryMainViewController alloc] initWithNibName:#"LibraryMainViewController.xib" bundle:foo];
That for sure leads to an error when I try to present that view controller modally. I'm not sure what bundle I have to use here. I tried something like
[NSBundle bundleForClass:[SomeClassInTheLibrary class]];
but that didn't solve the problem.
I would be glad if someone could tell me how to actually use a xib file in that kind of situation.
Thanks
–f
Update
Ok, I see I got it wrong somehow. Just for clarification: What I need is the library, the headers and the additional resources, right? Is there some kind of best practice for creating and shipping "a feature" with all it's parts above mentioned?
Static libraries can't include graphics, sounds, xibs or even headers. Only object code. Even if you added the xibs to Copy Bundle Resources, they won't become a part of the object file. Unfortunately, you can't use dynamic libraries or frameworks on the iPhone. See my answer here for a suggestion of how to create a separate assets bundle. You could also just send your customer the xib files separately, but then they have to replace them by hand if they change in the future.
Try without adding the extension to the XIB file. That's the way i usually do it. I'm also not sure if the XIB must be compiled to a NIB..
NSBundle *bundle = [NSBundle bundleForClass:[SomeClassInTheLibrary class]];
LibraryMainViewController *lmvc = [[LibraryMainViewController alloc] initWithNibName:#"LibraryMainViewController" bundle:bundle];