<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Re: Using iOS Settings Bundles to Quickly Switch Between Developer Environments in Product &amp; Engineering Blog</title>
    <link>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/39127#M7</link>
    <description>&lt;P&gt;Hi Adrian&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Just a quick email to thank you very much for this article. It's the best I've read on the subject by far - and I have read quite a few over the last few days as I have been wrestling with a very similar situation.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Best wishes&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Nick&lt;/P&gt;</description>
    <pubDate>Wed, 30 Oct 2024 09:27:06 GMT</pubDate>
    <dc:creator>NickR</dc:creator>
    <dc:date>2024-10-30T09:27:06Z</dc:date>
    <item>
      <title>Using iOS Settings Bundles to Quickly Switch Between Developer Environments</title>
      <link>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/35070#M4</link>
      <description>&lt;P data-renderer-start-pos="1"&gt;Ever had this happen? You've poured your heart and soul into preparing a &lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="28a65abd-eb80-4f20-867b-54a523cad778"&gt;regression build for the QA team to test the upcoming mobile app release&lt;/SPAN&gt;. The deployment pipeline is humming along smoothly, and just when you think you're in the clear, a message lands in your inbox:&lt;/P&gt;
&lt;P data-renderer-start-pos="1"&gt;&amp;nbsp;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P data-renderer-start-pos="277"&gt;QA Team: Actually, we needed a build pointing to QA, but this one's set for development.&lt;/P&gt;
&lt;P data-renderer-start-pos="367"&gt;Me: 🤦‍&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P data-renderer-start-pos="379"&gt;Navigating multiple environments can be like wandering through a maze, and miscommunication feels like an all-too-familiar foe. As if that weren't enough, the process of compiling and deploying new builds every time you need to switch environments can turn into a colossal time sink for developers.&lt;/P&gt;
&lt;P data-renderer-start-pos="679"&gt;But what if I told you there's a better way? (Spoiler alert: There is and its awesome!) Let's embark on a journey to streamline this process and make life a whole lot easier for developers and QA teams alike.&lt;/P&gt;
&lt;P data-renderer-start-pos="679"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="What-Did-We-Do?" data-renderer-start-pos="873"&gt;What Did We Do?&lt;/H2&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P data-renderer-start-pos="890"&gt;So, how did we solve the tangled web of environment-switching woes? The answer lies in the use of Swift's &lt;A class="cc-tgpl01" title="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UserDefaults/Preferences/Preferences.html" href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UserDefaults/Preferences/Preferences.html" target="_blank" rel="noopener" data-testid="link-with-safety" data-renderer-mark="true"&gt;Settings Bundle&lt;/A&gt;. This bundle allows you to create a customisable settings screen that can include various user inputs to configure our app.&lt;/P&gt;
&lt;DIV class="rich-media-item mediaSingleView-content-wrap image-center cc-avcjpy" data-layout="center" data-width-type="percentage" data-node-type="mediaSingle"&gt;
&lt;DIV class="cc-1de099x"&gt;
&lt;DIV data-type="file" data-node-type="media" data-width="1367" data-height="523" data-id="8505a6f1-40d7-46d0-add0-620b1b9507b1" data-collection="contentId-2938964472" data-file-name="Screenshot 2023-10-03 at 8.06.17 pm.png" data-file-size="327096" data-file-mime-type="image/png" data-alt="" data-context-id="2938964472"&gt;
&lt;DIV id="newFileExperienceWrapper" class="new-file-experience-wrapper cc-f59tgt" data-testid="media-card-view"&gt;
&lt;DIV class="media-file-card-view cc-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="Screenshot 2023-10-03 at 8.06.17 pm.png" data-test-progress="1"&gt;
&lt;DIV id="tinyMceEditorAdrianPrice_0" class="mceNonEditable lia-copypaste-placeholder"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="Screenshot 2024-02-09 at 2.09.19 pm.png" style="width: 999px;"&gt;&lt;img src="https://neighbourhood.agl.com.au/t5/image/serverpage/image-id/2897i44B8DF491E3445A1/image-size/large?v=v2&amp;amp;px=999" role="button" title="Screenshot 2024-02-09 at 2.09.19 pm.png" alt="Screenshot 2024-02-09 at 2.09.19 pm.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="1141"&gt;Using this, we are able to construct a settings page that allows an enviroment to be changed with just a few taps or clicks, eliminating the need for time-consuming rebuilds or reconfigurations, and we're excited to show you how.&lt;/P&gt;
&lt;H2 data-renderer-start-pos="1372"&gt;&amp;nbsp;&lt;/H2&gt;
&lt;H2 id="How-we-did-it?" data-renderer-start-pos="1372"&gt;How did we do it?&lt;/H2&gt;
&lt;P data-renderer-start-pos="1388"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P data-renderer-start-pos="1388"&gt;&lt;STRONG data-renderer-mark="true"&gt;STEP 1: Creating and accessing the Settings Bundle&lt;/STRONG&gt;&lt;/P&gt;
&lt;P data-renderer-start-pos="1440"&gt;Using Settings Bundle, setting up custom settings in your app is simple. To create a Settings Bundle, Open Xcode &amp;gt; New File &amp;gt; &lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="087be80a-d16f-48c1-bb90-5bbb211cea49"&gt;Settings Bundle.&lt;/SPAN&gt; Create the bundle making sure not to change the name from 'Settings' (otherwise it won't be added to your app's settings page).&lt;/P&gt;
&lt;P data-renderer-start-pos="1713"&gt;&lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="dc42eba0-12d5-4f07-8356-e243da3f2d6d"&gt;Your&lt;/SPAN&gt; bundle will be auto-populated with a Root.plist file and an en.lprog localizations folder. Given this is just for internal use, we will only need the Root.plist file. There are &lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="37587817-dd2a-4185-bab3-811f2d693be2"&gt;plenty of great tutorials &lt;/SPAN&gt;about setting up more complex settings structures (like this one from &lt;A class="cc-tgpl01" title="https://www.kodeco.com/books/catalyst-by-tutorials/v1.0/chapters/7-preferences-settings-bundle" href="https://www.kodeco.com/books/catalyst-by-tutorials/v1.0/chapters/7-preferences-settings-bundle" target="_blank" rel="noopener" data-testid="link-with-safety" data-renderer-mark="true"&gt;Kodeco&lt;/A&gt;), but for this simple case, we can just add in our values to the Root.plist file directly.&lt;/P&gt;
&lt;P data-renderer-start-pos="2090"&gt;Below is an example of the contents of a Root.plist file that contains a Multi-value Specifier allowing someone to select between Dev, QA/UAT, or Production.&lt;/P&gt;
&lt;P data-renderer-start-pos="2090"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;&lt;LI-CODE lang="markup"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "&amp;lt;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;gt;"&amp;gt;
&amp;lt;plist version="1.0"&amp;gt;
&amp;lt;dict&amp;gt;
	&amp;lt;key&amp;gt;StringsTable&amp;lt;/key&amp;gt;
	&amp;lt;string&amp;gt;Root&amp;lt;/string&amp;gt;
	&amp;lt;key&amp;gt;PreferenceSpecifiers&amp;lt;/key&amp;gt;
	&amp;lt;array&amp;gt;
		&amp;lt;dict&amp;gt;
			&amp;lt;key&amp;gt;Type&amp;lt;/key&amp;gt;
			&amp;lt;string&amp;gt;PSMultiValueSpecifier&amp;lt;/string&amp;gt;
			&amp;lt;key&amp;gt;Title&amp;lt;/key&amp;gt;
			&amp;lt;string&amp;gt;Environment&amp;lt;/string&amp;gt;
			&amp;lt;key&amp;gt;Key&amp;lt;/key&amp;gt;
			&amp;lt;string&amp;gt;dev.environment&amp;lt;/string&amp;gt;
			&amp;lt;key&amp;gt;DefaultValue&amp;lt;/key&amp;gt;
			&amp;lt;string&amp;gt;dev&amp;lt;/string&amp;gt;
			&amp;lt;key&amp;gt;Values&amp;lt;/key&amp;gt;
			&amp;lt;array&amp;gt;
				&amp;lt;string&amp;gt;dev&amp;lt;/string&amp;gt;
				&amp;lt;string&amp;gt;qa&amp;lt;/string&amp;gt;
				&amp;lt;string&amp;gt;prod&amp;lt;/string&amp;gt;
			&amp;lt;/array&amp;gt;
			&amp;lt;key&amp;gt;Titles&amp;lt;/key&amp;gt;
			&amp;lt;array&amp;gt;
				&amp;lt;string&amp;gt;Dev&amp;lt;/string&amp;gt;
				&amp;lt;string&amp;gt;QA/UAT&amp;lt;/string&amp;gt;
				&amp;lt;string&amp;gt;Production&amp;lt;/string&amp;gt;
			&amp;lt;/array&amp;gt;
		&amp;lt;/dict&amp;gt;
	&amp;lt;/array&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;​&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;/DIV&gt;
&lt;DIV data-layout-section="true"&gt;
&lt;DIV data-layout-column="true" data-column-width="50"&gt;
&lt;DIV class="cc-2c6ch1"&gt;
&lt;DIV class="rich-media-item mediaSingleView-content-wrap image-center cc-avcjpy" data-layout="center" data-width-type="percentage" data-node-type="mediaSingle"&gt;
&lt;DIV class="cc-pvjlhb"&gt;
&lt;DIV data-type="file" data-node-type="media" data-width="1179" data-height="2556" data-id="117ee5e5-1208-45e4-9acc-a80562af606b" data-collection="contentId-2938964472" data-file-name="Simulator Screenshot - iPhone 14 Pro - 2023-10-03 at 20.06.51.png" data-file-size="98250" data-file-mime-type="image/png" data-alt="" data-context-id="2938964472"&gt;
&lt;DIV id="newFileExperienceWrapper" class="new-file-experience-wrapper cc-1pj5oru" data-testid="media-card-view"&gt;
&lt;DIV class="media-file-card-view cc-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="Simulator Screenshot - iPhone 14 Pro - 2023-10-03 at 20.06.51.png" data-test-progress="1"&gt;
&lt;TABLE border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="50%"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="ezgif-4-5581d8c13e.jpg" style="width: 461px;"&gt;&lt;img src="https://neighbourhood.agl.com.au/t5/image/serverpage/image-id/2898i894A7A4F321CD244/image-size/large?v=v2&amp;amp;px=999" role="button" title="ezgif-4-5581d8c13e.jpg" alt="ezgif-4-5581d8c13e.jpg" /&gt;&lt;/span&gt;&lt;/TD&gt;
&lt;TD width="50%"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="ezgif-4-c97ab44eea.jpg" style="width: 461px;"&gt;&lt;img src="https://neighbourhood.agl.com.au/t5/image/serverpage/image-id/2899iE71987434BF995F2/image-size/large?v=v2&amp;amp;px=999" role="button" title="ezgif-4-c97ab44eea.jpg" alt="ezgif-4-c97ab44eea.jpg" /&gt;&lt;/span&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;/DIV&gt;
&lt;DIV class="media-file-card-view cc-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="Simulator Screenshot - iPhone 14 Pro - 2023-10-03 at 20.06.51.png" data-test-progress="1"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV class="media-file-card-view cc-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="Simulator Screenshot - iPhone 14 Pro - 2023-10-03 at 20.06.51.png" data-test-progress="1"&gt;&lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="4ffab5ce-f379-42b9-8f6a-bb4f2e8c9694"&gt;To access the selected enviroment, this can be done via the &lt;/SPAN&gt;&lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;&lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="4ffab5ce-f379-42b9-8f6a-bb4f2e8c9694"&gt;UserDefaults.standard&lt;/SPAN&gt;&lt;/CODE&gt;&lt;SPAN&gt; object. An important note though, your S&lt;/SPAN&gt;&lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="3147e231-e5c4-4566-95c5-0a41ad34ac65"&gt;ettings Bundle plist key won't have an asso&lt;/SPAN&gt;&lt;SPAN&gt;ciated value until it is changed. So in the case of it not existing, we will need to decode the plist and access the default value manually.&lt;/SPAN&gt;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;
&lt;DIV class="cc-9n57oc"&gt;
&lt;DIV class="cc-ygvpeu"&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&lt;LI-CODE lang="c"&gt;func getEnvironment() throws -&amp;gt; String {
	guard 
		let selectedEnvironment = UserDefaults.standard.value(forKey: "environment") as? String 
	else {
		// Value for "environment" doesn't exist, so decode the plist and access the default value
  		guard
    		let url = Bundle.main.url(forResource: "Settings.bundle/Root", withExtension: "plist"),
    		let data = try? Data(contentsOf: url),
    		let plist = try? PropertyListDecoder().decode(YourPlistStructure.self, from: data),
    		let item = plist.items.first(where: { $0.key == "environment" }),
    		let value = item.defaultValue else {
      	throw Error.Invalid
    	}
  		return value
	}
	return selectedEnvironment
}&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="4029"&gt;From here you can map that string into some enviroment &lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="27a6db7e-4672-4037-90c3-2e5a55ee280c"&gt;representing&lt;/SPAN&gt; datatype (like an enum) and we are able to start constructing our network calls.&lt;/P&gt;
&lt;P data-renderer-start-pos="4029"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P data-renderer-start-pos="4180"&gt;&lt;STRONG data-renderer-mark="true"&gt;STEP 2: Adjusting our network calls&lt;/STRONG&gt;&lt;/P&gt;
&lt;P data-renderer-start-pos="4217"&gt;Now that we've set up our environment selection mechanism, the next step is to adjust our network calls to use the selected environment.&lt;/P&gt;
&lt;P data-renderer-start-pos="4356"&gt;In this solution, we assume that you have a base domain that changes for each environment, but all the endpoints remain the same regardless of the environment. For example:&lt;/P&gt;
&lt;UL class="ak-ul" data-indent-level="1"&gt;
&lt;LI&gt;
&lt;P data-renderer-start-pos="4532"&gt;&lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;&amp;lt;&lt;A href="https://domain-dev.com.au/api/profile" target="_blank" rel="noopener"&gt;https://domain-dev.com.au/api/profile&lt;/A&gt;&lt;/CODE&gt;&amp;gt;&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P data-renderer-start-pos="4575"&gt;&lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;&amp;lt;&lt;A href="https://domain-qa.com.au/api/profile" target="_blank" rel="noopener"&gt;https://domain-qa.com.au/api/profile&lt;/A&gt;&lt;/CODE&gt;&amp;gt;&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P data-renderer-start-pos="4617"&gt;&lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;&amp;lt;&lt;A href="https://domain-prod.com.au/api/profile" target="_blank" rel="noopener"&gt;https://domain-prod.com.au/api/profile&lt;/A&gt;&lt;/CODE&gt;&amp;gt;&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P data-renderer-start-pos="4661"&gt;For this scenario, you can simply swap out the evironment part of the domain and keep the rest of the API endpoint the same. One way to do this is to list all your environments and their respective base URLs in a separate plist file. Here's an example using the above API endpoints:&lt;/P&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;
&lt;DIV class="cc-9n57oc"&gt;
&lt;DIV class="cc-ygvpeu"&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&lt;LI-CODE lang="markup"&gt;xml
&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "&amp;lt;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;gt;"&amp;gt;
&amp;lt;plist version="1.0"&amp;gt;
&amp;lt;dict&amp;gt;
	&amp;lt;key&amp;gt;dev&amp;lt;/key&amp;gt;
	&amp;lt;dict&amp;gt;
		&amp;lt;key&amp;gt;endpoint&amp;lt;/key&amp;gt;
		&amp;lt;string&amp;gt;&amp;lt;https://domain-dev.com.au&amp;lt;/string&amp;gt;&amp;gt;
	&amp;lt;/dict&amp;gt;
	&amp;lt;key&amp;gt;qa&amp;lt;/key&amp;gt;
	&amp;lt;dict&amp;gt;
		&amp;lt;key&amp;gt;endpoint&amp;lt;/key&amp;gt;
		&amp;lt;string&amp;gt;&amp;lt;https://domain-qa.com.au&amp;lt;/string&amp;gt;&amp;gt;
	&amp;lt;/dict&amp;gt;
	&amp;lt;key&amp;gt;prod&amp;lt;/key&amp;gt;
	&amp;lt;dict&amp;gt;
		&amp;lt;key&amp;gt;endpoint&amp;lt;/key&amp;gt;
		&amp;lt;string&amp;gt;&amp;lt;https://domain-prod.com.au&amp;lt;/string&amp;gt;&amp;gt;
	&amp;lt;/dict&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="5446"&gt;Now, using the selected environment we obtained previously, we can query this plist for the correct base URL and construct our API calls.&lt;/P&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;
&lt;DIV class="cc-9n57oc"&gt;
&lt;DIV class="cc-ygvpeu"&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&lt;LI-CODE lang="c"&gt;struct Endpoint: Codable {
  var endpoint: String
}
  
func getBaseURL(for environment: String) throws -&amp;gt; URL? {
    guard
      let resource = Bundle.main.url(forResource: "Environments", withExtension: "plist")
    else {
      return nil
    }
    
    let data = try Data(contentsOf: resource, options: .mappedIfSafe)
    let environments = try PropertyListDecoder().decode([String: Endpoint].self, from: data)

    guard
      let endpoint = environments[environment]?.endpoint
    else {
      return nil
    }
    return URL(string: endpoint)
}&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="6138"&gt;&lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="e4032cd7-ac53-4c08-8235-1196e3d23dcf"&gt;And voila, we now have our base URL, we can append the remainder of the API path and have a complete request.&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;
&lt;DIV class="cc-9n57oc"&gt;
&lt;DIV class="cc-ygvpeu"&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;&lt;LI-CODE lang="c"&gt;func constructURL(withPath path: String) -&amp;gt; URL? {
  URL(string: path, relativeTo: try? getBaseURL(for: getEnvironment()))
}&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;
&lt;DIV&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="6375"&gt;Then, assuming we are targeting the &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;Dev&lt;/CODE&gt; environment, calling the &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;constructURL&lt;/CODE&gt; function with the &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;'/profile'&lt;/CODE&gt; path will return us &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;&lt;A href="https://domain-dev.com.au/profile" target="_blank" rel="noopener"&gt;https://domain-dev.com.au/profile&lt;/A&gt;&lt;/CODE&gt;.&lt;/P&gt;
&lt;P data-renderer-start-pos="6375"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P data-renderer-start-pos="6539"&gt;&lt;STRONG data-renderer-mark="true"&gt;STEP 3: Making this release safe&lt;/STRONG&gt;&lt;/P&gt;
&lt;P data-renderer-start-pos="6573"&gt;Now, obviously, we don't want our end users to be able to swap the environment the app is running on. Therefore, we will need to disable the settings bundle and default to the production environment when creating release builds.&lt;/P&gt;
&lt;P data-renderer-start-pos="6803"&gt;You can achieve this by running the following script, which deletes the Settings Bundle. It's best included as part of a release CI pipeline:&lt;/P&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;
&lt;DIV class="cc-9n57oc"&gt;
&lt;DIV class="cc-ygvpeu"&gt;
&lt;DIV role="presentation"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;
&lt;DIV&gt;&lt;LI-CODE lang="c"&gt;settings_path="Settings.bundle"

if [[ -f "$settings_path" ]]; then
    rm -rf "$settings_path"
else
    echo "warning: Unable to find '${settings_path}'!"
    exit
fi&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="7115"&gt;Finally, in our code, we can call our getEnvironment with a try? to handle potential errors, such as if Settings.bundle doesn't exist. This way, we'll default to using the production environment:&lt;/P&gt;
&lt;P data-renderer-start-pos="7115"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="code-block  cc-ceksvt"&gt;
&lt;DIV class="cc-9n57oc"&gt;
&lt;DIV class="cc-ygvpeu"&gt;
&lt;DIV role="presentation"&gt;&lt;LI-CODE lang="c"&gt;let environment = try? getEnvironment() ?? productionEnviroment&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;DIV role="presentation"&gt;
&lt;DIV&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;P data-renderer-start-pos="7377"&gt;This ensures that the production environment is always used in release builds.&lt;/P&gt;
&lt;H2 data-renderer-start-pos="7457"&gt;&amp;nbsp;&lt;/H2&gt;
&lt;H2 id="Considerations" data-renderer-start-pos="7457"&gt;Considerations&lt;/H2&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P data-renderer-start-pos="7473"&gt;While this bundle is pretty easy to set up and use, there are some things worth considering to save you from potentially finding yourself in a sticky situation. Some of these points have been mentioned above already, but they are important enough to be worth reiterating.&lt;/P&gt;
&lt;P data-renderer-start-pos="7746"&gt;As mentioned earlier, your &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;UserDefaults.shared&lt;/CODE&gt; object won't have any reference to your environment key until the value is changed in the settings. That's why, in our &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;getEnvironment&lt;/CODE&gt; function, when the key didn't exist, we had to manually decode the plist and access the default value. Yes, you can simply hard code the default value in your code to save some extra coding, but then you'll have two separate default values that could cause confusion for you and others down the line. In this case, a bit of extra code is worth it for clarity.&lt;/P&gt;
&lt;P data-renderer-start-pos="8288"&gt;Another point worth explicitly mentioning is that since we are accessing these values from &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;UserDefaults.shared&lt;/CODE&gt;, the values are actually stored there (obvious, right? 🤯). But this comes with the caveat that you are not the only ones storing values in there. So, it's always worth double-checking that you won't be overriding any existing values and causing yourself trouble later.&lt;/P&gt;
&lt;P data-renderer-start-pos="8670"&gt;The final consideration is whether you should check your environment when the app launches or each time it returns to the foreground. While updating it on foreground entry might seem attractive, it adds extra code and risks putting you in weird states by making requests with different environments in the same session. I recommend implementing it in the &lt;CODE class="code cc-1o5d2cw" data-renderer-mark="true"&gt;AppDelegate&lt;/CODE&gt; during a new session launch. Yes, it requires restarting the app, but this ensures that each session uses a single environment, reducing code complexity and potential issues.&lt;/P&gt;
&lt;H2 data-renderer-start-pos="9213"&gt;&amp;nbsp;&lt;/H2&gt;
&lt;H2 id="Wrap-up" data-renderer-start-pos="9213"&gt;Wrap up&lt;/H2&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P data-renderer-start-pos="9222"&gt;In the ever-evolving world of app development, we've tackled the age-old challenge of handling multiple environments with style and ease. By ha&lt;SPAN data-renderer-mark="true" data-mark-type="annotation" data-mark-annotation-type="inlineComment" data-id="26c326b6-8cd5-46ab-bc73-b885bfbb7492"&gt;rnessing the power of Swift's settings bundles, we've transformed the once-daunting task of switching between development playgrounds into a seamless dance. &lt;/SPAN&gt;&lt;/P&gt;
&lt;P data-renderer-start-pos="9524"&gt;With a few taps in your app's settings, you can go from Dev to QA, or even reach for the stars in Production – all without breaking a sweat. Sure, you might have to give your app a little nudge by closing and reopening it, but hey, we all need a break sometimes, right?&lt;/P&gt;
&lt;P data-renderer-start-pos="9795"&gt;But wait, there's more! This is just the beginning of the settings bundle journey. The beauty of settings bundles lies in their versatility. Beyond handling environments, you can tweak a plethora of app settings – from feature flags and debug logs to enabling or disabling animations. If it's configurable, it can find a home in your settings bundle.&lt;/P&gt;
&lt;P data-renderer-start-pos="10147"&gt;So, go ahead, embrace the power of environments! Keep your codebase tidy, your network calls clean, and your users delighted. With this newfound knowledge, you're ready to conquer the app development universe, one environment at a time.&lt;/P&gt;
&lt;P data-renderer-start-pos="10147"&gt;Happy coding!&lt;/P&gt;</description>
      <pubDate>Fri, 09 Feb 2024 03:22:09 GMT</pubDate>
      <guid>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/35070#M4</guid>
      <dc:creator>Adrian_AGL</dc:creator>
      <dc:date>2024-02-09T03:22:09Z</dc:date>
    </item>
    <item>
      <title>Re: Using iOS Settings Bundles to Quickly Switch Between Developer Environments</title>
      <link>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/39127#M7</link>
      <description>&lt;P&gt;Hi Adrian&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Just a quick email to thank you very much for this article. It's the best I've read on the subject by far - and I have read quite a few over the last few days as I have been wrestling with a very similar situation.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Best wishes&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Nick&lt;/P&gt;</description>
      <pubDate>Wed, 30 Oct 2024 09:27:06 GMT</pubDate>
      <guid>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/39127#M7</guid>
      <dc:creator>NickR</dc:creator>
      <dc:date>2024-10-30T09:27:06Z</dc:date>
    </item>
    <item>
      <title>Re: Using iOS Settings Bundles to Quickly Switch Between Developer Environments</title>
      <link>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/39134#M8</link>
      <description>&lt;P&gt;Thanks for the kind words Nick!! Glad it helped.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Adrian&lt;/P&gt;</description>
      <pubDate>Thu, 31 Oct 2024 00:20:20 GMT</pubDate>
      <guid>https://neighbourhood.agl.com.au/t5/Product-Engineering-Blog/Using-iOS-Settings-Bundles-to-Quickly-Switch-Between-Developer/m-p/39134#M8</guid>
      <dc:creator>Adrian_AGL</dc:creator>
      <dc:date>2024-10-31T00:20:20Z</dc:date>
    </item>
  </channel>
</rss>

