Recently at Fyuse, we've started breaking out our main functionality into a public SDK that clients can use to view and create fyuses in their own apps.
Ok so first things first, let's pull our camera and fyuse viewing code out into their own frameworks. Should be easy right? Well no, especially not when you've got a 4 year old app that relies on internal computer vision libraries big enough to take 30+ minutes to build on your perfectly adequate 13" Macbook Pro. And definitely not when the app was built in startup land where, let's face it, "working" usually edges out well-factored.
This means we've really had to dig into our internal dependencies and think about how we wanted things to be structured. For me, knowing exactly how all the build systems an app relies on has been a bit of a weak spot for a while so this has been a good opportunity to really sit down and think about what happens when I hit the "run" button.
Compile Step
The first thing that happens when you hit "run" is that Xcode will start compiling each source file included in your app's target as well as any dependencies targets. The files that will get compiled are listed in Build Phases > Compile Sources. If there are any syntax errors, you'll get a build error for a failed compilation.
For each source file, the compile step will create a compiled binary object file.
Linker Step
Next, all of the .o (object) files that were generated during the compile step are linked together into a final archive or ipa.
Linker errors are, in general, less common and can end up being a bit more annoying to track down. Basically, your app thinks it should have a definition for some symbol and when it gets to this step the actual definition is nowhere to be found.
I've seen this when symbols were renamed with #defines or when a static lib was compiled with dynamic dependencies that weren't included in the target app. Originally, static libs in iOS only contained their own code and always expected client apps to pull in dependencies for them, though this might have changed with Xcode 9.
Ways to Include Dependencies
Subprojects and Workspaces
A project is something that Xcode can actually open and contains a list of targets that can be built.
A target is something that has a set of headers and source files as well as specific set of configurations set in Build Settings. Most commonly targets will output either a compiled framework, static library, or executable app.
Traditionally, the main way of including dependencies from source was to add a subproject to your app. In the case where you have a lot of projects that all depend on each other, you can use a workspace to eliminate the need to add a duplicate copy of the subproject to each other project that depends on it.
The pros of adding a dependency as a subproject are that you know exactly what code you're putting into your app and you can still set breakpoints in the code you've included. If your dependency is a 3rd-party library, then a big downside can be that any updates you want to do need to be done by hand, though this does force you to be very intentional about your updates.
Precompiled Libraries and Frameworks
Alternatively, if you don't care about having source files for your dependencies, you can compile your dependencies as either a static library with accompanying headers or a dynamic framework, which contains the headers in the framework bundle.
These are both added to your project in the Frameworks folder and will need to be added to your app by adding them in the Linked Frameworks and Binaries section. If you chose a static lib you'll need to add the path to the lib's headers to your Header Search Paths section in Build Settings. If you went with a framework, then you need to add the path to the framework to the Framework Search Paths list.
In general, I don't really see a reason to use dynamic frameworks for iOS apps. On a Mac they make sense since the system can share dynamic libraries and avoid loading them more than once, but for iOS the only difference I see is that they make your app take longer to launch. Totally possible I'm missing some big advantage but as far as I can tell there is none.
The big advantage here is that this pre-compilation can mean saving tons of time if your dependency is relatively large and rarely changes.
Automating Your Dependencies
If you don't care to manually manage subprojects or precompiled libs, you do have some other options.
1) Cocoapods: A super popular option is Cocoapods. To use it, all you need to do is define a Podfile for your app that has a list of frameworks you want to use. Then, you just run "pod install" and Cocoapods will magically pull down the framework code, generate a special "pods" project as well as a containing workspace that allows your app to use the pods project as a dependency. Each framework you've specified will show up as a target inside the pods project with build settings configured based on that frameworks podspec file.
2) Carthage: A popular competitor to Cocoapods has been Carthage. The difference here is that you define a Cartfile that specifies a list of frameworks, and then Carthage will pull down and build .framework binaries for you. You then need to include these binaries as dependencies in your app manually.
People seem to like that things are a little less magical with Carthage, but you do give up access to the source code (and thus, breakpoints/easy debugging) while gaining the ability to feel like you have more fine-grained control of your dependencies, so which one you choose really depends on where your priorities lie as well as how much control you feel you need.
Conclusion
I'll be honest, this isn't the most interesting topic in the world, but it's definitely something that you'll need to deal with at some point, so it's nice to know what your options are and what the pros and cons are of these options.