Xcode development workflow with binary framework - ios

I'm using an xcframework to hide code.
So I've got an App.xcodeproj and the xcframework in one repo visible to contractors.
Then I have another private project which has everything in App.xcodeproj except has the framework project as a sub-project for easier development (instead of the compiled xcframework).
This works reasonably well except I have to keep the internal (private) and external projects in sync. Changes the contractors make to App.xcodeproj have to be manually brought over to the internal project, updating paths accordingly.
Is there a better way to do this?

I solved this by switching to separate Frameworks instead of a single xcframework, and using an xcworkspace.
It turns out Xcode will prioritize an implicit dependency over the compiled framework. So I only needed my app and framework projects in one (private) xcworkspace.
I'm using the fat framework approach to support the simulator, which is a bit uglier than using an xcframework. There was also a wrinkle in that I'm using Metal in the framework, so I had to load a separate Metallib when running in the simulator.
Perhaps at some point Xcode will have a target type for xcframeworks so it can track the implicit dependencies. See here for more on implicit dependencies.

Related

Integrating XCFrameworks into the development and release workflows

I have an Xcode workspace with an iOS framework target and an iOS SampleApp target that depends on and embeds the framework. The framework is closed-source and thus gets delivered in binary form to customers, in a release package alongside a copy of the sources for the entire SampleApp project, so that they can test it out with our app and see how the integration is done.
With this setup, during the development phase, I can open the workspace in Xcode and build the SampleApp target, and because it has an explicit dependency on the framework, Xcode immediately triggers a build of the framework first and then the app is built and linked correctly.
Now I want to leverage the new XCFramework format to distribute this framework to customers instead of using lipo'ed fat frameworks. I update my shell script that produces the release package to create an xcframework bundle at the end with all the various platforms supported, and also update the SampleApp project to depend on this xcframework instead of on the individual frameworks as it did before.
But here comes the problem with the updated SampleApp: After removing the dependency on the frameworks, the workspace setup described above doesn't work anymore, i.e. Xcode will no longer automatically build the framework target when the SampleApp target is built, because the explicit dependency link between the two is now broken.
My first attempt at solving this involved making the xcframework part of my regular development workflow as well. For this I added a run-script phase to the framework build target that generates a current-platform-only xcframework bundle and copies it to the place where the SampleApp expects to find it, and then added the framework target to the targets of the SampleApp build scheme, in order for the framework to be built when the SampleApp is built. Unfortunately this approach does not work because Xcode 12 always seems to build both targets in parallel, no matter what the 'Parallelize Build' setting is set to, and because of that the app will fail to build simply because the xcframework is not yet ready when it's processed by the build system, which is very early in the build phases of the SampleApp. So unless there's a way to force Xcode to honor the order in which the build targets are defined in the scheme, this approach is a no-go.
If this approach cannot work then I'd have to ship the xcframework with the SampleApp unmodified, and instruct customers wanting to run the app to manually remove the references to the universal frameworks and then add/drag the xcframework to it. This would work but it's ugly and does not seem user friendly enough to me.
I'm also considering a different approach whereby the release script programmatically modifies the SampleApp.xcodeproj to depend on the xcframework instead of on the universal frameworks, but that seems like it could be a very fragile thing to do, so would rather avoid it if there's a better way.
This scenario does not seem unique to me so I'd guess someone else must have faced this problem too. If so, how did you solve it?
So I ended up solving this by programmatically modifying the SampleApp's project file at release time to link and embed the xcframework bundle instead of the universal frameworks.
Initially tried to use the tool pbxproj but it didn't quite work, the initial fat framework removal phase was not succeeding.
Then I figured I could try and diff the project.pbxproj file after having manually replaced the frameworks with an xcframework, and turns out it takes only a few small edits to get there, so they can be done programmatically with a simple shell script like the following:
#!/bin/bash
PROJECT_FILE=SampleApp/SampleApp.xcodeproj/project.pbxproj
FRAMEWORK_NAME=MyFramework.framework
XCFRAMEWORK_NAME=MyFramework.xcframework
sed -i '' "s|FileType = wrapper.framework; path = ${FRAMEWORK_NAME}; sourceTree = BUILT_PRODUCTS_DIR;|FileType = wrapper.xcframework; path = ${XCFRAMEWORK_NAME}; sourceTree = \"<group>\";|" ${PROJECT_FILE}
sed -i '' "s|${FRAMEWORK_NAME}|${XCFRAMEWORK_NAME}|g" ${PROJECT_FILE}
There might be small differences in other project setups but mostly it boils down to that. Just beware that if the xcframework is meant to be placed somewhere other than in the project root then you need both a name and a path properties in the first sed command, e.g.:
name = "MyFramework.xcframework"; path = "relative/path/to/MyFramework.xcframework"

Why Am I Not Able To Embed My Custom Framework?

I have a very simple framework project, MyFramework, that builds successfully. The framework defines a single, global function, myFunction.
The framework also defines 2 global variables: MyFrameworkVersionNumber and MyFrameworkVersionString (These 2 variables were created for me by Xcode)
I have a very simple application project, MyApplication. I am adding the framework to the application project by dragging and dropping the framework package (i.e. the MyFramework.framework in DerivedData that was produced by building the framework) onto the Xcode Navigator and then selecting the framework in the Embedded Binaries section of the project's General tab.
If I add code to MyApplication that references the variable MyFrameworkVersionNumber then I am able to successfully build and run MyApplication.
If I add code to MyApplication that references the function myFunction then I am not able to build MyApplication.
First Update
I suddenly remembered that Carthage does exactly what I am trying to do. So, I used Carthage to build MyFramework and Voila! - I can drag/drop the Carthage build of the framework into the application project and successfully use it. So now my inquiry has become: What does Carthage know that I do not?
Oh for heaven's sake! It turned out to be so simple. MyFramework was being built for a Generic iOS Device (arm64 architecture). MyApplication was being built for a simulator (x86_64 architecture). As soon as I matched those two up all was well. The reason that the Carthage build worked so well is that it produces a universal binary (i.e. one that contains both architectures).
I still do not understand why the MyFrameworkVersionNumber global variable was able to be accessed regardless of the architecture. But I am okay with deferring that little mystery as some arcane bit of information that will be revealed in the goodness of time (perhaps something such as that it is in a header that is structured the same for both architectures).

How to re-export a framework and use it in another project in iOS? (Should I?)

I have two projects I'm working on in XCode. Project 2 depends on Project 1, and I want it to be able to use the same frameworks I have embedded in Project 1 (they're Carthage dependencies). I discovered the "Re-exported Framework Names" section in Build Settings, but I'm not at all clear on how I might use it.
What should I put in that field? Just the name? (PromiseKit). Or the filename (PromiseKit.framework)? Or the full path?
How do I then reference this framework in Project 2? Right now I'm getting a build error: framework not found PromiseKit for architecture arm64 and I can't see any obvious way to add that framework link.
And a broader question: do I need to do this? I'm a relative newbie and working on the assumption that setting up separate Carthage dependencies for both projects means that there will be two separate compiled frameworks in my final binary, which would be a waste. But is XCode cleverer than I think and taking this into account?
If the two projects are related, put them in the same Xcode workspace and then you can share the frameworks between the two.
If the projects are independent, put the frameworks into a separate folder, add them separately to both projects and when adding them to the projects, don't choose Copy files if needed.

How to build dynamic Framework for iOS with no Embedded Binaries?

I try to generate framework named AfTestFramework for ios. I created through Xcode Cocoa Touch Framework with some basic code, ran it and got:
So far so good.
I created new Test Application and drag # dropped AfTestFramework.framework into the project, like crashlytics does (see below)
After building project and running I got Library not loaded ... Reason: image not found:
dyld: Library not loaded:
#rpath/AfTestFramework.framework/AfTestFramework Referenced from:
/var/containers/Bundle/Application/9B12D0AD-94AA-4119-A5DE-6BCAA806FA9F/TestAvoidEmbedded1.app/TestAvoidEmbedded1 Reason: image not found
I know the solution is to attach this framework to Embedded Binaries but I look for the way to avoid this approach.
Further, It demands to create 2 Frameworks: for Simulator and for Release otherwise I cannot release application with my framework, that looks a bit messy and weird.
I found that following frameworks work without Embedded Binaries:
crashlytics
firebase
facebook
Chartboost
ans so on.
So far I didn't find any description, I know also that its something new.
I started to play with xcodebuild like:
xcodebuild -sdk "iphoneos10.1" "ARCHS=arm64 armv7 armv7s" "VALID_ARCHS=arm64 armv7 armv7s" "ONLY_ACTIVE_ARCH=NO" -target AfTestFramework -configuration "Release" clean build
but no success.
Can somebody spread the light on this problem?
If you add a dynamic Framework to your iOS project, you must add it to the Embedded Binaries. In opposite to static libraries are dynamic libraries loaded at runtime. Thus the dynamic linker must access them on the device.
The only possible way to add the code inside of the framework without embedding the framework is to bind it statically to your app. For that you should create a static library from the framework. (Probably, libtool(1) may help you with that.) But anyway you may run into other problems, if the framework needs its bundle structure for loading resources etc.
I'm not quite sure what you are trying to achieve but it sounds like you are trying to create a framework which has a dependency on another framework.
So when used, it would be something like app -> your-framework -> some-other-framework.
I would strongly recommend looking at carthage as a solution to our problem. Carthage is an OS X native dependency manager that is designed to manage dependencies for you.
Firstly you need to check that the framework your framework uses is Carthage friendly. For now I'll assume so.
In your frameworks project you would add a Cartfile where you declare your dependency on the other framework, then use Carthage to download and build the other framework. You then add the built framework to your project as you would with any Apple supplied framework. You DO NOT need to embed the framework and generally speaking it's not recommended to do so. Carthage will handle this issue.
In the app project you would then also add a Cartfile, but you only need to specify your project in it. When Carthage builds the framework, it will automatically locate the other framework, download and build it as well. Then in the apps build phases you simple specify linking agains both frameworks and add a carthage-copy script phase to include them in the Frameworks directory of the finished app.
With regard to building for simulator and release. You don't need to build multiple targets. For a start, using Carthage means that the frameworks are only built when you need them, so this removes any requirement for you to supply binaries.
When telling Carthage to build the dependencies, by default it builds Release binaries. Building Debug frameworks for testing is just a matter of add --configuration Debug to Carthage's command line arguments.
Finally - to be fair, there are also two other package managers out there: Ruby based CocoaPods which is an older manager and for Swift, the Swift Package Manager. Personally I prefer Carthage for reasons I explain in a blog post here.
this is old but if its useful for future reference:
you can do
$lipo -create <path to simulator framework> <path to device framework> -output <path to the output framework>

Embedding frameworks inside closed-source Swift framework

Our company wants to distribute a closed-source SDK for iOS to our clients. I've been using Cocoapods to build the framework and built an example app making use of it. Previously the app worked fine on the simulator as well as when deployed on the device. However, I was also embedding the Pods.framework file in the app itself. One other piece of information that may be of interest is that the framework is written in Swift, the included cocoapods dependencies are both Swift and Objective-C.
I wanted to make the pods requirements easier to manage so the user doesn't need to be concerned with them and tried to embed the Pods.framework file inside of the SDK we're building - so I removed the steps to Embed Pods Frameworks and Copy Pods Resources from the example app, leaving them only in the framework, I also removed Pods.framework as a dependency of the example app, leaving it only in the SDK. This seemed to work in the simulator, but the app now crashes on mobile device with dyld: Library not loaded error.
Upon researching it, I stumbled into a few related discussions:
https://github.com/CocoaPods/CocoaPods/issues/344 https://objectpartners.com/2014/06/25/developing-private-in-house-libraries-with-cocoapods/
However, the suggested solution of using private pods does not look like it would work for us, it's my understanding that the source code in the private pod would still be open, and we can't share it with our clients.
Could someone advise on a solution that would work in this case?
OK, I finally have a more durable solution. It's a modified, cleaner version of my old one now that I understand how Xcode links in my Swift sub-frameworks better
Problem that makes distribution/compilation a bit ugly:
Since Swift standard libraries aren't bundled on the device like Obj-C, nor are they guaranteed to be stable between versions yet (stable binary interface promised in Swift 3: https://github.com/apple/swift-evolution#development-major-version--swift-30) we have to make sure the entire project is compiled against the same version of Swift. That means the guy using your closed-source framework has to be using the same version of Swift in their Xcode for their project as you did for compiling the library, even if he's not using Swift in his code because ultimately it's his version of Swift that gets bundled into the app and your SDK runs against. This is only an issue for closed-source frameworks because open-source ones will always be compiled against the same version as final project. Possible workaround is to restrict clients to same version you use or distribute multiple compilations (i.e. Swift 2.1 and Swift 2.0). To remedy this, you could provide users with copies of binaries compiled against multiple versions of Swift.
Aside from that, here is what I had to do during compilation/distribution to make a binary framework that works in Swift:
When building the framework:
In project target, make sure to add Pods.framework to Linked Frameworks and Libraries (make sure this is a pre-compiled RED version of Pods.framework, I had a black compiled Pods.framework in the same directory which built fine but then resulted in a framework that would cause the project to complain about missing armv7 architecture during linker phase in later project)
In Build Settings, under User-Defined section, add a field called BITCODE_GENERATION_MODE and set it to bitcode
DO NOT #import any frameworks in your bridging header, all instructions telling you to do that are leftover from Swift 1.0-1.2 days, you don't need it anymore and it does more harm than good (the later project will complain that it can't find these headers that aren't even exposed to it)
Change build target to Generic iOS Device, Archive and Export the framework
When building the project using the framework:
Drag and drop the framework into the project, in General tab add it to Embedded Binaries and Linked Frameworks and Libraries (you only need to add the framework itself, not the sub-frameworks or the pods file)
In Build Settings tab, add a new path to Framework Search Paths: $(PROJECT_DIR)/MyFramework.framework/Frameworks
Build the project

Resources