May 29, 2015

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

TL;DR
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?
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:
PRODUCT_MODULE_NAME = "MyProject"

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.

BTW
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).

5 comments:

  1. Hi thanks for the post. I'm in a similar situation (trying to write swift tests including Obj-C files that use Swift code) and I got past the error you're getting by following this SO post: http://stackoverflow.com/questions/26473058/ios-myproject-swift-h-file-not-found-when-running-unit-tests-for-swift/29111547#29111547

    However I'm now getting a different type of linker error:
    "_OBJC_CLASS_$__TtC10AppName20ClassA", referenced from:
    objc-class-ref in ClassB.o"

    Any idea what this could be? Thanks!

    ReplyDelete
    Replies
    1. I'm the same problem here. Did you manage to solve this?

      Thanks

      Delete
    2. This comment has been removed by the author.

      Delete
  2. Hi,
    Thanks for the post,Really you given a valuable information.worth to read this type of articles .very grateful to you for sharing this article.
    Thank you.
    oracle fusion training

    ReplyDelete