iOS: Embedding frameworks into extensions, debugging issues

Since iOS 8, apple allows developers to ship and make use of dynamic libraries. This allows us to easily share code between our different targets, and raises a tremendous challenge to package, distribute and embed those frameworks properly.

This gets very tricky when your app has both:

  • A Today widget

  • A Watch App + Watch Extension

  • Another kind of Extension(Share extension for example)

Today I would like to focus on how to debug and properly package a watch App. This post started with me receiving the following error from iTunes Connect after uploading a build:

Invalid Bundle - One or more dynamic libraries that are referenced by your app are not present in the dylib search path.

When receiving this message, apple has a technical note which explains how to debug framework issues. For this message apple instructs us to follow the steps in Inspecting A Binary's Linkage .

In short it says, run otool on each executable to check what it links against and make sure the frameworks are properly embedded. Why the big fuss.

But how should properly embedded frameworks look like and how should you interpret the results of otool ? To do this we're going to use the poster child project from Apple called Lister (Download it here). Lister is an iOS app with a Watch App and Watch app extension.

First we must build our app (And so we do). Then , we go to Products-> Show in finder and press Show Package Contents on the app

You might have an XCArchive, but inside there is still an app you can inspect. Now we dive straight into this app of ours's structure:

Right now we could run otool -L PathToListerExeecutable to find out which frameworks our app links against. Running this command yields

    @rpath/ListerKit.framework/ListerKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1349.13.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.dylib (compatibility version 1.0.0, current version 1238.0.0)
    /System/Library/Frameworks/NotificationCenter.framework/NotificationCenter (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 3600.6.21)
    /System/Library/Frameworks/WatchConnectivity.framework/WatchConnectivity (compatibility version 1.0.0, current version 124.0.0)
    @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftCoreGraphics.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftDarwin.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftDispatch.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftFoundation.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftSwiftOnoneSupport.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftUIKit.dylib (compatibility version 1.0.0, current version 800.0.63)

Dont be scared. All we care about is the lines that start with @rpath. Those are things that we should expect to see in the frameworks folder. We see @rpath ListerKit.framework and a bunch of @rpath/libswift.. Sure enough, our app contains these items under the Frameworks directory:

So far so good. This means that our frameworks are properly embedded. Next up: The watch app. The Watch app is located (unsurprisingly) under the Watch folder.

Before we dive into it's structure it is important to note that you should not embed frameworks into your Watch App target. Instead you should embed frameworks into the Watch App Extension target. Doing it differently gets your app rejected on iTC. Why? As Apple puts it :

A Watch app consists of two bundles: the Watch app bundle and the WatchKit extension bundle. Figure 3-1 shows the relationship between these two bundles. The Watch app bundle contains your app’s storyboards, while the WatchKit extension contains your app’s code and additional resources here.

So we embed our Watch Extension into our Watch App:

And we only embed and link against frameworks in our Watch Extension:

Lets continue inspecting our Watch folder. Opening it yields:

This is very similar to our main App parent folder. The PlugIns directory contains our Watch Extension.

Here, inside the Frameworks directory you should only find Swift dylibs. Anything else in the Watch App Frameworks folder and you will get your app rejected on iTC.

Next. We go into PlugIns and we open our Lister Kit Watch Extension:

Here we see the same folder structure we saw before and we also see that the Frameworks directory contains swift libraries and a ListerWatchKit Framework.

If we run otool -l PathTo-Lister-Watch-Extension it will give us the following output:

System/Library/Frameworks/WatchKit.framework/WatchKit (compatibility version 1.0.0, current version 1.0.0)  
    @rpath/ListerWatchKit.framework/ListerWatchKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1349.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.dylib (compatibility version 1.0.0, current version 1238.0.0)
    /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (compatibility version 64.0.0, current version 1070.2.0)
    /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 3599.7.0)
    /System/Library/Frameworks/WatchConnectivity.framework/WatchConnectivity (compatibility version 1.0.0, current version 116.0.0)
    @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftCoreGraphics.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftCoreLocation.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftDarwin.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftDispatch.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftFoundation.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftHomeKit.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftMapKit.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftSceneKit.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftSwiftOnoneSupport.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftUIKit.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftWatchKit.dylib (compatibility version 1.0.0, current version 800.0.63)
    @rpath/libswiftsimd.dylib (compatibility version 1.0.0, current version 800.0.63)

Remember, we only care about things that start with @rpath. We see here that swift libraries and ListerWatchKit we link against. This is correct and they are indeed found under the Frameworks directory. If your otool output doesn't match what you see in the Frameworks folder, you've got a problem, most likely you forgot to embed frameworks and only linked against them.

The Catch: What Apple doesn't tell us however is that each embedded framework might also link against other frameworks. We need to check each framework for this.

What I needed to do was to run otool -L Path-toListerApp/Watch/PlugIns/Watch Extension/Frameworks/ListerWatchKit.framework/ListerWatchKit and check which frameworks it links against. If it links against other frameworks, then you need to embed+link them in the Watch Extension target and they should show up in the Frameworks folder as well.

Frameworks on iOS are flat. They can't embed other frameworks. So if you have a framework which makes use of another, you will want to embed both of them side by side.

Side note: Embedding Swift libs into your targets is another story. This answer has helped me a lot, and we currently set Always Embed Swift Libraries to YES for our Watch App, and NO for our Watch Extension. This works but it is counter intuitive to the architecture apple defined previously where the Watch App is for UI resources, and the Watch Extension is for code resources.

So how does our app structure look like when it is accepted on the AppStore?

- Lister App
  - Frameworks
     - SwiftLibs.framework
     - FrameworksYourAppTargetLinksAgains.framework
  - PlugIns
     - Today Widget Extension
     - Share Extension
  - Watch
     - Watch App
       - Frameworks
         - SwiftLibs.framework
       - PlugIns
         - Watch Extension
           - Frameworks 
             - WatchFrameworks.framework
           - Watch Extension Executable
       - Watch App Executable
  - ListerAppExecutable

If you need help, or if you're facing the same issues ping me on twitter. The information on the topic is scarce and it helps to get together and share knowledge

The Code Bug

A passionate iOS developer. Looking to radically improve the way we all develop software.

Amsterdam