BlackDog Foundry Bookmark This page

One of the projects I am working on at the moment could possibly be deployed to the iPhone, iPad and Mac OS X. There is quite a chunk of behaviour that I would like to extract into a common library, and then just develop the UI for the respective platforms in their own projects. The project structure I have in mind is:

  • BDCommon – A shared library that contains logic that is common across all applications
  • MyMacApp – A Lion-based Mac application that depends on the BDCommon project
  • MyPhoneApp – A universal iOS application that depends on the BDCommon project

As you probably know, when it comes to linking libraries, iOS apps can only link static libraries and the preferred approach for Mac OS X apps is to use a framework. So, I decided to create a single project that has two targets:

  • BDCommon.framework (used when linking from a Mac OS X app)
  • libBDCommon.a (used when linking from an iOS app)

This post describes the steps I performed in Xcode4 to achieve this.

Create Common Project

What you are about to do is create a new MAC OS X Framework project called BDCommon. This will be the containing shell for both targets.

Create new Mac OS X Framework

Create a new Mac OS X Framework:

Create new project

Set the product name and company identifier:

Setting project properties

And save the project:

Save the project

Configure BDCommon Project

Now we need to configure our newly create BDCommon project to support having two targets (one for the framework and one for the static library). The default behaviour of Xcode4 is to use the target’s name as the product name. This is OK if you only have one target, but because we want to build two products that have the same “name” (ie. BDCommon.framework and libBDCommon.a) we have to have two targets, both with different names. Hence, we need to decouple the product’s name from the target’s name.

In build settings for the BDCommon target, change the Packaging/Product Name from $(TARGET_NAME) to BDCommon:

Change product name

We will now rename the name of the target itself to BDCommon.framework so that we can easily recognise it in the list of targets:

Change target name

One last thing is to rename the Scheme to BDCommon.Framework (like renaming the Target, this is mostly a cosmetic act that helps us distinguish between the framework’s scheme and the static library’s scheme:

Change scheme name

Perform a build. Confirm that the BDCommon.framework gets created:

Check framework has been built

Now would probably be a good time to commit your changes to git.

Adding the libBDCommon.a Static Library Target

So, we now have a project that contains a target that will build a framework for our Mac OS X apps. Next, we want to add an additional target to build a static library for our iOS apps.

Choose the File/New/New Target… menu option and add a Cocoa Touch Static Library:

Create new target

Set the name to BDCommon and add it to the BDCommon project:

Set static target name

Again, because we want to be able to rename the target, we need to decouple the product’s name by hard-coding it to BDCommon:

Change static product name

Let’s rename the target to libBDCommon.a so that it is more obvious which target is which:

Rename static target

And lastly, let’s rename the scheme to libBDCommon.a to match the target name (again, to make it easier to distinguish between the two targets’ schemes):

Rename static library scheme

Perform a build for both schemes (BDCommon.framework and libBDCommon.a). Here is where you will notice what I believe is a bug in Xcode4:

Static library not built

Despite Xcode4 reporting that the build was successful, libBDCommon.a is depicted in red (indicating that it hasn’t built successfully). If you choose Open in Finder for both products, you will notice that it can find where it built BDCommon.framework, but not where it built libBDCommon.a.

It turns that it has actually built libBDCommon.a – it just displays it in the wrong colour. Using Finder, you can navigate manually to the common build directory and see that they do indeed get built successfully:

Libraries being built

Almost done now. If you have a look at the files in your Project Navigator, you will notice that the BDCommon group is duplicated. This is due to Xcode4 being “helpful” and generating code when you added the second target. Note that it hasn’t actually generated two copies of the physical files… it has just created several references to the existing files. If you expand both BDCommon groups, you can delete the one with the fewer entries:

Extra BDCommon group

Make sure, though, that you choose Remove References Only though, otherwise, it will actually physically delete the files.

Deleting extra groups

When you deleted the references in the step above, you also would have removed the membership of those references for the physical files from the two targets. You need to make sure that both BDCommon.h is in the membership list of both framework and static library targets.

Target membership

The convention for shared libraries (whether they be a static library or a framework) is to have a single monolithic header file (in our case, that will be BDCommon.h) that imports all the other .h files. At the moment, we don’t have any other .h files to include, so go ahead and remove all content from BDCommon.h so that it is an empty file.

Make sure that BDCommon.h is declared as a public header for both targets:

Making headers public

You can safely remove BDCommon.m now, as it no longer serves any really purpose. Note that this time, you will really delete the file (not just the reference).

Perform another build for both schemes and make sure your products get rebuilt correctly.

Again, this would be a good time to check your code into git.

Adding Some Behaviour to the Common Project

Over time, I expect that my common project will evolve to contain many different classes. To illustrate the process for the purpose of this article, we’ll add a simple class called BDLog that has a single method that outputs Hello world. Clearly, this is a contrived example, but it should give an indication on how to add new functionality to your common library.

Add a new Objective-C class:

Create new BDLog class

Set the class name to be BDLog:

Setting class name to BDLog

And save to your BDCommon group. Make sure you included it in both the BDCommon.framework and the libBDCommon.a targets:

Saving new class dialog

Add a static method definition called log to BDLog.h:

+(void)log;

And a corresponding implementation in BDLog.m:

+(void)log {
	NSLog(@"Hello world!");
}

Now you need to declare it as a public header for both your targets so that it is accessible from the application projects:

Making headers public

Modify BDCommon.h to add the following line:

#import <BDCommon/BDLog.h>

When building your libraries, Xcode4 copies the public headers for your static library into a directory like .../DerivedData-dflasdfiouknclskjdchvlawioiewhsk/Build/Products/Debug-iphonesimulator/usr/local/include. This is fine, but it means that you need to change the header search path for every project that wants to include it, which is a pain in the ass. What we can do to make this easier is to rely on a little bit of knowledge about the list of directories that Xcode automatically uses when building. As it turns out, it always adds a directory called include to the header search path. To take advantage of this, we will change the Packaging/Public Headers Folder Path from /usr/local/include to include/BDCommon (per the screenshot below, you can also use $(PRODUCT_NAME) which will automatically substitute BDCommon). Using this path, allows us to import our headers with a statement like #import <BDCommon/BDCommon.h>. This step is only required for the libBDCommon.a target.

Setting public header

Perform a build and check that both the framework and static libraries still build correctly.

Creating a Mac OS X Application

Alright, we are now at the moment of truth. We need to see whether all the hard work above (which should be a once-off setup) allows us to share common code. To do this, we’re first going to create a Mac OS X application and link to our framework.

Create a new Mac OS X app:

Create new Mac OS X app

Set the name to MyMacApp – although it doesn’t really matter… we are just verifying that the framework will link OK:

Setting Mac app name

And save the application:

Saving Mac OS X app

Open the Build Phases tab for your application, and expand the Link Binary with Libraries section:

Linking libraries

Click on the +, and with a bit of luck, our newly created framework should appear in the list of frameworks in the workspace. Select it, and choose Add:

Linking framework

Open up your App Delegate class implementation and add the following statement at the top of the file:

#import <BDCommon/BDCommon.h>

Now add the following code to the applicationDidFinishLaunching: method (if all goes well, Xcode4 will even auto-complete the class and method names for you):

[BDLog log];

Build and run. If everything goes to plan, you will see Hello world! appear in the console output.

Creating an iPhone Application

One down, one to go. Same basic deal as before – we are going to create an iPhone application, but this time we will link in the static library, instead of the framework.

Create a new iPhone app:

Creating iphone app

Set the name to MyPhoneApp:

Setting iphone app name

And save the application:

Saving iphone app

Open the Build Phases tab for your application, and expand the Link Binary with Libraries section:

iphone library links

Click on the +, and our newly created static library should appear in the list of libraries in the workspace. Select it, and choose Add:

iphone link static library

Open up your App Delegate class implementation and add the following statement at the top of the file:

#import <BDCommon/BDCommon.h>

Now add the following code to the application:didFinishLaunchingWithOptions: method (if all goes well, Xcode4 will even auto-complete the class and method names for you):

[BDLog log];

Build and run. If everything goes to plan, you will see Hello world! appear in the console output.

Linking Static Libraries That Contain Categories

If you use Objective-C categories in your common library, you will likely get an error when you link your static library against the iOS app. Apple has written an article that describes the process to resolve this problem. In a nutshell, though, you can modify the Other Linker Flags in your iOS app to include the -ObjC and -all_load options:

Setting other linker flags

Wrapping Up

There are a few things that I haven’t yet touched on that I may include in a future post:

  • Bundling your library for distribution to other developers
  • Private header files

As this was my first foray into this topic, I would be very interested in hearing feedback from you. In particular, if there are pieces of the above that are incorrect or misleading, please let me know and I will correct ASAP.

Drop me a line at craig (at) blackdogfoundry (dot) com

23 Comments »

  1. Xavier says:

    Fantastic tutorial. Exactly what I was looking for

  2. Jackie says:

    Fascinating! Thanks for this clean and straightforward tutorial…took only few minutes to set up and the result is more than I expected :) Also it’s great to know, that frameworks are allowed on Mac AppStore :)

  3. Neal says:

    Excellent tutorial, managed to set it up easily.

    I was wondering how you would go about unit testing? Would you have to have two test targets, one for the framework and one for the library? This seems like it would be a lot of duplication of test code.

    Would love to know the answer to this, or a pointer in the right direction.

    Thanks again.

  4. craig says:

    To be honest, I normally just create a single unit test target and link it against the framework.

    I use GHUnit to manage my test cases for two main reasons:

    1. It is quite quick to launch a Mac app and run through the test cases vs waiting for the simulator to start

    2. I hated the output for the built-in test case runner (trying to find specific messages in amongst all the other stuff that gets output to the console is a nightmare) and I also like to be able to re-run single testcases when a single one fails.

    Both of those reasons led me to use GHUnit linking against the framework.

  5. Steve says:

    Great tutorial. One addition, and one question.

    As of XCode 4.4.1, it appears that .h files can no longer have their target memberships set in the file inspector; there’s a checkbox, but it’s disabled for header files. The workaround for this appears to be to go to the common project -> build phases -> copy files section and add all of the .h files to the list, with the destination “products directory” and the subpath “include/${PRODUCT_NAME}”.

    As far as updating the common library goes, though, I find that while the mac osx app gets changes to the common library without even a rebuild of the mac app, getting changes to stick in the ios app is laborious:

    1. Delete the static library from the ios app
    2. Make changes to the common library
    3. Rebuild the static library
    4. Re-add the static library to the ios app
    5. Rebuild the ios app

    If I omit any of those steps, the changes to the library don’t propagate to the iOS app. Is there something I’m missing in this regard, or maybe a setting somewhere that’s wrong? Danged if I can find it so far.

  6. Steve says:

    And as a follow-up question, I wonder if you’d have any thoughts or advice regarding re-using resources like images, sounds or video in iOS and OSX apps. Since a static library can’t contain resources, it seems like they have to be duplicated in the iOS and either the OSX or Common project, and this seems wasteful and a bit of a pain. Until and unless Apple allows 3rd party frameworks in apps, what’s the cleanest way to handle this issue with dual-platform development?

  7. Chuck says:

    I am trying to do the reverse. That is, I already have an iOS static library project and am trying to also create a MacOS framework. Once I rename the Product Name (to MyLibName) and Target/Scheme (I chose to use MyLibName_iOS instead of the library name) and after deleting derived data and restarting Xcode, my unit tests can no longer find my library’s header files. It appears that renaming the Product Name has also altered many path names in the settings to include the _iOS suffix. I’m new to Xcode (using 4.4.1) and am quite frustrated. I reverted and tried again using the naming convention in the article but it breaks similarly. Any advice?

  8. craig says:

    @steve

    Hmm… interesting observation about the target membership. I can indeed confirm this behaviour on 4.4.1. Interestingly, I can still modify the target membership on header files for static lib projects that had been created in earlier versions of Xcode. I did some googling, and the general consensus appears to be to do what you are doing, but I can’t find anything definitive. Weird!

    I hadn’t noticed the iOS behaviour that you mentioned – although it must be said that I have been focusing on my Mac app rcently, so most of my testing is using the Framework, rather than the static library.

    I haven’t yet had the need to share resources between my iOS and Mac apps (although, I can certainly see lots of potential cases where that would be useful). It is a tricky one because I can envisage that there are actually a bunch of scenarios that would need to be catered for. For example:

    • Shared between Mac, iPhone and iPad
    • Shared between iPhone and iPad
    • Unique to iPhone
    • Unique to iPad
    • Unique to Mac

    Just managing the separation and version control of these bad boys would be challenging.

  9. craig says:

    @chuck

    Renaming targets always seems to end up being harder than it should be. In the past, when I’ve done it, I’ve often had to go back into the Build Settings and use the search field to look for instances of your target name (eg. in your case, I would filter all build settings on MyLib). Most of the time, Xcode uses $(PRODUCT_NAME), but not always. The only way I have found is to go into each setting manually, and check to make sure it is using the correct target name.

    With regard to not finding your library’s header files, a couple of things that I would check are:

    • Are your framework and static libraries actually getting built? When I am in doubt, I use Finder and manually delete the directories out of my derived data directory.
    • Are your include build settings setup correctly? You don’t state how you are running your unit tests, but the way Xcode picks up the include paths for static libs and frameworks is slightly different.

    Good luck!

  10. Tryck says:

    When I include the files to my new OSX project and I try importing the <BDCommon/BDCommon.h>. It says that it’s not included???

    I followed all the steps, but it would be nice if you actually showed where to go for certain things like for the Target Membership options. That took me a sec before I figured out where it was located. Yes I am new to Xcode. :)

    Please someone, HELP!

  11. Błażej Biesiada says:

    @steve

    I handle shared resources with bundles. You have to link a final iOS/OSX app with the bundle manually (along with a library/framework) but at least it all remains in one shared subproject.

    @craig

    You wrote that you create only one test target, hence do you actually test the library only on OSX?

  12. craig says:

    @Błażej – That is correct. I acknowledge that this may expose me a little to possible platform differences, but historically I’ve found that the functionality that I am putting into my shared projects don’t tend to be affected. I guess it depends on what stuff is going into the shared library.

    In the future, if I find that I really do need test coverage across both platforms, I will probably create a common set of test cases within my shared library/framework, and then create two separate projects (one for iOS and Mac) that basically just invoke those shared test cases from within the relevant platform. When I need to do that, I will type up a new blog post describing what I learn.

  13. nevyn says:

    Never use -all_load or -force_load if you can avoid it. -ObjC has worked for a long time now, so using it should suffice (though -ObjC it was broken in Xcode 3.x).

    When linking to static archives, you can avoid importing a lot of symbols by just not referencing to them from the parent project, which can save you megabytes in the resulting binary if you’re linking to a large library.

  14. Motti Shneor says:

    This is a truly marvelous tutorial, But I believe things have changed a bit.

    First off, there are several “iOS Framework” templates for Xcode, that allow almost identical targets for iOS and MacOS (the difference is that the iOS “Framework” is statically linked to the application instead of dynamically like in MacOS “Real” framework.

    All the headers, and internal framework advantages can be now enjoyed by both MacOS and iOS developers.

    Secondly, In the latest Xcode 5.0 preview, there is a strong tendency of Apple to merge iOS and MacOS back to each other, and many settings are now available for iOS and MacOS on the same build target. I still don’t know if it is possible to create a universal framework for iOS and MacOS, but it’s time to test with this…

    Again — thanks. BEAUTIFUL tutorial.

  15. Mike says:

    Thanks for this! The only questions I have are, why not name the OS X framework project BDCommon and the iOS Library libBDCommon instead of going through all of the effort of renaming everything? The other is, shouldn’t you keep both info.plist files and the .pch files so that each target has its own platform specific info (I would also add a #define to one or both .pch files to use when a line or two of code needs to be different between platforms)?

  16. craig says:

    Thanks for the feedback Mike.

    Not sure I follow your question about naming? Are you talking about the project name, or the target name?

    You raise an excellent point about the .plist and .pch files. At the time I wrote the post, I didn’t need different plist or pch files, however, I have subsequently needed to create separate .pch files – one for the static lib and one for the Mac OS X framework.

    And exactly as you suggest, I have two lines in these .pch files that look like:

    #define BDCOMMON_STATIC    1
    #define BDCOMMON_FRAMEWORK 0
    

    And vice versa in the framework file. I haven’t needed separate .plist files yet, but I can certainly foresee the need.

    I plan on producing an updated version of this blog post for Xcode 5 once it is released, and I will be sure to include your points. Thanks again for the feedback.

  17. Marco says:

    Hello, do you know if the same is possible with Xcode 5 ? I am following the steps but when I am trying to have BDCommon.h selected for both targets, the libBDCommon.a is greyed out and not selectable. As a result it seems that I cannot specify the lib as public and <BDCommon/BDCommon.h> is not working.

  18. craig says:

    The process definitely works in Xcode5 (I have been using it for quite a while now) and I’ve just re-checked a couple of different examples that I have previously created.

    If you are able to zip up your project and email to me (craig at blackdogfoundry dot com), I can have a quick look.

  19. Kain says:

    Thanks for the tutorial, but I have the same problem as Marco’s, lib is greyed out in Target Memberships :(

  20. Keith Sharp says:

    I ran into the same problem as Marco and Kain with Xcode 5. The answer seems to be that the libBDcommon.a is missing the Copy Headers build phase by default. Adding this build phase to the target allowed me to add the header files with public visibility.

    The other area that is different is that Xcode 5 automatically creates test targets for both the framework and the static library. This doesn’t create a problem, but for the same cosmetic reasons Craig mentions these should be renamed.

  21. Ramona says:

    Thank you so much for this tutorial.

    I am looking for a similar tutorial but instead of 1 project with 3 targets, including 1 shared library, I need to create 3 projects 1 of which is a shared library between the others (i.e 3 projects rather than 3 targets). Could you possibly direct me on this? any guidance would be extremely welcome.

    Thank you, Ramona

  22. craig says:

    Hi Ramona,

    I must admit I’m not sure exactly what you need. The technique I describe above actually uses 3 projects:

    1. The common library (two targets)
    2. The Mac app project (one target)
    3. The iOS app project (one target)

    Can you provide more info about why your situation is different?

  23. Prez says:

    For those having issues with header files and assigning target membershp to an iOS static lib, the trick seems to be to add a Headers Phase to Build Phase on the static lib target. This will enable to target for membership assignment.

    I’ve verified this only in Xcode 6, so not sure if this applies to Xcode 4 and 5 users.

Leave a Comment »




Categories

Copyright © 2012 BlackDog Foundry