Configuring Managed App Configuration for iOS Apps
Managed App Configuration—Apple MDM mechanism allowing IT admin to pass config to app without user involvement and without hardcoding parameters. MDM server sends plist dictionary, app reads from UserDefaults. Standard for any corporate iOS app—works with Jamf, Intune, Workspace ONE, MobileIron, any MDM.
How App Reads Managed Config
Everything stored in UserDefaults under key com.apple.configuration.managed:
func loadManagedConfiguration() {
guard let config = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") else {
// Device unmanaged or config not yet delivered
applyDefaultConfiguration()
return
}
let backendURL = config["BackendURL"] as? String ?? AppDefaults.backendURL
let tenantID = config["TenantID"] as? String
let sessionTimeout = config["SessionTimeoutMinutes"] as? Int ?? 30
let enableDebugLogs = config["EnableDebugLogs"] as? Bool ?? false
AppConfig.shared.apply(
backendURL: backendURL,
tenantID: tenantID,
sessionTimeout: sessionTimeout,
debugLogs: enableDebugLogs
)
}
One gotcha: config may not arrive immediately on first launch, but after MDM checkin. App shouldn't block waiting—apply defaults, then update later.
Reacting to Configuration Changes
MDM can update config anytime—e.g., change backend URL during infrastructure migration. React without restart:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(managedConfigChanged),
name: UserDefaults.didChangeNotification,
object: nil
)
}
@objc private func managedConfigChanged() {
guard let newConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") else { return }
let newBackendURL = newConfig["BackendURL"] as? String
if newBackendURL != AppConfig.shared.backendURL {
// Reinitialize network layer with new URL
NetworkManager.shared.reconfigure(baseURL: newBackendURL)
}
}
UserDefaults.didChangeNotification fires on any UserDefaults change—filter relevant key to avoid reloading on every minor change.
Configuration Dictionary Structure
Recommended—JSON Schema documenting config keys. AppConfig Manager on app side:
struct ManagedConfig: Decodable {
let backendURL: String
let tenantID: String?
let sessionTimeoutMinutes: Int
let allowBiometricAuth: Bool
let supportedLanguages: [String]
let featureFlags: [String: Bool]?
enum CodingKeys: String, CodingKey {
case backendURL = "BackendURL"
case tenantID = "TenantID"
case sessionTimeoutMinutes = "SessionTimeoutMinutes"
case allowBiometricAuth = "AllowBiometricAuth"
case supportedLanguages = "SupportedLanguages"
case featureFlags = "FeatureFlags"
}
}
func decodeManagedConfig() -> ManagedConfig? {
guard let dict = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed"),
let data = try? JSONSerialization.data(withJSONObject: dict),
let config = try? JSONDecoder().decode(ManagedConfig.self, from: data) else {
return nil
}
return config
}
Typed struct better than manual type casting from [AnyHashable: Any]—config errors caught earlier.
AppConfig in MDM Consoles
In Jamf Pro configure in Apps → App Configuration → XML Profile:
<dict>
<key>BackendURL</key>
<string>https://api.corp.example.com/v2</string>
<key>TenantID</key>
<string>CORP-EU-001</string>
<key>SessionTimeoutMinutes</key>
<integer>20</integer>
<key>AllowBiometricAuth</key>
<true/>
<key>SupportedLanguages</key>
<array>
<string>en</string>
<string>de</string>
</array>
</dict>
In Intune via Apps → App Configuration Policies → Managed Devices → iOS/iPadOS → General. Values entered as key-value or XML. Intune sends via Apple MDM protocol—same com.apple.configuration.managed key.
Testing Without MDM Server
For development, simulate config via UserDefaults.standard.set() in launch args or separate debug screen:
#if DEBUG
func injectTestManagedConfig() {
let testConfig: [String: Any] = [
"BackendURL": "https://staging-api.corp.example.com",
"TenantID": "TEST-001",
"SessionTimeoutMinutes": 5,
"AllowBiometricAuth": true
]
UserDefaults.standard.set(testConfig, forKey: "com.apple.configuration.managed")
}
#endif
Also test in Simulator with Managed Preferences via defaults write in terminal—emulates real MDM delivery without actual MDM server.
Feedback Channel: Status Report to MDM
MDM can not only send config, but read app state via com.apple.configuration.managed.feedback. App writes status:
UserDefaults.standard.set([
"LastSyncTime": ISO8601DateFormatter().string(from: Date()),
"ConfigVersion": "2.1",
"EnrollmentStatus": "active"
], forKey: "com.apple.feedback.managed")
MDM server (Jamf, Intune) reads key on checkin, displays in device inventory. Convenient for diagnostics without user contact.
Implementation Stages
Define config parameters → develop dictionary schema → implement reading + reaction → test without MDM → publish documentation for IT → configure profiles in MDM console → pilot → rollout.
Timeline: implement Managed App Configuration in ready app—1–2 weeks. Cost is calculated individually.







