To make this exercise even more fun, I also added a separated target for my unit testing suite (and see how XCode supports tests), then another target for the Kiwi BDD testing framework for Obj-C, and another one for CocoaOniguruma as I have just explained in my previous article.
I’ve been playing with ways of reorganizing my project and I realized that I was doing it wrong. I was adding all the source files from my “Rubyfication” target into my Specs target. So everything was compiling fine, the specs were all passing, but the way I defined dependencies was wrong. It is kind of complicated to understand at first, but it should be something like this:
- CocoaOniguruma Target: should be a static library, with no target dependencies and no binary libraries to link against, just a dependency to the standard Foundation framework. It exposes the OnigRegexp.h, OnigRegexpUtility.h and oniguruma.h as public headers.
- Kiwi Target: should be another static library, with no target dependencies and no binary libraries to link against, just having the Foundation and UIKit framework dependencies.
- Rubyfication: should be another static library, with CocoaOniguruma as a target dependency, linking against the libCocoaOniguruma.a binary and depending on the Foundation framework as well. It exposes all of its .h files as public headers.
- RubyficationTests: should be a Bundle which were created together with the Rubyfication target (you can specify whether you want a unit test target when you create new targets), with both Kiwi and Rubyfication targets as dependencies, linking against the libKiwi.a and libRubyfication.a binaries, and the Foundation and UIKit frameworks as well.
If you keep creating new targets manually, XCode 4 will also create a bunch of Schemes that you don’t really need. I keep mine clean with just the Rubyfication scheme. You can access the “Product” menu and the “Edit Scheme” option. Then my Scheme looks like this:
I usually configure all my build settings to use “LLVM Compiler 2.0” for the Debug settings and “LLVM GCC 4.2” for the Release settings (actually, I do that for precaution as I am not aware if people are actually deploying binaries in production compiled from LLVM).
I also set the “Targeted Device Family” to “iPhone/iPad” and I try to make the “iOS Deployment Target” to “iOS 3.0” whenever possible. People usually leave the default one which will be the latest release – now at 4.3. Be aware that your project may not run on older devices that way.
Finally I also make sure that the “Framework Search Paths” are pointing to these options:
1 2 |
"$(SDKROOT)/Developer/Library/Frameworks" "${DEVELOPER_LIBRARY_DIR}/Frameworks" |
Everything compiles just fine that way. Then I can press “Command-U” (or go to the “Product” menu, “Test” option) to build the “RubyficationTests” target. It builds all the target dependencies, links everything together and runs the final script to execute the tests (you must make sure that you are selecting the “Rubyfication – iPhone 4.3 Simulator” in the Scheme Menu). It will fire up the Simulator so it can run the specs.
But then I was receiving:
1 2 3 4 5 |
Test Suite '/Users/akitaonrails/Library/Developer/Xcode/DerivedData/Rubyfication-gfqxbgyxicfpxugauehktilpmwzv/Build/Products/Debug-iphonesimulator/RubyficationTests.octest(Tests)' started at 2011-04-24 02:16:27 +0000 Test Suite 'CollectionSpec' started at 2011-04-24 02:16:27 +0000 Test Case '-[CollectionSpec runSpec]' started. 2011-04-23 23:16:27.506 otest[40298:903] -[__NSArrayI each:]: unrecognized selector sent to instance 0xe51a30 2011-04-23 23:16:27.508 otest[40298:903] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI each:]: unrecognized selector sent to instance 0xe51a30' |
It says that an instance of NSArray is not recognizing the selector each: sent to it in the CollectionSpec file. It is probably this snippet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#import "Kiwi.h" #import "NSArray+functional.h" #import "NSArray+helpers.h" #import "NSArray+activesupport.h" SPEC_BEGIN(CollectionSpec) describe(@"NSArray", ^{ __block NSArray* list = nil; context(@"Functional", ^{ beforeEach(^{ list = [NSArray arrayWithObjects:@"a", @"b", @"c", nil]; }); it(@"should iterate sequentially through the entire collection of items", ^{ NSMutableArray* output = [[NSMutableArray alloc] init]; [list each:^(id item) { [output addObject:item]; }]; [[theValue([output count]) should] equal:theValue([list count])]; }); ... |
Reference: CollectionSpec.m
Notice that at Line 3 there is the correct import statement where the NSArray(Helpers) category is defined with the correct each: method declared. The error is happening at the spec in line 18 in the above snippet.
Now, this was not a compile time error, it was a runtime error. So the import statement is finding the correct file and compiling but something in the linking phase is not going correctly and at runtime the NSArray(Helpers) category, and probably other categories, are not available.
It took me a few hours of research but I finally figured out one simple flag that changed everything, the -all_load linker flag. As the documentation states:
Important: For 64-bit and iPhone OS applications, there is a linker bug that prevents -ObjC from loading objects files from static libraries that contain only categories and no classes. The workaround is to use the -all_load or -force_load flags.
-all_load forces the linker to load all object files from every archive it sees, even those without Objective-C code. -force_load is available in Xcode 3.2 and later. It allows finer grain control of archive loading. Each -force_load option must be followed by a path to an archive, and every object file in that archive will be loaded.
So every target that depends on external static libraries that loads Categories has to add this -all_load flag in the “Other Linker Flags”, under the “Linking” category on the “Build Settings” of the target, like this:
So both my RubyficationTests and Rubyfication targets had to receive this new flag. And not the Tests all pass flawlessly!