Showing posts with label Swift. Show all posts
Showing posts with label Swift. Show all posts

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

Dec 1, 2014

Why I think Swift is not ready yet




UPDATE Feb '15: With Xcode 6.3, Apple fixed some of the issues this post is talking about. However - there are still some major issues for me, the number one of them is the fact I can't use Swift classes in Objective-C without porting the tests for them to Swift.

So,
Like every relatively-early-adpoter-iOS-developer, I decided to give Swift a try in the past few features I worked on for Piano Maestro.

It was a lot of fun to get into, and I wrote some pretty code and features came out very nice. However, I ran into some issues, that made me eventually come to the conclusion that swift isn't quite ready yet, and I should stick to Objective-C as the main language for developing iOS apps. At least for now.

Here are my top reasons:

Inaccuracies in compilation errors
Often, when a line is relatively complex and has a compilation error, compiler will give you an error message that's simply not describing the real issue.

The most common use case is a simple error like sending the wrong primitive type as an argument to a function (a problem that happens a lot to us Objective-C/C/C++ developers, which are used to implicit conversion of primitives).

Consider the following example:
WAT??
dispatch_after receives exactly these types arguments. So what's wrong here?

Turns out the problem is that dispatch_time expects Int64 to be sent as the second argument, and I actually sent NSTimeInterval.
But how could I know that from that irrelevant error message?

There went an hour of banging my head until I figured this out.


UPDATE Feb '15: at least for this example, was fixed in Xcode 6.3 Beta 1 and error message is now very descriptive and relevant.




Debugging is hard
More head banging often happened when trying to see the value of a variable in the debugger, I eventually find myself adding debug prints instead.
A tip about this: If you get "Some" as the output of trying to 'po var', you can 'po print(var)' instead, and it will (hopefully) work.


Can't use Swift code within Objective-C in unit tests
Update May 2015: I found workaround!

While it's absolutely possible to mix Swift <-> Objective-C code in the app's main target (apart for some limitations I will rant about later), in the test target, I can't import $(PRODUCT_MODULE_NAME)-Swift.h in Objective-C code.
This does not only mean that tests for Swift classes can only be written in Swift (fair enough), it also means that any Objective-C code in my app that uses Swift classes cannot be tested at all. Build will fail if it's included in the test target of the project.
LAME.

See:

Can't use tuples and trivial enums in Objective-C
As the official documentation says, you can't use Swift-only features in your Objective-C code.
This makes sense for some things, but it would make a huge difference if they could have made an effort to expose some of the more trivial features in the $(PRODUCT_MODULE_NAME)-Swift.h bridge:
- Trivial enums can become C enums with a prefix (like it's done in the opposite direction)
- Tuples can become trivial classes
etc.
For now, I can't expose functions that receive a tuple or a Swift enum to Objective-C, and must add some kind of bridge myself (e.g. get an Int parameter instead of an enum and use the constructor with rawValue:). 
Maybe it was too much of an effort for Apple to deal with, but this makes it harder to use these features if I know the class will be used in Objective-C code.

UPDATE Feb '15: you can now use trivial enums!

Can't use c++ in Swift
If like us, your app uses Objective-C++ code and not only Objective-C, you're in trouble and won't be able to use any code that uses c++ types in the header. You'll have to create some kind of Objective-C bridge that will hide the c++ types.


Can't use mocking frameworks
Because of limitations in the availability of the language runtime in Swift, there's no good way to use a mocking framework for tests right now. That's a big problem.
In the meanwhile - best workaround IMO is adding an Inner Class called "MockMyClass" in your test, and overriding any methods you want to mock (like we did in 2006 before mocking frameworks were introduced).

See:

Xcode sucks, AppCode has only limited support
So, as you probably know from my Xcode vs AppCode comparison (a bit outdated, but most of it still applies), I don't use Xcode for coding. It's lacking some very basic IDE mechanisms that AppCode is excellent at (e.g. Extract Method, Quick Fix, Find Usages, ...).
This is still true for Xcode 6.1 and Swift support. Sure, Xcode improved since writing this comparison, but still - I can't even extract method? This sucks.
Unfortunately, Swift support in AppCode is still very rudimentary. But the good news is that they are working on it.
In the meanwhile, here's a link to AppCode EAP that contains some basic features of syntax highlighting and some basic refactoring in Swift. I still don't recommend it over Xcode's current Swift support though.

UPDATE Feb '15: AppCode 3.x has basic Swift support, but still has a very long way to go in order to be as good as it is for Objective-C in terms of refactoring etc.

To sum it up

I guess things are not that bad, because Swift is a very neat language and has some really cool features I'm in love with, like tuples, better enums, cooler closures, generics, property observers, and many many more. 
I assume my complaints are either going to be solved soon, or become less of an issue - many of them are mix&match issues, which only affect apps with existing Objective-C/C++ code. Hopefully

Anyhow, at least for the time being, I guess I'll stick with Objective-C as the main iOS development language.

Further reading:
Developing a Bidding Kiosk for iOS in Swift - a great retrospective with some more reasons of why Swift isn't ready yet.