Being inspired by the solution to this question I tried using the same approach with XCTest.
I've set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES'.
XCode still doesn't produce any gcda files. Anyone have any ideas of how to solve this?
Code:
#import <XCTest/XCTestLog.h>
#interface VATestObserver : XCTestLog
#end
static id mainSuite = nil;
#implementation VATestObserver
+ (void)initialize {
[[NSUserDefaults standardUserDefaults] setValue:#"VATestObserver"
forKey:XCTestObserverClassKey];
[super initialize];
}
- (void)testSuiteDidStart:(XCTestRun *)testRun {
[super testSuiteDidStart:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (mainSuite == nil) {
mainSuite = suite;
}
}
- (void)testSuiteDidStop:(XCTestRun *)testRun {
[super testSuiteDidStop:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (mainSuite == suite) {
UIApplication* application = [UIApplication sharedApplication];
[application.delegate applicationWillTerminate:application];
}
}
#end
In AppDelegate.m I have:
extern void __gcov_flush(void);
- (void)applicationWillTerminate:(UIApplication *)application {
__gcov_flush();
}
EDIT: I edited the question to reflect the current status (without the red herrings).
EDIT To make it work I had to add the all the files under test to the test target including VATestObserver.
AppDelegate.m
#ifdef DEBUG
+ (void)initialize {
if([self class] == [AppDelegate class]) {
[[NSUserDefaults standardUserDefaults] setValue:#"VATestObserver"
forKey:#"XCTestObserverClass"];
}
}
#endif
VATestObserver.m
#import <XCTest/XCTestLog.h>
#import <XCTest/XCTestSuiteRun.h>
#import <XCTest/XCTest.h>
// Workaround for XCode 5 bug where __gcov_flush is not called properly when Test Coverage flags are set
#interface VATestObserver : XCTestLog
#end
#ifdef DEBUG
extern void __gcov_flush(void);
#endif
static NSUInteger sTestCounter = 0;
static id mainSuite = nil;
#implementation VATestObserver
+ (void)initialize {
[[NSUserDefaults standardUserDefaults] setValue:#"VATestObserver"
forKey:XCTestObserverClassKey];
[super initialize];
}
- (void)testSuiteDidStart:(XCTestRun *)testRun {
[super testSuiteDidStart:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
sTestCounter++;
if (mainSuite == nil) {
mainSuite = suite;
}
}
- (void)testSuiteDidStop:(XCTestRun *)testRun {
sTestCounter--;
[super testSuiteDidStop:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (sTestCounter == 0) {
__gcov_flush();
}
}
Update 1:
After reading a bit more about this, 2 things have now become clear to me (emphasis added):
Tests and the tested application are compiled separately. Tests are actually injected into the running application, so the __gcov_flush() must be called inside the application not inside the tests.
— Xcode5 Code Coverage (from cmd-line for CI builds) - Stack Overflow
and,
Again: Injection is complex. Your take away should be: Don’t add .m files from your app to your test target. You’ll get unexpected behavior.
— Testing View Controllers – #1 – Lighter View Controllers
The code below was changed to reflect these two insights…
Update 2:
Added information on how to make this work for static libraries, as requested by #MdaG in the comments. The main changes for libraries is that:
We can flush directly from the -stopObserving method because there isn't a separate app where to inject the tests.
We must register the observer in the +load method because by the time the +initialize is called (when the class is first accessed from the test suite) it's already too late for XCTest to pick it up.
Solution
The other answers here have helped me immensely in setting up code coverage in my project. While exploring them, I believe I've managed to simplify the code for the fix quite a bit.
Considering either one of:
ExampleApp.xcodeproj created from scratch as an "Empty Application"
ExampleLibrary.xcodeproj created as an independent "Cocoa Touch Static Library"
These were the steps I took to enable Code Coverage generation in Xcode 5:
Create the GcovTestObserver.m file with the following code, inside the ExampleAppTests group:
#import <XCTest/XCTestObserver.h>
#interface GcovTestObserver : XCTestObserver
#end
#implementation GcovTestObserver
- (void)stopObserving
{
[super stopObserving];
UIApplication* application = [UIApplication sharedApplication];
[application.delegate applicationWillTerminate:application];
}
#end
When doing a library, since there is no app to call, the flush can be invoked directly from the observer. In that case, add the file to the ExampleLibraryTests group with this code instead:
#import <XCTest/XCTestObserver.h>
#interface GcovTestObserver : XCTestObserver
#end
#implementation GcovTestObserver
- (void)stopObserving
{
[super stopObserving];
extern void __gcov_flush(void);
__gcov_flush();
}
#end
To register the test observer class, add the following code to the #implementation section of either one of:
ExampleAppDelegate.m file, inside the ExampleApp group
ExampleLibrary.m file, inside the ExampleLibrary group
#ifdef DEBUG
+ (void)load {
[[NSUserDefaults standardUserDefaults] setValue:#"XCTestLog,GcovTestObserver"
forKey:#"XCTestObserverClass"];
}
#endif
Previously, this answer suggested to use the +initialize method (and you can still do that in case of Apps) but it doesn't work for libraries…
In the case of a library, the +initialize will probably be executed only when the tests invoke the library code for the first time, and by then it's already too late to register the observer. Using the +load method, the observer registration in always done in time, regardless of which scenario.
In the case of Apps, add the following code to the #implementation section of the ExampleAppDelegate.m file, inside the ExampleApp group, to flush the coverage files on exiting the app:
- (void)applicationWillTerminate:(UIApplication *)application
{
#ifdef DEBUG
extern void __gcov_flush(void);
__gcov_flush();
#endif
}
Enable Generate Test Coverage Files and Instrument Program Flow by setting them to YES in the project build settings (for both the "Example" and "Example Tests" targets).
To do this in an easy and consistent way, I've added a Debug.xcconfig file associated with the project's "Debug" configuration, with the following declarations:
GCC_GENERATE_TEST_COVERAGE_FILES = YES
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
Make sure all the project's .m files are also included in the "Compile Sources" build phase of the "Example Tests" target. Don't do this: app code belongs to the app target, test code belongs to the test target!
After running the tests for your project, you'l be able to find the generated coverage files for the Example.xcodeproj in here:
cd ~/Library/Developer/Xcode/DerivedData/
find ./Example-* -name *.gcda
Notes
Step 1
The method declaration inside XCTestObserver.h indicates:
/*! Sent immediately after running tests to inform the observer that it's time
to stop observing test progress. Subclasses can override this method, but
they must invoke super's implementation. */
- (void) stopObserving;
Step 2
2.a)
By creating and registering a separate XCTestObserver subclass, we avoid having to interfere directly with the default XCTestLog class.
The constant key declaration inside XCTestObserver.h suggests just that:
/*! Setting the XCTestObserverClass user default to the name of a subclass of
XCTestObserver indicates that XCTest should use that subclass for reporting
test results rather than the default, XCTestLog. You can specify multiple
subclasses of XCTestObserver by specifying a comma between each one, for
example #"XCTestLog,FooObserver". */
XCT_EXPORT NSString * const XCTestObserverClassKey;
2.b)
Even though it's common practice to use if(self == [ExampleAppDelegate class]) around the code inside +initialize [Note: it's now using +load], I find it easier to omit it in this particular case: no need to adjust to the correct class name when doing copy & paste.
Also, the protection against running the code twice isn't really necessary here: this is not included in the release builds, and even if we subclass ExampleAppDelegate there is no problem in running this code more than one.
2.c)
In the case of libraries, the first hint of the problem came from this code comment in the Google Toolbox for Mac project: GTMCodeCovereageApp.m
+ (void)load {
// Using defines and strings so that we don't have to link in XCTest here.
// Must set defaults here. If we set them in XCTest we are too late
// for the observer registration.
// (...)
And as the NSObject Class Reference indicates:
initialize — Initializes the class before it receives its first message
load — Invoked whenever a class or category is added to the Objective-C runtime
The “EmptyLibrary” project
In case someone tries to replicate this process by creating their own "EmptyLibrary" project, please bear in mind that you need to invoke the library code from the default emtpy tests somehow.
If the main library class is not invoked from the tests, the compiler will try to be smart and it won't add it to the runtime (since it's not being called anywhere), so the +load method doesn't get called.
You can simply invoke some harmless method (as Apple suggests in their Coding Guidelines for Cocoa # Class Initialization). For example:
- (void)testExample
{
[ExampleLibrary self];
}
Because you have to create a new XCTestSuiteRun instance in the testSuiteDidStop method, you are not going to get the proper results on an == check. Instead of depending on instance equality, we used a simple counter and call flush when it hits zero, which it will when the top-level XCTestSuite finishes executing. There are probably more clever ways to do this.
First, we had to set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES' in both the Test and main app targets.
#import <XCTest/XCTestLog.h>
#import <XCTest/XCTestSuiteRun.h>
#import <XCTest/XCTest.h>
// Workaround for XCode 5 bug where __gcov_flush is not called properly when Test Coverage flags are set
#interface GCovrTestObserver : XCTestLog
#end
#ifdef DEBUG
extern void __gcov_flush(void);
#endif
static NSUInteger sTestCounter = 0;
static id mainSuite = nil;
#implementation GCovrTestObserver
- (void)testSuiteDidStart:(XCTestRun *)testRun {
[super testSuiteDidStart:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
sTestCounter++;
if (mainSuite == nil) {
mainSuite = suite;
}
}
- (void)testSuiteDidStop:(XCTestRun *)testRun {
sTestCounter--;
[super testSuiteDidStop:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (sTestCounter == 0) {
__gcov_flush();
}
}
#end
There was an additional step required, because the +initialize call was not being made on the observer when included in the Test target.
In the AppDelegate, add the following:
#ifdef DEBUG
+(void) initialize {
if([self class] == [AppDelegate class]) {
[[NSUserDefaults standardUserDefaults] setValue:#"GCovrTestObserver"
forKey:#"XCTestObserverClass"];
}
}
#endif
Here's another solution that avoids having to edit your AppDelegate
UIApplication+Instrumented.m (put this in your main target):
#implementation UIApplication (Instrumented)
#ifdef DEBUG
+ (void)load
{
NSString* key = #"XCTestObserverClass";
NSString* observers = [[NSUserDefaults standardUserDefaults] stringForKey:key];
observers = [NSString stringWithFormat:#"%#,%#", observers, #"XCTCoverageFlusher"];
[[NSUserDefaults standardUserDefaults] setValue:observers forKey:key];
}
- (void)xtc_gcov_flush
{
extern void __gcov_flush(void);
__gcov_flush();
}
#endif
#end
XCTCoverageFlusher.m (put this in your test target):
#interface XCTCoverageFlusher : XCTestObserver
#end
#implementation XCTCoverageFlusher
- (void) stopObserving
{
[super stopObserving];
UIApplication* application = [UIApplication sharedApplication];
SEL coverageFlusher = #selector(xtc_gcov_flush);
if ([application respondsToSelector:coverageFlusher])
{
objc_msgSend(application, coverageFlusher);
}
[application.delegate applicationWillTerminate:application];
}
#end
- (void)applicationWillTerminate:(UIApplication*)application must be defined in your application delegate, not in the observer class.
I didn't have any library problems. "-lgov" is not needed and you don't have to add any libraries. Coverage is supported directly by LLVM compiler.
The process for this is a little different if you're using Specta, since it does its own swizzling. The following is working for me:
Test Bundle:
#interface MyReporter : SPTNestedReporter // keeps the default reporter style
#end
#implementation MyReporter
- (void) stopObserving
{
[super stopObserving];
UIApplication* application = [UIApplication sharedApplication];
[application.delegate applicationWillTerminate:application];
}
#end
AppDelegate:
- (void)applicationWillTerminate:(UIApplication *)application
{
#ifdef DEBUG
extern void __gcov_flush(void);
__gcov_flush();
#endif
}
You then need to enable your custom reporter subclass by setting the environmental variable SPECTA_REPORTER_CLASS to MyReporter in the Run section of your main scheme.
GCOV Flush in -(void)applicationWillTerminate didn't work for me, I think because my App is running in Background.
I also set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES' but no gcda-Files.
Then I executed "__gcov_flush()" in -(void)tearDown from the TestClass, which gave me gcda-Files for my TestClass ;)
Then I created the following Function in my AppDelegate:
#interface AppDelegate : UIResponder <UIApplicationDelegate>
+(void)gcovFlush;
#end
#implementation AppDelegate
+(void)gcovFlush{
extern void __gcov_flush(void);
__gcov_flush();
NSLog(#"%s - GCOV FLUSH!", __PRETTY_FUNCTION__);
}
#end
I called [AppDelegate gcovFlush] in my -(void)tearDown and voilá, there are my gcda Files ;)
I hope this helps, bye Chris
Related
I am trying to create a plugin for Unity using Objective-C for an app running on Mac. I need to get the URL when launching my app from a link using an url protocol. I haven't used Objective-C before, so I am having trouble trying to make it work.
I am using an example provided by Unity (download example) and changing the methods to the ones I need to get the URL, but my app crashes on the line nsApplication = [[NSApplication alloc] init]; on the _GetUrl method. I have no idea what I am missing/doing wrong. Also, _GetUrl is the method called from Unity when I want to ask for the url (which is called at the first frame), but I am afraid it might be called after applicationWillFinishLaunching. So where should I actually set the delegate so that applicationWillFinishLaunching happens after the delegate is set?
I use an .h and a .m script and then compile the bundle and import it into Unity as a plugin. This is my code:
PluginUrlHandler.h
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#interface NSApplicationDelegate : NSObject
{
NSString* urlString;
}
// NSApplication delegate methods
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification;
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
//Other methods
- (NSString *)getUrl;
#end
PluginUrlHandler.m
#import <Foundation/Foundation.h>
#import "PluginUrlHandler.h"
#implementation NSApplicationDelegate
- (id)init
{
self = [super init];
urlString = #"nourl";
return self;
}
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:self
andSelector:#selector(handleGetURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass andEventID:kAEGetURL];
}
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
[event paramDescriptorForKeyword:keyDirectObject] ;
NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
urlString = urlStr;
}
- (NSString *)getUrl
{
return urlString;
}
#end
static NSApplicationDelegate* delegateObject = nil;
static NSApplication* nsApplication = nil;
// Helper method to create C string copy
char* MakeStringCopy (const char* string)
{
if (string == NULL)
return NULL;
char* res = (char*)malloc(strlen(string) + 1);
strcpy(res, string);
return res;
}
#if c__plusplus
extern "C" {
#endif
const char* _GetUrl ()
{
if (delegateObject == nil)
delegateObject = [[NSApplicationDelegate alloc] init];
if (nsApplication == nil)
nsApplication = [[NSApplication alloc] init];
[nsApplication setDelegate:delegateObject];
return MakeStringCopy([[delegateObject getUrl] UTF8String]);
}
#if c__plusplus
}
#endif
If your application is crashing, you need to provide some information, such as a stack trace or error message so we can best help. I'm assuming the error you're receiving looks like this:
2016-03-06 10:07:14.388 test[5831:230418] *** Assertion failure in -[NSApplication init], /Library/Caches/com.apple.xbs/Sources/AppKit/AppKit-1404.34/AppKit.subproj/NSApplication.m:1980
2016-03-06 10:07:14.391 test[5831:230418] An uncaught exception was raised
2016-03-06 10:07:14.391 test[5831:230418] Creating more than one Application
You shouldn't create your own NSApplication object. Just use the system one by referencing [NSApplication sharedApplication].
Generally speaking, you shouldn't need an NSApplication (or NSApplicationDelegate) for a plugin, though. The program that's loaded you should already have one, and you don't want to mess with that. Just create a custom NSObject subclass to be your AppleEvent handler. You don't need NSApplication (or it's delegate) at all for this. Any object can be the target of an AppleEvent.
You can't use things like applicationDidFinishLaunching:withOptions: from a plugin in any case. It's too late. The application has long since launched. You'll need to add your AppleEvent handler in some function called from Unity. I'm not particularly familiar with Unity's plugin engine, so I don't know if there's a particular "load" function that gets called automatically (I don't see one in the sample code). You may have to call something yourself. It would have to occur after the plugin is loaded, but before the Get URL Apple Event happens (it's unclear what you expect to generate that).
Just curious what you're trying to pull off with this. I've never seen a protocol handler used this way.
Creation of NSApplication instance looks very suspicious. Normally you don't create it as it is a singleton by definition.
So instead of this:
if (nsApplication == nil)
nsApplication = [[NSApplication alloc] init];
you should have rather this (getting current NSApplication instance):
if (nsApplication == nil)
nsApplication = [NSApplication sharedApplication];
I made a tutorial on this, see Override app delegate in Unity for iOS and OSX (4/4) Inject code to Mach-O binary.
It uses code injection to set an Objective-C class to respond the corresponding Apple Event.
I am trying to create a Boolean variable, debugMode, and access it across several different classes. This way I can set it's value once in my ViewController, and will be able to access it in my different classes (subclasses of SKScene) to show framerate, log physics values, etc.
I have read that I need to create an instance of my class? I don't see how that applies in this program.
I am new to objective-c and would greatly appreciate any help! Thank you!
The default solution is a preprocessor define, this is set by default in xcode projects.
So, in the source you can put
#ifdef DEBUG
// code that should only run in Debug Configuration
#endif
So if I get you right you want an instance of a given class that you can use across the whole of your application without losing the state of the class but this should only exist in the DEBUG version of your code?
Ok we can do this using a Singleton Pattern mixed with the #ifdef DEBUG to determine whether in debug mode or not.
DebugManager.h
// Our Debug Class that we have just made up.
// Singleton class
#interface DebugManager : NSObject
// Some properties that we want on the class
#property (nonatomic, strong) NSString *debugName;
#property (nonatomic, readonly) NSDate *instanceCreatedOn;
// a method for us to get the shared instance of our class
+ (id)sharedDebugManager;
#end
DebugManager.m
#import "DebugManager.h"
#implementation DebugManager
// Create a shared instance of our class unless one exists already
+ (id)sharedDebugManager
{
static DebugManager *sharedDebugManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedDebugManager = [[self alloc] init];
});
return sharedDebugManager;
}
- (id)init
{
if (self = [super init]) {
debugName = #"Debug Instance";
instanceCreatedOn = [NSDate new];
}
return self;
}
#end
Now that we have a Singleton class setup we can add the below line to our *-Prefix.pch which will give us an instance of our DebugManager class that we can use throughout our app.
#ifdef DEBUG
DebugManager *manager = [DebugManager sharedDebugManager];
#endif
Just remember that when you want to use your manager instance you will need to wrap it in #ifdef DEBUG because when running in production this will not see the instance of manager anymore. So make sure you do:
#ifdef DEBUG
NSLog(#"The DebugManagers instance name is %#", [manager debugName]);
#endif
Don't forget to add your preprocessor macro in xcode under your Build Settings follow this answer to find out how to do that
If you have any questions just ask below.
I attended to a watchkit hackathon yesterday and I had some problems regarding calling a method on an NSObject class which uses the Google Maps API and send local notifications. If I call this method from my Watchkit extension, the code doesn't compile, but If I call from the ViewController, for example, everything works perfectly
#import "InterfaceController.h"
#import "Methods.h"
#interface InterfaceController()
#end
#implementation InterfaceController
- (instancetype)initWithContext:(id)context {
self = [super initWithContext:context];
if (self){
// Initialize variables here.
// Configure interface objects here.
NSLog(#"%# initWithContext", self);
}
return self;
}
- (IBAction)butRoute
{
Methods *mt = [[Methods alloc]init];
[mt notif:#"ARRIVING!"];
//***** If I call this method, my code won't compile!!! *****
}
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
NSLog(#"%# will activate", self);
}
- (void)didDeactivate {
// This method is called when watch view controller is no longer visible
NSLog(#"%# did deactivate", self);
}
#end
The error I get is:
Check the target for your Methods class and make sure it is in your watch kit extensions target.
Alternately, look at building a framework for your shared classes. https://developer.apple.com/videos/wwdc/2014/?id=416
I don't know what xcode version you are using but take into account that the initWithContext method is no longer valid. You should be using:
- (void)awakeWithContext:(id)context
And you shouldn't be overwriting it, just use it.
just remove #import line and replace it with WatchKit framework.
I want to be able to have two classes that are responsible to respond to selectors differently depending if the platform is iOS or OSX.
However, I want to have code that uses only one class, and I want to avoid repeating #ifdefs.
Ideally I would want to have 3 classes:
UniversalClass
iOSSpecificClass
OSXSpecificClass
iOSSpecificClass and OSXSpecificClass both extend UniversalClass.
All calls would be done to UniversalClass, and that class is responsible to call the respective methods of iOSSpecificClass and OSXSpecificClass.
There are two solutions that I came up with:
#interface UniversalClass : NSObject
+ (void) universalMethod;
#end
#implementation UniversalClass
+(id)forwardingTargetForSelector:(SEL)aSelector {
#if TARGET_OS_IPHONE
return [iOSSpecificClass class];
#else
return [OSXSpecificClass class];
#endif
}
#end
The problem with this approach is that UniversalClass promises something in the .h that can or cannot deliver. The warnings also tell us that. Grr. Warnings.
The second approach would be like this:
#implementation UniversalClass
+ (Class)correctClass {
Class aClass = Nil;
#if TARGET_OS_IPHONE
aClass = [iOSSpecificClass class];
#else
aClass = [OSXSpecificClass class];
#endif
return aClass;
}
+ (void)universalMethod {
Class masterClass = [UniversalClass correctClass];
[masterClass universalMethod];
}
#end
The problem with this approach is that I have to perform changes for every method I add and I feel that I am kinda repeating myself without needing.
What are the edge cases I have to pay attention to in both solutions? Is there any better solution than those?
One option is to have a common header file and two different implementations for two targets (one for OSX and another for iOS) that both import and implement the header methods.
Something like this:
Another alternative is to examine if you really need two classes. One #interface and two #implementations (potentially in separate files) is a pattern that I've seen.
Something like (this from CodeRunner where I did my test):
#import <Foundation/Foundation.h>
// #define iPHONE 1
#interface MyClass : NSObject
- (NSString*) someString;
- (BOOL) aMethod: (NSString*) inString;
#end
// common implementations here
#interface MyClass (common)
- (NSString*) commonString;
#end
#implementation MyClass (common)
- (NSString*) commonString
{
return #"same";
}
#end
#ifdef iPHONE
// iPhone specific implementations
#implementation MyClass
- (BOOL) aMethod: (NSString*) inString
{
return [inString isEqualToString: #"iPhone Impl"];
}
- (NSString*) someString
{
return #"iPhone Impl";
}
#end
#else
#implementation MyClass
- (BOOL) aMethod: (NSString*) inString
{
return [inString isEqualToString: #"iPhone Impl"];
}
- (NSString*) someString
{
return #"OS X Impl";
}
#end
#endif
// test
int main(int argc, char *argv[]) {
#autoreleasepool {
MyClass * obj = [[MyClass alloc] init];
NSLog(#"is iPhone? %#", [obj aMethod: [obj someString]] ? #"YES" : #"NO");
NSLog( #"string: %#", [obj someString] );
}
}
You could obviously do this more elegantly by having two .m files and putting one implementation in each (iPhone in one, OS X in the other); or three if you are going to have common routines that are shared by both.
Anyway, just an alternative way to get the same / similar effect - single interface to differing functionality.
You could go with something like this:
#implementation UniversalClass
static Class class;
+ (void)load
{
class = [UniversalClass correctClass];
}
+ (Class)correctClass {
Class aClass = Nil;
#if TARGET_OS_IPHONE
aClass = [iOSSpecificClass class];
#else
aClass = [OSXSpecificClass class];
#endif
return aClass;
}
+ (void)universalMethod {
[class universalMethod];
}
This will keep the promise you made on the .h by implementing the corresponding method (no warnings) and get the right class only once.
How about just ignoring the warning for the specific case of your forwardingTargetForSelector: version? It's like saying “hey, I know what I'm doing!” :-)
Add something like these #pragma calls around your #implementation line:
...
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
#implementation UniversalClass
#pragma clang diagnostic pop
...
See this answer here on Stack Overflow.
The solution that you are proposing is the Class Cluster pattern, which is quite common in Cocoa (e.g. it is used in NSArray, NSValue, etc). Class clusters are classes that return a private subclass from their constructor instead of an instance of the class that was requested. Here is how you might implement that in this case:
MyClass.h
#interface MyClass : NSObject
- (void)someMethod;
#end
MyClass.m
#implementation MyClass
+ (id)alloc
{
if (self == [MyClass class])
{
#if TARGET_OS_IPHONE
return [MyClass_iOS alloc];
#else
return [MyClass_Mac alloc];
#endif
}
else
{
return [super alloc];
}
}
- (void)someMethod
{
//abstract, will be overridden
}
#end
MyClass_iOS and MyClass_Mac would be declared in separate files and privately imported in the McClass.m file.
This seems like a pretty elegant solution at first, but it's not really appropriate for this situation. Class clusters are great for swapping class implementation at runtime when you don't know which implementation you want at compile time (good examples would be supporting different iOS versions, or universal apps that behave differently on iPad and iPhone) but for Mac/iOS we know at compile time which code we need, so introducing a cluster of 3 separate classes is redundant.
This solution doesn't really offer any benefit over the ones suggested by https://stackoverflow.com/users/145108/dad or https://stackoverflow.com/users/3365314/miguel-ferreira because we still have to branch the import code:
#if TARGET_OS_IPHONE
#import "MyClass_iOS.h"
#else
#import "MyClass_Mac.h"
#endif
We could solve that by having a single header for both MyClass_iOS and MyClass_Mac (which was Miguel's solution) or by having both implementations in the same file (which was Dad's solution) but then we've just built a layer on top of one of the solutions you already rejected.
Personally, I would just use a single .m file with three clearly delineated sections:
#interface MyClass
#pragma mark -
#pragma mark Common code
- (void)someMethod1
{
}
#pragma mark -
#pragma mark iOS code
#if TARGET_OS_IPHONE
- (void)someMethod2
{
}
#pragma mark -
#pragma mark Mac code
#else
- (void)someMethod2
{
}
#endif
#end
This avoids creating unnecessary classes and gives you freedom to easily have shared methods or separate implementations for each platform without exposing any of that in the interface.
If the classes for the two platforms definitely won't have any code in common, I'd probably opt for Miguel's solution, which is very clean.
I don't accept the "user confusion" explanation. You'd basically have these three files:
MyClass.h
MyClass_iOS.m
MyClass_Mac.m
I think if someone is confused by what that means, they shouldn't be working on your code base ;-)
You could also combine this with the class cluster approach if you did want to inherit shared code between the two platforms, in which case your MyClass.m file would contain both the shared implementation and the private interface:
#interface MyClass_Private : MyClass
- (void)somePlatformSpecificMethod;
#end
#implementation MyClass
+ (id)alloc
{
if (self == [MyClass class])
{
return [MyClass_Private alloc];
}
else
{
return [super alloc];
}
}
- (void)someSharedMethod
{
//concrete implementation
}
#end
And your project structure would look more like this:
MyClass.h
MyClass.m
MyClass_Private_iOS.m
MyClass_Private_Mac.m
Hope that helps!
while creating a library that will be used on several projects, I encountered an error that I was not able to resolve by myself.
The library is composed of several "modules" that each declares its set of classes. The modules declares a header file that references the classes. Each module header is included in the library header, and all of them are copied to the library target.
The "GMData" module defines the ORM layer of the library, it declares a "GMInitializerBase" class, its purpose is to initialize the module. It must be called once in the UIApplicationDelegate.
The "GMModel" module contains the base model for the application (Categories, Articles, ...), It must register itself to "GMData" in order to function properly.
Structure:
<Library Root>
Library.h
<GMData>
GMData.h
GMInitializerBase.{h,m}
<GMModel>
GMModel.h
GMInitializerBase+GMModel.{h,m}
Contents of Library.h
#import "GMData.h"
#import "GMModel.h"
Contents of GMData.h
#import "... ORM related headers ..."
#import "GMInitializerBase.h"
Contents of GMInitializerBase.{h,m}
#import "... ORM Classes ..."
#interface GMInitializerBase : NSObject {
}
+ (void) bootstrap;
+ (GMInitializerBase*) initializer; // autoreleased instance creator
- (void) setup;
- (void) setupStore:(GMManagerFactory*)factory; // Setup database connection
- (void) setupHelpers:(GMHelperFactory*)factory; // Register helpers (abstract)
- (void) setupManagers:(GMManagerFactory*)factory; // Register managers (abstract)
#end
#implementation GMInitializerBase
+ (void) bootstrap {
GMInitializerBase* initializer = [self initializer];
[initializer setup];
}
- (void) setup {
/* Breakpoint 01 */
GMHelperFactory* helperFactory = [GMHelperFactory sharedInstance];
GMManagerFactory* managerFactory = [GMManagerFactory sharedInstance];
[self setupStore:managerFactory];
[self setupHelpers:helperFactory];
[self setupManagers:managerFactory];
}
#end
Contents of GMModel.h
#import "... Base Models files ..."
#import "GMInitializerBase+GMModel.h"
Contents of GMInitializerBase+GMModel.{h,m}
#interface GMInitializerBase (GMModel_Additions)
- (void) setup;
- (void) setupGMModelHelpers:(GMHelperFactory*)factory;
- (void) setupGMModelManagers:(GMManagerFactory*)factory;
#end
#implementation GMInitializerBase (GMModel_Additions)
- (void) setup {
/* Breakpoint 02 */
GMHelperFactory* helperFactory = [GMHelperFactory sharedInstance];
GMManagerFactory* managerFactory = [GMManagerFactory sharedInstance];
// parent implementation
[self setupStore:managerFactory];
// current implementation
[self setupGMModelHelpers:helperFactory];
[self setupGMModelManagers:managerFactory];
// parent implementation
[self setupHelpers:helperFactory];
[self setupManagers:managerFactory];
}
- (void) setupGMModelHelpers:(GMHelperFactory*)factory { /* ... */ }
- (void) setupGMModelManagers:(GMManagerFactory*)factory { /* ... */ }
#end
Contents of ProjectAppDelegate.m (located in another project, it includes the library.a and search the "Headers" directory)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[GMInitializerBase initializer] setup];
}
Stops at the first breakpoint (Breakpoint 01)
It crashed when in the library:
I declare an addition without overloading a method;
I declare an addition to a Cocoa class ([NSString toto]) without overloading;
In works when in the test project:
I declare an addition to a Cocoa class ([NSString toto]) without overloading;
I didn't try to overload a library class but I assume it will work too.
My problem is the following: I can't get the addition workingm and I need it.
Thanks for reading, thanks for answering.
Make sure you have the -all_load and -ObjC flags set in the "Other Linker Flags" in the project settings. Categories in a library won't work without them.
In Objective-C, you shouldn't override the method in a category of a class. Say you have
#implementation MyClass
-(void)foo
{
NSLog(#"%#",#"original!");
}
#end
and later you have
#implementation MyClass (MyCategoryA)
-(void)foo
{
NSLog(#"%#",#"categoryA!");
}
#end
#implementation MyClass (MyCategoryB)
-(void)foo
{
NSLog(#"%#",#"categoryB!");
}
#end
Then the result of
MyClass* myInstance=...;
[myInstance foo];
is not reliable, see the discussion in the official documentation. The documentation says it works if you have only one category, but the documentation says at the same time you shouldn't use that feature. So, don't do this.
The sole exception is +load. If a category defines this method, the runtime calls it for each category you define. So, if you want to perform some initialization task per category, +load is the way. Read the official documentation and this blog post by Mike Ash.