When creating a nested framework, most but not all symbols found

I've got an app where I want to split its Model code into a framework (.xcframework and .framework for debugging) so that it can be used by more than one app.

The code has dependencies on 3rd party code, which are installed via pods.

During the conversion process I keep running into the same issue which manifests with all the 3rd party code - which is that the majority of its api can be used (something like 80-90%) but for the remainder there is a linker error at runtime showing undefined symbols. I have this problem with CocoaLumberjack,RealmSwift, PhoneNumberKit and more.

Its very quick and easy to reproduce the issue with a minimal framework and minimal app, below I'll describe how a minimal setup using CocoaLumberjack reproduces the issue:

From scratch, I use Xcode to create a framework project, run pod init, then modify the pod file to be:

platform :ios, '16.0'
workspace 'TheFramework'
project 'TheFramework'

target 'TheFramework' do
  use_frameworks!
  pod 'CocoaLumberjack/Swift', '3.8.5'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
      config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
    end
  end
end

Then I add source code:

import Foundation
import CocoaLumberjack

public class AClassInTheFramework {
    public class func aMethod() {
        let consoleLogger = DDOSLogger.sharedInstance
        DDLog.add(consoleLogger, with: .debug)
        DDLogDebug("Some logging")
    }
}

Within the Xcode project, Build Libraries for Distribution is set to Yes, I also add that line to the pod file in case CocoaLumberjack isn't set similarly.

In the Framework's Xcode General section, Frameworks and Libraries contains Pods_TheFramework.framework set to Do Not Embed. In the Build Phases section, in the Link Binary with Libraries section, Pods_TheFramework.framework is set to required.

Next I create an Xcode app template, run pod install, and edit the app pod file to be:

platform :ios, '16.0'
workspace 'AppUsingFramework'
project 'AppUsingFramework'

target 'AppUsingFramework' do
   use_frameworks!
   pod 'CocoaLumberjack/Swift', '3.8.5'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
    end
  end
end

I build the framework, and drag and drop it into the app. I add the following code to the app's delegate:

  import TheFramework

  ...
  AClassInTheFramework.aMethod()

The App's target has the following linkage settings:

When I build and run the app, there is the following error:

If I change the source code in the framework to this:

public class AClassInTheFramework {
    public class func aMethod() {
        let consoleLogger = DDOSLogger.sharedInstance
        DDLog.add(consoleLogger, with: .debug)
     //   DDLogDebug("Some logging")
    }
}

Then there is no error and the code runs successfully. This illustrates the problem I've encountered with all the nested frameworks - in this particular case calls to DDLog.add() don't result in an error but calls to DDlogDebug() do, and that has been mirrored with other nested frameworks (for example with Realm, opening a database, adding, finding,retrieving an item all works without a problem, however attempting to use Realm's Results<> API results in a similar symbol not found error).

Additionally note that the identical CocoaLumberjack code can run fine when used directly from within the app, i.e., if I add the following code to the app:

import CocoaLumberjack

    func useCocoaLumberjackDirectlyFromWithinApp() {
        let consoleLogger = DDOSLogger.sharedInstance
        DDLog.add(consoleLogger, with: .debug)
        DDLogDebug("Some logging")
    }

useCocoaLumberjackDirectlyFromWithinApp()

Then it runs, i.e. DDLogDebug() can be successfully called from within the app, its only when its called via the framework that the error occurs.

Why might I be encountering these issues? I'd have thought either I'd be able to use 100% of the nested framework's public api, or 0% of it (is something is not configured correct), not ~80% which is what I am encountering.

Any ideas?

TIA

Answered by DTS Engineer in 829950022

You’re standing at a critical fork in the road here. On the one hand, you can continue down the CocoaPods path. That path is effectively deprecated [1], so it’ll be hard to find folks to help you along the way. And your loneliness will only increase as time goes by.

The other path leads to Swift Package Manager. It’s new and unfamiliar, and it may well have the same bugs, or worse!, but at least you’ll have folks to help you out. And if you encounter a problem that’s unique to your situation, and thus have to do your own research, the knowledge you gain will be useful going forward.

I can’t tell you what path to take, but I can say that DTS doesn’t support third-party tools, so I can’t follow you down the CocoaPods path )-:

[1] https://blog.cocoapods.org/CocoaPods-Support-Plans/


I can, however, offer some general advice. Both CocoaPods and Swift Packager Manager are based on our underlying tools — our compilers and linker — and those tools have documented interfaces. When you run into problems like your dynamic linker error, you can use various lower-level tools to explore what actually happened. For example, you can demangle the symbol name:

% swift demangle '_$s15CocoaLumberjack19asyncLoggingEnabledSbvg'
_$s15CocoaLumberjack19asyncLoggingEnabledSbvg ---> CocoaLumberjack.asyncLoggingEnabled.getter : Swift.Bool

and then work back through the build transcript to see how that symbol was originally generated by the compiler into a Mach-O object file and how those object files were merged by the linker to form a Mach-O image. I have a bunch of links to docs and other resources in An Apple Library Primer. Specifically, Understanding Mach-O Symbols goes a long way to explain how symbols are generated and consumed in Mach-O.

So, it’s possible to research this stuff on your own. You just have to decide on whether it’s worth doing that in the context of your current tooling.

Share and Enjoy

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

@eskimo @DTS Engineer

FYI Note that this issue isn't specific to installing dependencies via pods. If the 3rd party frameworks are added directly to the parent framework, the exact same issues manifest.

i.e.

Accepted Answer

You’re standing at a critical fork in the road here. On the one hand, you can continue down the CocoaPods path. That path is effectively deprecated [1], so it’ll be hard to find folks to help you along the way. And your loneliness will only increase as time goes by.

The other path leads to Swift Package Manager. It’s new and unfamiliar, and it may well have the same bugs, or worse!, but at least you’ll have folks to help you out. And if you encounter a problem that’s unique to your situation, and thus have to do your own research, the knowledge you gain will be useful going forward.

I can’t tell you what path to take, but I can say that DTS doesn’t support third-party tools, so I can’t follow you down the CocoaPods path )-:

[1] https://blog.cocoapods.org/CocoaPods-Support-Plans/


I can, however, offer some general advice. Both CocoaPods and Swift Packager Manager are based on our underlying tools — our compilers and linker — and those tools have documented interfaces. When you run into problems like your dynamic linker error, you can use various lower-level tools to explore what actually happened. For example, you can demangle the symbol name:

% swift demangle '_$s15CocoaLumberjack19asyncLoggingEnabledSbvg'
_$s15CocoaLumberjack19asyncLoggingEnabledSbvg ---> CocoaLumberjack.asyncLoggingEnabled.getter : Swift.Bool

and then work back through the build transcript to see how that symbol was originally generated by the compiler into a Mach-O object file and how those object files were merged by the linker to form a Mach-O image. I have a bunch of links to docs and other resources in An Apple Library Primer. Specifically, Understanding Mach-O Symbols goes a long way to explain how symbols are generated and consumed in Mach-O.

So, it’s possible to research this stuff on your own. You just have to decide on whether it’s worth doing that in the context of your current tooling.

Share and Enjoy

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

When creating a nested framework, most but not all symbols found
 
 
Q