Lummox Labs

Mobile app maker since 2015

Import fun

For those of us working in Objective C... 

At my day job with FreshBooks in Toronto we came across a strange issue last week. After making some changes, Xcode started giving my colleague Jameson an error that it couldn't find an #imported header from a Cocoapod. The import was in a file in the test project that he hadn't touched. Nobody else's machine exhibited the problem.

#import <KeyChainAccess/KeyChainAccess.h>

// Can't find header <KeyChainAccess/KeyChainAccess.h> ...

So we tried a bunch of stuff: Try with a fresh repo (fine), look for changes in the pod (none), check the xcode version (same on his machine as on mine). No luck.

The next thing to look at was the Search Paths. There are a number of build settings that dictate where Xcode looks for headers where building. For headers imported with <>, the Framework Search Path is where you want to look. For anything coming from a cocoapod, it's the .xcconfig file that cocoapods generates that will add the right paths to the Search Path build setting. So 

  1. Make sure the .xcconfig file from cocoapods is being used. Check this in the build settings for the project (not the targets). There you can set a per-configuration .xcconfig file which should show the pod xcconfig.
  2. In Build Settings for the target, filter for "Search Path", and press the "Levels" button to see where all the various paths are coming from, and how they are inherited.
Search Paths from a different project

Search Paths from a different project

All of that looked fine on both our machines. And besides, that wouldn't explain why it was failing for him but working for me.

If the Search Paths are set correctly, then Xcode's autocomplete should be able to help out with the #import. So we tried it out on his machine, and sure enough autocomplete worked. That's so odd: Xcode can find the header for autocomplete but not compiling??

Just because, Jameson compiled at this point and it all worked. All we had changed is to re-write the same #import using autocomplete. So we looked at the diff to see what (if anything changed) the diff marked the #import as changed, but it looked the same. ALMOST the same! Notice the difference?

// Bad
#import <KeyChainAccess/KeyChainAccess.h>

// Good
#import <KeychainAccess/KeychainAccess.h>

Okay, yeah. It's easy to see when I lay them out like that. The bad one has a "C" instead of "c". Success! Well, sort of. So Xcode was complaining that it couldn't find that file because we were asking for a file that doesn't exist. That makes sense, but why was the same, misspelled line working on my machine that was failing on his?

We talked about what the changes he had made. And in cleaning up a bunch of #imports, he removed a bunch from other header files (awesome). So in my version of the codebase, the misspelled #import was preceded (through a chain of imports) by another, correctly spelled import, like this:

#import <KeychainAccess/KeychainAccess.h>
...
// A bunch of code imported from a bunch of headers
...
#import <KeyChainAccess/KeyChainAccess.h>

So as long as the misspelled #import follows a correctly-spelled one, it's fine?? Why would that be the case? (See what I did there?)

#import is a bit of helping syntax on #include. In my C++ days, you had to watch out for #including headers twice, because it could cause duplicate symbols. So you surrounded pretty much every header with preprocessor commands to ignore the content if it's already been #included. Like this:

// In MyHeader.h

#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__

... all the codes ...

#endif

You had to do this ALL THE TIME. So in Objective C, #import is like a smart #include, in that it does that for you! But notice what happens when you write the #ifndef: The casing is lost on the header name!

So in our case, the correctly spelled header is #imported. #import creates something like #ifndef for you. Then when you try to #import the misspelled header, it checks #ifndef __MY_HEADER__, finds that it *was* defined before, and doesn't bother trying to find it at all. 

I think, anyways. One of the problems I had while confirming this myself in a test project is that my #imports at home seem to be case-INsensitive. I can happily #import "mYhEaDeR.h" and xcode just figures it out. So there's something at work that I don't know about that makes it matter. 

Regardless, it was an interesting problem to chew on...