How to add a Diagnostic Mode using Settings.bundle and SwiftUI
Tags: swift, ios, tutorial, mobile
Published: 2022-04-27 20:58:11

---
title: How to add a Diagnostic Mode using Settings.bundle and SwiftUI
published: true
date: 2022-04-27 09:39:54 UTC
tags: swift, ios, tutorial, mobile
canonical_url: https://sowenjub.me/writes/how-to-add-a-diagnostic-mode-using-settings-bundle-and-swiftui/
---

Technical stuff shouldn't surface in your app, but it can be useful at times: let's see how we can enable a "Diagnostic Mode" or a "Developer" menu in your app using Settings.bundle.

![](https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-14.40.05.png)

# Fight your inner geek

Before diving into the instructions, here are some personal insights explaining why I needed this.

[![XKCD about solving vs creating problems](https://imgs.xkcd.com/comics/manuals.png)](https://xkcd.com/1343/)

I get it, _you love_ tweaking everything, and your app should let users gut it open and dissect everything.

But for the average user, _anything technical can create confusion and complexity_. Our job as software developers is to solve problems, not create new ones. And by offering too many options, you're creating a burden for your users: they have to understand what these options are for, figure out if/when they need them and learn how to use them.

Even as a tech-savvy user, I don't want to be bothered with too many options inside the app itself if it's not part of the experience.

My personal rule is that **any menu item that users are unlikely to need shouldn't appear in the in-app Settings.**

For instance, my app has a hidden "iCloud Sync" menu option in its Settings to help identify potential iCloud sync issues.

 ![](https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-13-mini---2022-04-27-at-10.33.19.png)

 ![](https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-12-Pro---2021-09-22-at-22.10.09-3.png)

 ![](https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-12-Pro---2021-09-22-at-22.10.12-2.png)

Until now, this menu appeared automatically based on the connectivity status (which I get from [CloudKitSyncMonitor](https://github.com/ggruen/CloudKitSyncMonitor)).

But there are 2 drawbacks:

1. **it can be disturbing or anxiety-inducing** : sometimes the menu will appear for a split second during the course of a normal event, and if you don't know what this is about it can be troubling
2. **you can't access it willingly** : I added a "Data integrity" section that is not directly related to CloudKit's connectivity status and I want to be able to access it

So I wanted to have a way to reveal this menu manually, and this is where Settings.bundle comes in.

This means I will also be able to add other menu options that could help me collect data, like the entire SQLite store if need be.

**By adding a toggle in the app Settings (the ones inside the device Settings, not the in-app Settings), where (almost) no one ever goes looking.**

# Adding Settings.bundle

I'm only going to cover adding a "Diagnostic Mode" toggle, I'm sure you can figure out the rest from there.

First, add a new file (⌘N) and look for "Settings Bundle"

![](https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-10.51.10.png)
_Snapshot of template picker_

This will add the Settings.bundle which includes a default Root.plist file and a localization file.

In the "Preference Items":

- keep only the "Toggle Switch"
- change the `Title` to "Diagnostic Mode"
- change the `Identifier` to "diagnostic\_mode\_enabled"
- set the `Default Value` to NO so that it's `false` by default

It should look like this:

![](https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-10.52.47.png)
_Target Root.plist_

Let's see if this value is available to us:

- add a breakpoint somewhere in your app 
- run it (⌘R)
- get into the console
- Enter `po UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")`

`false`, it works 🥳

**WRONG.**

Go back and now enter`po UserDefaults.standard.dictionaryRepresentation().keys.sorted()`

**The** `diagnostic_mode_enabled` **key is missing 😒.**

You only got `false` because `bool(forKey:)` returns false if the key doesn't exist, and not because `Default Value` is "NO".

**Here's the catch:** that key won't be available until the user changes the setting.

So you'd better be careful about the default values you use there. In our case, since we want false by default (including if the user didn't touch the settings), NO is perfect.

# Showing the button in your SwiftUI view

The only thing you have to do is to listen to the `UserDefaults.didChangeNotification` and toggle a @State variable to show or hide your Button or NavigationLink.

It should look something like this:

```
struct SettingsSection: View {
    @State var isShowingCloudLink = false

    var body: some View {
        Section {
            if isShowingCloudLink {
                NavigationLink(destination: CloudSettingsView()) {
                    Label(…)
                }
            }
        }
        .onReceive(
            NotificationCenter.default
                .publisher(for: UserDefaults.didChangeNotification)
                .receive(on: RunLoop.main)
        ) { _ in
            isShowingCloudLink = UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")
        }
    }
}
```

_Code Sample of the SeriousSection in my Settings_

Don't forget to receive on `RunLoop.main` or you will get a notification from Xcode warning that you are "Publishing changes from background threads".

🍒 As suggested by [@alpennec](https://twitter.com/alpennec/status/1519276975302529025), it's a good idea to wrap the `isShowingCloudLink = UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")` in a `withAnimation {}.

# Localizing the Settings

If your app is available in multiple languages, you'll probably want to localize the settings.

Unfortunately, this has to be done manually (instead of the usual box-checking from the File Inspector)

- Right-click on "en.lproj" and select "Show in Finder"
- Duplicate and rename "en.lproj" for each language you support

Since the Settings.bundle is added as a reference, you don't have to drag them into Xcode, they will appear automatically.

If you're using [Localazy](https://localazy.com/register?ref=aAHPjuDt3H4D-eth) like me, here's what the config for your `localazy.json` could look like (more about this [here](https://sowenjub.me/writes/translating-your-ios-app-with-localazy/))

```
{
    
    "upload": {
        "files": [
            {
                "type": "ios-strings",
                "pattern": "YourApp/Settings.bundle/en.lproj/Root.strings",
                "path": "YourApp/Settings.bundle"
            }
        ]
    },
    
    "download": {
        "files": [
            {
              "conditions": [["!startsWith: fastlane, ${path}"]],
              "output": "${path}/${iosLprojFolder}/${file}"
            }
        ]
    }
}

```

# 🫵 Enjoyed this article?

If you liked this article or want to talk, please like/RT/comment this tweet:

{% embed https://twitter.com/sowenjub/status/1519273609457414144 %}