Build settings in Xcode define how your app is going to be built. Usually, when your app shows build errors or it fails to validate on iTunesConnect, build settings is the place that you should search for errors. Is some path configured in a wrong way? Are the architectures properly set? What about the linker flags? In this post, we will look at several build settings (not all since there are too many), and what errors they might cause. Here’s a list of all the settings for reference.
It’s the first section in build settings, so we will start with Architectures first.
Here you can set the supported device architectures on which your app can run on. It’s always recommended to leave those to Standard – $(ARCHS_STANDARD), which is armv7 and arm64. Apple has now officially ended support for 32-bit apps, so you should always stick to the standard ones.
The Build Active Architecture Only setting defines whether the app should be build for all the valid architectures or only for the one on which the app is currently being run on (simulator or device). Usually, we set this for the Debug configuration to be Yes, because the build triggered from Xcode, will only run on that device. There’s no need to build it for other platforms. On the other hand, the Release setting must be No, because otherwise our release app will only be built with the currently selected device architecture. What about all the other users that have different device architecture? When it comes to the Architecture section, it’s always best to stick to the default settings when the project is created.
We skip few sections, which we mostly keep with the default settings, to go to the Build options.
Here, we can for example set the Compiler for C/C++/Objective-C, but it’s best to keep the default LLVM. For the Debug Information Format, it’s important that for Release, we have DWARF with dSYM File. This is needed because dSYM files are used to de-symbolicate the crashes your app might have. What this means, is that you can see the actual stack trace, with method calls from the crash logs, instead of their memory locations, which makes your hunt for crashes a lot easier.
Another important setting is enabling Bitcode. Bitcode is a representation of the app binary that can be compiled on demand as 32-bit or 64-bit, depending on the device that downloads the app from the AppStore. This also enables compiler improvements made by Apple to be implemented automatically, without the developers needing to re-submit their apps. However, to enable Bitcode, all of your dependencies (frameworks and libraries), have to be compiled with Bitcode.
The Enable Testability flag, as its name implies, determines whether the configuration can be tested. Usually, we set this to true for Debug or Coverage configurations, but leave it to false for the release build. When we do release builds, we also want to make sure to run product-validation tests, specified by the Validate Built Product property.
For the Deployment section, the most important property is the iOS / macOS / tvOS / watchOS deployment target, which is the minimum supported OS version.
Usually, the apps I work on support the latest two OS versions (currently iOS 10 and 11), so for an iOS app this would be iOS 10. The Targeted Device Family defines which devices your app supports – 1 is for iPhone, 2 for iPads, 4 is for Apple Watch.
There are many settings here, we will see few of them.
The Mach-O Type setting defines what would be the format of the resulting binary. If you are developing an app, it will be Executable. It can also be a dynamic or static library, or bundle.
The Other Linker Flags property tells the build system what flags should be send to the linker. Here, the flags to the frameworks you are using will be added. This is usually done automatically when you import a framework, but if you don’t do it correctly, this is the place to fix it.
The Runpath Search Paths property is a list of paths to be added to the runpath search path list for the image being created. For example, ‘@executable_path/Frameworks’ is the frameworks folder in the resulting binary, as shown in the next image. You can find such binary in your Derived Data path or if you unzip generated .ipa file.
In this section, you can find information about how your app is packaged as a binary.
Few important settings here are the path to the Info.plist File, which contains all the relevant information (name, bundle id, supported devices, permissions and much more) about your application. If you receive an error that the Info.plist file can’t be parsed, probably its path is wrong.
You can also set the Product Bundle Identifier, which uniquely identifies your app. You can set different identifiers for different configurations. For example, if you have separate Test Flight apps for Test, Staging and Production, you must provide different bundle ids for all of them.
In the Search Paths section, you tell Xcode where to look for frameworks, libraries, header files.
If you have imported a library or framework, and you get an error that it’s not found, you should look in this section. Usually, the error is either in Framework Search Paths (if it’s a framework), Library Search Paths (if it’s a library) or Header Search Paths (if the dependency is the complete code taken as CocoaPod for example, applicable for Objective-C). When you install the Pods for your project, the newly generated workspace adds the required search paths here.
This section defines how your app is going to be signed – the source of frustration for many junior iOS developers, when they first try to run the app on an actual device.
The Code Signing Entitlements is a file used in the signing of the app that provides information about whether your app for example uses push notifications, app groups, Siri etc. The values defined in the entitlements file, must match the supported capabilities of the app id of your app. If they are not matching, you will have a signing error, stating that the provisioning profile doesn’t support something defined in the entitlements file.
For Code Signing Identity, it’s always best to leave the automatic iOS Developer for Debug. This will try to find a developer certificate in the Keychain that matches the bundle id and provisioning profile provided. If you don’t set it to automatic, if multiple developers with different certificates work on the project, they will always have to override this setting, since otherwise they will not be able to run the app on the device.
For Release, this problem doesn’t happen, since there’s one distribution certificate. But still, it’s good practice to leave it to iOS Distribution, since the certificates expire in a year. When you re-generate it, Xcode will find it automatically from your Keychain.
For the Provisioning Profiles, you can also either set a concrete profile or to be automatic. If it’s automatic, Xcode will try to find a provisioning profile that matches the Product Bundle Identifier set in the Packaging section.
There are a lot of settings for Apple LLVM, static analyzer, warnings, language versions etc.
Few important mentions are Preprocessor Macros, which are especially used in Objective-C. These are macros that you can set for each configuration and then check if they are set from code.
Also, you can set tell Xcode to treat warnings as errors (Treat Warnings as Errors setting). You can also set Objective-C Bridging Header, which is used if you want to use Objective-C framework in your Swift code.
You can also create your own settings and use them throughout the project. We will see one example. You can create user-defined setting by clicking the + icon at the top of the build settings.
Let’s add two values for APP_GROUP and PUSH_ENVIRONMENT. CocoaPods also adds few custom settings.
You can see we have set different values for Debug and Release. This lets us use this constants in one entitlements file. Based on which configuration we build for, the appropriate user-defined settings will be taken. There’s no need to maintain several entitlements (or any other) kind of files. You would only need to put the following to the entitlements file.
You can read the user-defined settings from the Run Phases as well.
As you can see, there are a lot of ways to customize the build for your iOS application. If you open a Watch or Widget target, you will see some other settings apart from the ones we have seen. You can override build settings from the command line, by running the xcodebuild command. Here’s an example on how to do that.
There are a lot of build settings, most of them will remain as the default ones during the lifetime of your application. But when you encounter a problem, a build error, signing error or similar, you should be able to fix it by checking how the build is configured in the build settings.