Mergeable Libraries: Missing Resources

It's not possible to merge a framework with resource into an iOS app target because the resource are not included in the app bundle.

Steps to reproduce:

  • Create an Xcode Project with iOS App Template

  • Add a Framework Target (make sure to "Embed in Application")

  • Add an Asset Catalog to Framework Target

  • Add an Color Resource (or Image Set, or any other Resource)

  • Reference the Resource in App Target (I have used a SwiftUI View)

  • Run on Device (!) to make sure everything works as expected

  • Change "Create Merged Binary (MERGED_BINARY_TYPE)" build setting of app target to "Automatic (automatic)"

  • Change app target settings to link, but not embed framework target (e.g. change from "Embed and Sign" to "Do Not Embed" in "Frameworks, Libraries and Embedded Content" section in "General" tab)

  • Run again (on Device!) and observe how the resources framework resource cannot be found anymore (using SwiftUI you will see a "No image/color named '...' in asset catalog for ..." error message in console logs)

Note:

  • Everything works fine in Simulator
  • Same behavior for Release and Debug configuration
  • Same behavior for manual and automatic merging
  • Same behavior for resources which are not bundled in Asset Catalog
  • When archiving the app, an "Assets.car" file is never present (even when creating archiving for Simulator target, when "Allow archiving for Simulator" is enabled)

Reported as FB13716505 Test Project: https://github.com/iteracticman/MergeableResources/

Accepted Reply

I don’t have time today to work through all of your steps, but I have a couple of bits of feedback.

First, you wrote:

Change app target settings to link, but not embed framework target

That shouldn’t be necessary. When you use a mergeable framework in your app, you still need to embed the framework so that you get all of its resources. However, what gets embedded is a cut down framework, with all the resources but only a stub Mach-O image.

Remember that the Build Mergeable Library build setting only results in merging in release builds. So, if you changed this setting because you weren’t seeing the framework’s code getting merged in, make sure you’re not testing on a Debug build.

The other tricky part here is the involvement of asset catalogue magic. I recommend that you re-test with some code, to see if you can get that working. Once you have it working, you can then go on to explore the asset catalogue stuff.

In my case I put this simple code into my framework:

public final class Greeter {

    public static func greeting() -> String {
        guard let u = Bundle(for: Self.self).url(forResource: "Greeting", withExtension: "txt") else {
            return "+++ URL"
        }
        guard let s = try? String(contentsOf: u, encoding: .utf8) else {
            return "+++ string"
        }
        return s
    }
}

along with a Greeting.txt file. It was able to access call that code and have it fetch that file in both Debug and Release builds, and in the Release build I saw the framework’s code merged into my main app.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

I don’t have time today to work through all of your steps, but I have a couple of bits of feedback.

First, you wrote:

Change app target settings to link, but not embed framework target

That shouldn’t be necessary. When you use a mergeable framework in your app, you still need to embed the framework so that you get all of its resources. However, what gets embedded is a cut down framework, with all the resources but only a stub Mach-O image.

Remember that the Build Mergeable Library build setting only results in merging in release builds. So, if you changed this setting because you weren’t seeing the framework’s code getting merged in, make sure you’re not testing on a Debug build.

The other tricky part here is the involvement of asset catalogue magic. I recommend that you re-test with some code, to see if you can get that working. Once you have it working, you can then go on to explore the asset catalogue stuff.

In my case I put this simple code into my framework:

public final class Greeter {

    public static func greeting() -> String {
        guard let u = Bundle(for: Self.self).url(forResource: "Greeting", withExtension: "txt") else {
            return "+++ URL"
        }
        guard let s = try? String(contentsOf: u, encoding: .utf8) else {
            return "+++ string"
        }
        return s
    }
}

along with a Greeting.txt file. It was able to access call that code and have it fetch that file in both Debug and Release builds, and in the Release build I saw the framework’s code merged into my main app.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

When you use a mergeable framework in your app, you still need to embed the framework so that you get all of its resources. However, what gets embedded is a cut down framework, with all the resources but only a stub Mach-O image.

Interesting! That explains a lot and was not clear to me at all. (Also it does not seem to be documented anywhere?)

In the beginning I analyzed the app bundle to find out if this feature really works as expected and just assumed that something was wrong because I expected that:

  • The resources are part of the framework in "ReexportedBinaries" folder in debug builds (which they are not)
  • The whole merged ".framework" folder should not exist at all in release builds (which it does)

Then I thought maybe I should stop embedding merged frameworks and tried that.

And everything worked great - in Simulator.

And then I tried on device - and it crashed I was totally confused.

And there's also this issue, which made me think this whole feature is just horribly broken.

But apparently it's just very complex and under-documented.

Do you happen to know how to find out if a framework is just a "cut down framework" that really is merged into the app binary or not (to make really sure everything is configured correctly)?

Oh and another point of confusion is that everything works great when not embedding the merged framework, even on device, as long as it does not contain any resources.

Sorry, I must have confused something in my testing. This actually only works with Release configuration on device, not in Debug configuration. (on simulator it always works)

I have looked into the generated app bundle for different builds and compared the value returned by Bundle(for: SomeClass.self) where SomeClass is part of the merged framework:

Simulator/Debug

  • Bundle Path: /DerivedData/.../Build/Products/Debug-iphonesimulator/FrameworkWithResources.framework/
  • ✅ Resources can be found

Device/Debug

  • Bundle Path: /var/containers/Bundle/Application/.../MergeableResources.app/ReexportedBinaries/FrameworkWithResources.framework/
  • Actual Contents of App Bundle:
    • Frameworks/FrameworkWithResources.framework/
      • FrameworkWithResources (stub, 35kB)
      • Assets.car
      • ...
    • ReexportedBinaries/FrameworkWithResources.framework/
      • FrameworkWithResources (real, 104kB)
      • No Resources
      • ...
  • ❌ Resources can not be found because Bundle URL does not match contents of app bundle

Device/Release

  • Bundle Path: /var/containers/Bundle/Application/.../MergeableResources.app/Frameworks/FrameworkWithResources.framework/

  • Actual Contents of App Bundle:

    • Frameworks/FrameworkWithResources.framework/
      • FrameworkWithResources (stub, 35kB)
      • Assets.car
      • ...
    • No ReexportedBinaries folder (as expected)
  • ✅ Resources can be found