Making an app always requires a lot of debugging and testing. Sometimes additional information must be provided to the u’r QA team. But often this info is not needed on prod build.

To handle this situation we have a lot of options:

  • implement a separate screen
  • add hidden features (like a shake or 4-time tap on some place)
  • add special frameworks that can handle and provide this info for u
  • user remote config (like firebase or something similar)
  • etc.

All solutions are great, but sometimes require a way to more effort to add them into the app.

Settings.bundle

An alternative to all this stuff may be a simple yet powerful solution: Settings.bundle - a special kind of bundle provided by Apple to allow developers to add their app preferences into the iOS Settings app.

U can use this bundle for release and/or for debugging config.

The configuration for this feature is a simple one:

Create

Create a Settings.bundle. Select create a new file and choose Settings.bundle item:

setting_add.png



The system then will check if u’r app bundle contains the Settings.bundle and if so - it will be included in the standard Settings.app

Configure

Configure content. U may have a nested page, localized resources, and even images inside u’r app settings. This content will be appended to u’r app default settings:

demo_settings.png



We can have a different options in there:

  • textField (PSTextFieldSpecifier)
  • switch (PSToggleSwitchSpecifier)
  • slider (PSSliderSpecifier)
  • multivalue selection (PSMultiValueSpecifier)
  • title (PSTitleValueSpecifier)
  • group (PSGroupSpecifier)
  • child pane (PSChildPaneSpecifier)

I won’t describe the whole process of configuring the Settings.bundle content, thus this is perfectly described in Apple doc.

Example of the configuration

example.png



The complete code of the solution

Root.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>StringsTable</key>
	<string>Root</string>
	<key>PreferenceSpecifiers</key>
	<array>
		<dict>
			<key>Type</key>
			<string>PSChildPaneSpecifier</string>
			<key>Title</key>
			<string>DEBUG</string>
			<key>Key</key>
			<string>kDebug</string>
			<key>File</key>
			<string>Debug</string>
		</dict>
	</array>
</dict>
</plist>
Debug.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PreferenceSpecifiers</key>
	<array>
		<dict>
			<key>Type</key>
			<string>PSTitleValueSpecifier</string>
			<key>Title</key>
			<string>Build info</string>
			<key>Key</key>
			<string>kBuildInfo</string>
			<key>DefaultValue</key>
			<string>Not loaded</string>
		</dict>
		<dict>
			<key>Type</key>
			<string>PSTextFieldSpecifier</string>
			<key>Title</key>
			<string>Base URL</string>
			<key>Key</key>
			<string>kFeedURL</string>
			<key>DefaultValue</key>
			<string>Not loaded</string>
			<key>IsSecure</key>
			<false/>
			<key>KeyboardType</key>
			<string>URL</string>
			<key>AutocapitalizationType</key>
			<string>None</string>
			<key>AutocorrectionType</key>
			<string>No</string>
		</dict>
	</array>
</dict>
</plist>
The output

Control

Control the info/change

To control the data shown in settings and entered by a user we must note a few things:

1) Settings.bundle all values are stored in a separate bundle inside a bundle of our app. 2) Stored format - XML key and values, so just a dictionary representation.

To manage all these settings we can use UserDefaults, thus system automatically syncs data for us.

Note that you shouldn’t read from the settings bundle directly, as it makes no sense. You should always fetch and set user defaults using NSUserDefaults. When the user makes a change in the settings application, NSUserDefaults will reflect this automatically. They will always be kept in sync. source

To observe the changes in UserDefaults we can use observers:

NotificationCenter.default.addObserver(
    self,
    selector: #selector(didChangedBundleSettings(notification:)),
    name: UserDefaults.didChangeNotification,
    object: nil
)

or with Combine

NotificationCenter.default
    .publisher(for: UserDefaults.didChangeNotification)
    .sink(didChangedBundleSettings(notification:))
    .store(in: &tokens)

Note: To read the changes for the very first time we may get nothing due to early read requests and non-synced data. The workaround for this - is to read the data from Settings.bundle and register it in UserDefaults using register(defaults:) method.

Manage

Manage environments

Now the interesting part - to allow Settings.bundle only in some environments there is no build-in solution for that. But, as we know, this bundle is just a file inside the app bundle, so we can control the presence of this bundle, replace it and do whatever we want.

To do so, we may use a build script that can remove Settings.bundle from the app bundle for Release config:

BUILD_APP_DIR=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app

if [ "$CONFIGURATION" == "Release" ]; then
    rm -Rf $BUILD_APP_DIR/Settings.bundle
    echo "Removed Settings Bundle"
fi
build_phases.png



Pitfalls

Know u’r pitfalls

  • The name of this bundle must be Settings.bundle. Other options will not work.
  • Different app targets can use different Settings.bundle (from different folders) - change only Target Memebership
  • on early access use register(defaults:) to register Settings.bundle on UserDefault
  • only 1 Settings.bundle can be in app bundle
  • use *.lproj folder with <PLIST-NAME>.string for localization
  • u may observe specific value in UserDefault using keyPath observer
UserDefaults.standard.addObserver(
             self,
             forKeyPath: someValuePrefName,
             options: .new,    
             context: nil
            )

Conclusion

Always look for a native solution, that for the majority part always easy to use and simpler to implement. Settings.bundle is one of this stuff.

Resources