This is a startup guide for an iOS project covering how to use .xcconfig
files to store Xcode
configuration settings, and through them any configuration specific variables, ie settings for Debug / Release or Dev / Test / Stage / Prod.
This guide also covers other topics and serves as a place to store examples of how to do them.
This guide covers setting up .xcconfig
files as a place to store Project
and Target
settings. This enables us to create different builds using different project schemes
and configurations
. The end goal is a code base that is the same for Dev, Test, Stage, and Prod environments, though you can choose to have as many configurations as you want. This allows for settings, for example a server URL in Dev / Test / Stage / Prod, to be stored within the .xcconfig
files. Doing this also makes working with project / target changes much easier to manage in configuration management, ie git
.
It will also cover setting up CocoaPods
for use in these different environments as well as setting up Swift
bridging for interoperability with Objective-C
, both which will involve the .xcconfig
files.
Under Other topics, we cover areas that are not related to the use of .xcconfig
files.
To setup this project, follow the steps below.
- Clone this repository
- Install the
Podfile
cd
toios-startup-guide/ios-startup-guide/
- Run
pod install
- Start
Xcode
and select theios-startup-guide.xcworkspace
, located atios-startup-guide/ios-startup-guide/ios-startup-guide.xcworkspace
- Select each scheme and build it
This will create four versions of the ios-startup-guide
app, with the dev
, test
, stage
, and prod
settings.
As mentioned in the overview, .xcconfig
files allow us to put the Project
and Target
settings there and move them out of the project.pbxproj
file. This makes for much better configuration management as the project.pbxproj
file can be arcane and difficult to read, and hence difficult to diff/merge. Since Project
and Target
settings will override .xcconfig
files, it also allows us to test settings out in the project settings, and easily revert them, before committing them to the .xcconfig
files.
Let's get started!
These are the general steps to create the xcconfig
files. The commits for this can be easily identified in the git log and are all labeled ios-setup.
For this tutorial we used Objective-C
, but the steps will be the same for Swift
.
- Create a
.gitignore
and add in GitHub's gitignore for Objective-C, Swift, and Xcode- I prefer to uncomment the 'Pods/' entry as I don't think it's necessary to check in that directory - my opinion
- Create a new
iOS
project - Create the Debug, Test, Stage, and Release schemes
- Create the Project Configurations
- Modify the Project Configurations
- Modify Debug
- Set the ios-startup-guide project to use the Global
- Set the ios-startup-guide target to use Debug
- Modify Test
- Set the ios-startup-guide project to use the Global
- Set the ios-startup-guide target to use Test
- Modify Stage
- Set the ios-startup-guide project to use the Global
- Set the ios-startup-guide target to use Stage
- Modify Release
- Set the ios-startup-guide project to use the Global
- Set the ios-startup-guide target to use Release
- Modify Debug
- Modify the schemes
- Edit the Debug project scheme and choose 'Debug' where possible in the Build, Run, Test, Profile, Analyze, and Archive settings
- Edit the Test project scheme and choose 'Test' where possible in the Build, Run, Test, Profile, Analyze, and Archive settings
- Edit the Stage project scheme and choose 'Stage' where possible in the Build, Run, Test, Profile, Analyze, and Archive settings
- Edit the Release project scheme and choose 'Release' where possible in the Build, Run, Test, Profile, Analyze, and Archive settings
- Create a Config Group
- Create the .xcconfig files
- Right click on the 'Config' group, select New File..., choose Configuration Settings File, and name it Global.xcconfig
- Right click on the 'Config' group, select New File..., choose Configuration Settings File, and name it Debug.xcconfig
- Right click on the 'Config' group, select New File..., choose Configuration Settings File, and name it Test.xcconfig
- Right click on the 'Config' group, select New File..., choose Configuration Settings File, and name it Stage.xcconfig
- Right click on the 'Config' group, select New File..., choose Configuration Settings File, and name it Release.xcconfig
- Copy the Project settings into the xcconfig files
- Click on the Project and select Build Settings. Make sure that All and Levels is selected.
- Scroll down and check for entries that are in bold under the project column. These entries are different from the default and should be copied into the xcconfig files.
- If there is a value under the iOS Default column, expand it and see if it is the same for all entries. If so, it should be placed in Global.xcconfig. If they differ they should be placed into their respective xcconfig file, ie Debug to Debug.xcconfig.
- Delete the project settings
- Copy the Target settings into the xcconfig files
- Click on the Targets, select the project, and select Build Settings. Make sure that All and Levels is selected.
- Scroll down and check for entries that are in bold under the target column. These entries are different from the default and should be copied into the xcconfig files.
- Expand the entry and check to see which entries are bold. These should be copied into the xcconfig files. For example, if all entries are bold then all xcconfig files should have an entry. If just a particular entry is bold, then only that entry should be placed into an xcconfig file.
- In the end, the Debug.xcconfig and Test.xcconfig should be the same and the Stage.xcconfig and Release.xcconfig should be the same.
- Delete the target settings
- Change the 'product bundle identifier' in the xcconfig files
- This creates the separate builds.
- Change the 'product name'
- This creates a unique name when building the project. Note that this will not work with CocoaPods, so do not do this if you are planning on using CocoaPods; this is purely for aesthetics anyways.
- Create an AppIcon set for Dev, Test, and Stage
- Select the Assets.xcassets, right-click in the area where the AppIcon entry is, select App Icons & Launch Images, and select new iOS App Icon.
- Drag over the images from the resources/images/appicons directory
- Update the App Icon settings in the xcconfig files
- Change the xcconfig files to use the configuration-specific app icon
Now change the scheme and built the project. Do this for all schemes: Debug, Test, Stage, and Release. We will have four separate and easily identifiable project builds.
Storing variables in plist will allow for configuration specific variables, rather than requiring multiple entries in a plist with different key names or having to use multiple plist files. The commits for this can be easily identified in the git log and are all labeled plist-example. The commits cover adding a label to the Main.storyboard
and populating the label text with the value from the plist.
- Create an ENVIRONMENT entry under the User-Defined section of the xcconfig files
- Open Info.plist and create a dictionary entry called 'Project settings'
- Right click on the Information Property List and create an entry of type of Dictionary. It is better to keep all our values that we add under one entry, in effect name spacing them, rather than littering the plist file with arbitrary entries
- Right click on the 'Project settings' and create an entry of type String. Set it to be $(ENVIRONMENT), and this will tell the plist file that the value will be determined from the xcconfig file
If you have downloaded the code, and you switch between the schemes, you can see that each different build shows its environment value in the environment label.
An alternative approach to storing multiple environment specific values in the plist would be to just store the ENVIRONMENT variable, as outlined above, and then create a class in which to retrieve values specific to each environment. The commits for this can be easily identified in the git log and are all labeled settings-file. The commits cover adding a label to the Main.storyboard
and populating the label text with the value from the exampleSetting function provided through the StartupProjectSettingsUtils.h
.
- Create a 'Utils' group under the target entry
- Right-click on the 'ios-startup-guide' entry and create a group called 'Utils'. This group should be at the same level as the AppDelegate entries and NOT at the same level as the 'Config' group.
- Create a new set of Objective-C files called StartupProjectSettingsUtils
- Create both the StartupProjectSettingsUtils, the .m and .h
- Right-click on the 'Utils' group and add a new Objective-C file called StartupProjectSettingsUtils.m
- Add a function called exampleSetting that will retrieve a value based on the ENVIRONMENT plist value
- Right-click on the 'Utils' group and add a new Header file called StartupProjectSettingsUtils.h
- Add interface to the exampleSetting function so that it can be called publicly
- Create both the StartupProjectSettingsUtils, the .m and .h
The pros to storing configuration settings in this manner, rather than through the plist, is that everything is code based. This reduces the probability of the plist file being filled with junk entries, and this is perhaps easier to understand for people new to Xcode development. The downside to this approach would be that each new value to retrieve would require several lines of code to check the value of the current environment.
CocoaPods is an application dependency manager for Objective-C and Swift projects. This allows us to easily install a specific version of a third-party tool, such as AFNetworking
. Using CocoaPods requires the location of the Pods xcconfig files be added to the project, which as we know by now, means adding it to our project's xcconfig
files.
The commits for this can be easily identified in the git log and are all labeled cocapods.
- Run 'sudo gem install cocoapods'
- Run 'pod setup --verbose'
- cd to the location of your project file, ie wherever the .xcodeproj file is located
- Run 'pod init'
- Edit the Podfile
- If CocoaPods was already installed
- Delete the .xcworkspace file (rm -rf .xcworkspace)
- Delete the Podfile.lock file
- Delete the Pods/ directory
- Run 'pod install' in the terminal
- CocoaPods will state that it 'did not set the base configuration of your project because your project already has a custom config set'
- Follow the CocoaPods instructions to add the correct line to the xcconfig files
- In Debug.xcconfig, add #include "Pods/Target Support Files/Pods-ios-startup-guide/Pods-ios-startup-guide.debug.xcconfig"
- In Test.xcconfig, add #include "Pods/Target Support Files/Pods-ios-startup-guide/Pods-ios-startup-guide.test.xcconfig"
- In Stage.xcconfig, add #include "Pods/Target Support Files/Pods-ios-startup-guide/Pods-ios-startup-guide.stage.xcconfig"
- In Release.xcconfig, add #include "Pods/Target Support Files/Pods-ios-startup-guide/Pods-ios-startup-guide.release.xcconfig"
As more developers begin to use Swift, it is worthwhile to see how to incorporate Swift into an Objective-C project. This provides a means to slowly update a legacy project or allow developers who don't know Objective-C to work on a primarily Objective-C based project.
The commits for this can be easily identified in the git log and are all labeled swift-bridging.
- Create a Swift file through the Xcode interface
- When prompted whether to add an Objective-C bridging Header, choose 'Yes'
- This will create the ios-startup-guide-Bridging-Header.h file, where the name comes from the project name
- When prompted whether to add an Objective-C bridging Header, choose 'Yes'
- For Swift bridging to work, 'DEFINES_MODULE' must be set to 'YES' in the project settings
- Open Global.xcconfig and add DEFINES_MODULE = YES
- Move the Swift project settings to the xcconfig files
- Move the Debug and Test settings into Debug.xcconfig and Test.xcconfig
- Move the Stage and Release settings into Stage.xcconfig and Release.xcconfig
- Remove the entries from the actual project settings
- Make sure the 'Product Name' is the same across all xcconfig files
- The header file to import the Swift code is auto-generated based off the 'Product Name', thus if they are different for each environment, the build process will not find the correct header when the scheme is changed from Debug to Test. A series of #IFDEF statements might work, but it is easier if 'Product Name' is simply the same across all builds.
- Create an example Swift class with an example function
- Be sure that the Swift class and function both have a '@objc' at the beginning; without the '@objc', Objective-C will not be able to call the class or function
- Modify an Objective-C .m file
- Import the Swift header file, 'ios_startup_guide-Swift.h'
- Call the Swift class and function as if it was an Objective-C class and function
Using storyboards can be nice, as it visually displays how the views are connected, making understanding how the app works fairly easy. The downside to this approach is that version conflicts can be hard to merge due to the XML that underlies a storyboard file. A good write up on this topic exists here.
Legacy iOS projects though, may not even have a storyboard file. To show how a project can work with just .xib
view files, we remove the Main.storyboard
and launch from a view file. The commits for this are in a separate branch labeled remove-storyboards, found here.
Note that the Launch.storyboard
file should be kept, and if one does not exist, ie on a legacy project, it should be added in. Generally, the Launch.storyboard
is not going to be heavily modified, even in a team setting. This means it is easy to maintain as there really should not be any diff/merge conflicts. Not having a Launch.storyboard
is harder to maintain as a Launch icon
needs to be created and added to the Assets
. This icon may need to change depending on Apple releasing a new phone size or discontinuing support for a phone size. Also, it may not rotate or fill the view well. Hence adding an empty Launch.storyboard
is a better solution.
- Create a new view file, ie an .xib file
- Create a new view controller file
- Edit .xib view file in the Interface Builder
- Set the File's Owner to be the view controller class
- Connect the view outlet to the File's Owner
- In the AppDelegate.m, load and display the view controller
- This will override the Main.storyboard, and now it can be deleted
- Delete the 'Main storyboard file base name' entry from Info.plist
- Delete the Main.storyboard file
- As it is no longer needed, and no longer reference, it can be deleted
It is possible to embed a view controller within another view controller. This is different than having one view controller present another view controller. In this case, the presented view controller is usually presented in full-screen or modally and can just be dismissed. Adding a child view controller is for situations when a view has another view within it, and that new view has its own view controller. Perhaps this quote is clearer "The parent/child relationship is for when a view controller has subviews that are managed by their own view controllers" from this discussion.
The commits for this are in a separate branch labeled child-controller, found here. Note that the delegate setup is not the focus here, just the final commit where the parent view controller adds a child view controller, and then later removes that child view controller.
- Create a new view file, ie an .xib file, as the child view
- Create a new view controller file as the child view controller
- Edit the child .xib view file in the Interface Builder
- Set the File's Owner to be the view controller class
- Connect the view outlet to the File's Owner
- Add a button to the view called 'Close' and add the IBAction to its view controller
- Add a 'closeButtonClicked' function that is called when the button in the view 'close' button is clicked
- Create a delegate in the child view controller's header file
- Create the delegate forward declaration as well as a 'startupChildViewControllerDidFinish' function
- Create a weak property for the actual delegate
- Modify the parent view controller to
- Import the child view controller's header file
- Implement the child view controller's delegate function
- Edit the parent .xib view in the Interface Builder
- Add a button to the view called 'Open' and add the IBAction to its view controller
- Modify the parent view controller to
- Have the 'openButtonClicked' function add a child view controller and display it
- Have the delegate function remove the child view controller and deallocate it
Here is a list of articles I read that cover these topics. I was motivated me to condense them all into one place, hence I wrote this tutorial.
These cover how to setup the different xcconfig files, including different configuration environments, using different App Icons, and setting up CocoaPods.
- http://www.jontolof.com/cocoa/using-xcconfig-files-for-you-xcode-project/
- https://www.appcoda.com/xcconfig-guide/
- https://hackernoon.com/a-cleaner-way-to-organize-your-ios-debug-development-and-release-distributions-6b5eb6a48356
This covers why the schemes were 'shared' and why they need to be shared in a team setting.
This covers the usage of xcconfig
files in more detail, including topics like variable overriding, conditional variables, build setting inheritance, etc.
I was not sure whether or not the .Podlock
file should be committed when I was first using CocoaPods. This is why you should commit it. Reading the gitignore for Objective-C, Swift, or Xcode also covers whether you want to commit other files, such as the Pods/
directory, and why you should or should not.
This Stack Overflow question has a series of good answers that cover the steps needed to incorporate Swift in to an Objective-C project and then how to actually import the Swift code into a .h
or .m
file.
Covers a good range of iOS topics, such as the pros and cons to using storyboards, dependency management, project structure, etc - all of which were used in this example project.