May 29, 2015

Importing Swift code from Objective-C in a Test Target - It's Possible!

By changing the 'PRODUCT_MODULE_NAME' build setting + adding XCTest.h import in the bridging header file, I was able to make this work.

On my previous post about Swift, I mentioned that one of the biggest issues I had with Swift is the fact I get a compilation error on my unit tests target if I ever use the import "MyProject-Swift.h" in a file that is included in this target (whether it's a test case or production code).
I also mentioned this is a big issue in the Swift talk I gave in Reversim Summit 2015 (in Hebrew).

Well, turns out I was wrong (or at least - it can be fixed).

To fully understand the solution, lets' first make sure you understand the problem and why this is such a big issue for me.

I'll try to show a concrete example so you could understand it, and maybe because all names are stupid it may be harder to relate to, but believe me - if you write new swift code in a real life Objective-C project, you will eventually need to use it SOMEWHERE from your Objective-C code.

Here goes: Lets' say I have an existing Objective-C project with unit tests.
Class A is written in Objective-C and has a unit test.
It also internally uses a class called B, written in Objective-C and doesn't have a unit test.

I now decided I want to add a new class named C. It's written in Swift and I add a unit test for it in Swift.

Now I want to use class C in class B.
I need to add "MyProject-Swift.h" to B's code for it to work, right?

So here's what we've come up with by now:
If I build and run my app, everything is fine.
If I build and run my unit tests target, I get the following error:
'MyProject-Swift.h' file not found

When I originally found out about this (somewhere during the early days of Xcode 6), I was helpless and devastated.

There were only 3 workarounds I could think of and apply to my projects:

  1. Not importing "MyProject-Swift.h", and therefore having to use tricks like NSClassFromString(@"C") and performSelector UGLY as hell
  2. Removing B.m from the Test target (by unchecking the target in the "Target Membership" section of the File Inspector) - Might work for certain cases, but as you see in my example, this means ATest can't compile as it uses A that uses B (that uses Swift code). So if I have enough coverage in my unit tests this is hopeless. 
  3. Porting the whole project to Swift - Too expensive.

This essentially meant that I used swift only for very isolated features in my project, which was very upsetting.

The reason I originally came to the conclusion that it can't be fixed and there aren't any other workarounds, apart from banging my head for a long time about it without success, was an answer on SO basically saying that Apple declared it's a known issue.

In a weird coincidence, just several hours before I decided I should tackle this issue again, a new answer was posted to the same question. It didn't quite solve my problem (I think it's because I didn't move to Xcode 6.3 yet for the problematic project), but it gave me the hope and the direction to a solution!

The Solution
What essentially happens, is that the name of the generated header file that contains all the exported classes from Swift to Objective-C, is called "${PRODUCT_MODULE_NAME}-Swift.h"
What's ${PRODUCT_MODULE_NAME}? By default, Xcode sets it to be the same as the ${TARGET_NAME}, meaning: MyProject-Swift.h for the App target, and MyProjectTests-Swift.h for the test target.

So, we could use some #ifdef statements each time we want to include the generated Swift header, but this is a terrible solution. Instead, I manually changed the product module name the following way:

That's it. For the example project I created for this post it was all I needed to do for solving the problem.
In my real project however, possibly because it was created with an older version of Xcode, I had another issue that I thought was worth mentioning - an issue of Xcode suddenly not running my Swift unit tests (CTest.swift in this example). It was complaining about not finding XCTestCase in the generated MyProject-Swift.h file.
What eventually solved it for me, is adding an XCTest import in my "MyProjectTests-Bridging-Header.h" file in the following way:
Add #import <XCTest/XCTest.h> to MyProjectTests-Bridging-Header.h

And that's it - problem solved.

If you are having problems with the other way around (Swift code importing Objective-C code in Unit Tests), I recommend reading the following post.

Also, if you have found this post interesting, you should follow me on twitter and listen to my podcast (in Hebrew).