Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/publish_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ concurrency:
jobs:
build_docs:
name: Build and Archive Docs
runs-on: macos-12
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v7

- name: Generate docs
run: |
Expand All @@ -34,7 +34,7 @@ jobs:
--output-path github-pages

- name: Upload docs archive
uses: actions/upload-pages-artifact@main
uses: actions/upload-pages-artifact@v5
with:
path: github-pages

Expand All @@ -54,4 +54,4 @@ jobs:
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v5
10 changes: 7 additions & 3 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ jobs:
name: Run unit tests

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: Unit Test (macOS)
run: swift test -v

- name: Unit Test (iOS)
run: xcodebuild test -scheme ManagedAppConfigLib -sdk iphonesimulator16.2 -destination "OS=16.2,name=iPhone 13"
run: |
UDID=$(xcrun simctl list devices available | grep 'iPhone' | head -1 | grep -Eo '[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}')
xcodebuild test -scheme ManagedAppConfigLib -destination "id=$UDID"

- name: Unit Test (tvOS)
run: xcodebuild test -scheme ManagedAppConfigLib -sdk appletvsimulator16.1 -destination "OS=16.1,name=Apple TV"
run: |
UDID=$(xcrun simctl list devices available | grep 'Apple TV' | head -1 | grep -Eo '[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}')
xcodebuild test -scheme ManagedAppConfigLib -destination "id=$UDID"
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-07-22
### Changed
- Raised the Swift Package Manager tools version to 5.9 and set the CocoaPods `swift_versions` to 5.9.
- Raised the minimum deployment targets to macOS 13, iOS 17, and tvOS 17 across `Package.swift`, the podspec, the `@available` annotations, and the README. **This is a breaking change for consumers on older OS versions.**
- Refreshed GitHub Actions workflows to run on current macOS runners and simulators, and bumped the docs-publishing action versions.

### Added
- Added unit tests covering the `ManagedAppConfig` class (config/feedback getters, `updateValue`, change hooks, and the public key constants).

## [1.1.2] - 2023-08-11
### Fixed
- Updated podspec file to target iOS 9 and higher to work around a problem with CocoaPods and Xcode 14.3+ ([Issue #8](https://github.com/jamf/ManagedAppConfigLib/issues/8)) [@macblazer](https://github.com/macblazer).
Expand Down
10 changes: 5 additions & 5 deletions ManagedAppConfigLib.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "ManagedAppConfigLib"
s.version = "1.1.2"
s.version = "2.0.0"
s.summary = "Simplify working with Managed App Configuration and Feedback."

s.description = <<-DESC
Expand All @@ -11,10 +11,10 @@ Pod::Spec.new do |s|
s.license = { :type => "MIT", :file => "LICENSE" }
s.author = "Kyle Hammond", "James Felton"

s.swift_versions = "5.1"
s.platform = :ios, "9.0" # Actually supports iOS 8, but CocoaPods issue #11839
s.tvos.deployment_target = "10.2"
s.macos.deployment_target = "11"
s.swift_versions = "5.9"
s.platform = :ios, "17.0"
s.tvos.deployment_target = "17.0"
s.macos.deployment_target = "13.0"

s.source = { :git => "https://github.com/jamf/ManagedAppConfigLib.git", :tag => "#{s.version}" }

Expand Down
10 changes: 5 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// swift-tools-version:5.6
// swift-tools-version:5.9
import PackageDescription

let package = Package(
name: "ManagedAppConfigLib",
platforms: [
.macOS(.v11),
.iOS(.v8),
.tvOS(.v10)
.macOS(.v13),
.iOS(.v17),
.tvOS(.v17)
],
products: [
.library(name: "ManagedAppConfigLib", targets: ["ManagedAppConfigLib"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.5.0")
],
targets: [
.target(name: "ManagedAppConfigLib"),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The purpose of ManagedAppConfigLib is to make it much easier to work with Apple'
[Managed App Configuration](https://developer.apple.com/library/content/samplecode/sc2279/Introduction/Intro.html)
by providing a class that manages access to it, as well as some property wrappers for modern Swift usage.

Managed App Configuration is supported by Apple on iOS 7+, macOS 11+, and tvOS 10.2+.
ManagedAppConfigLib requires iOS 17+, macOS 13+, and tvOS 17+.

## Installation

Expand Down
2 changes: 1 addition & 1 deletion Sources/ManagedAppConfigLib/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SwiftUI
/// declaration.
///
/// When the Managed App Config value changes, SwiftUI updates the parts of any view that depend on those properties.
@available(macOS 11, iOS 14.0, tvOS 14.0, *)
@available(macOS 13, iOS 17.0, tvOS 17.0, *)
@propertyWrapper public struct AppConfig<Value>: DynamicProperty {
// Very simple listener that observes AppConfig changes, and has a local copy of the AppConfig's value.
private final class Listener: ObservableObject {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ManagedAppConfigLib/AppConfigPlain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
/// If used in a SwiftUI view, does **not** trigger a SwiftUI view redraw when the
/// Managed App Configuration value changes. See ``AppConfig`` if you want a SwiftUI view to redraw on update
/// of a Managed App Configuration value.
@available(macOS 11, iOS 7.0, tvOS 10.2, *)
@available(macOS 13, iOS 17.0, tvOS 17.0, *)
@propertyWrapper public struct AppConfigPlain<Value> {
// Very simple listener that observes AppConfig changes, and updates it's internal copy of the value as needed.
private final class Listener {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ManagedAppConfigLib/AppConfigService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension Notification.Name {

/// An internal service class that keeps track of all used `UserDefaults` objects and listens for changes
/// to Managed App Configuration in them.
@available(macOS 11, iOS 7.0, tvOS 10.2, *)
@available(macOS 13, iOS 17.0, tvOS 17.0, *)
class AppConfigService {
static let shared = AppConfigService()

Expand Down
2 changes: 1 addition & 1 deletion Sources/ManagedAppConfigLib/ManagedAppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
///
/// The `ManagedAppConfig` class provides a programmatic interface for interacting with Managed App
/// Configuration and Managed App Feedback values.
@available(macOS 11.0, iOS 7.0, tvOS 10.2, *)
@available(macOS 13.0, iOS 17.0, tvOS 17.0, *)
public class ManagedAppConfig {
/// The `UserDefaults` key for Managed App Configuration.
public static let configurationKey = "com.apple.configuration.managed"
Expand Down
128 changes: 128 additions & 0 deletions Tests/ManagedAppConfigLibTests/ManagedAppConfigTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
SPDX-License-Identifier: MIT
Copyright (c) 2017-2026 Jamf
*/

@testable import ManagedAppConfigLib
import XCTest

final class ManagedAppConfigTests: XCTestCase {
// `ManagedAppConfig` reads and writes `UserDefaults.standard` directly, so each test seeds and
// then clears the two well-known keys to avoid leaking state between tests.
private let configurationKey = ManagedAppConfig.configurationKey
private let feedbackKey = ManagedAppConfig.feedbackKey

override func tearDown() {
UserDefaults.standard.removeObject(forKey: configurationKey)
UserDefaults.standard.removeObject(forKey: feedbackKey)
super.tearDown()
}

// MARK: - Key constants

func testPublicKeyConstants() {
XCTAssertEqual(ManagedAppConfig.configurationKey, "com.apple.configuration.managed")
XCTAssertEqual(ManagedAppConfig.feedbackKey, "com.apple.feedback.managed")
}

func testSharedIsSingleton() {
XCTAssertTrue(ManagedAppConfig.shared === ManagedAppConfig.shared)
}

// MARK: - Configuration getter

func testGetConfigValueReturnsValueFromStandardDefaults() {
UserDefaults.standard.set(["deviceId": "abc123"], forKey: configurationKey)

let sut = ManagedAppConfig()

XCTAssertEqual(sut.getConfigValue(forKey: "deviceId") as? String, "abc123")
}

func testGetConfigValueReturnsNilForMissingKey() {
UserDefaults.standard.set(["deviceId": "abc123"], forKey: configurationKey)

let sut = ManagedAppConfig()

XCTAssertNil(sut.getConfigValue(forKey: "notThere"))
}

func testGetConfigValueReturnsNilWhenNoDictionary() {
let sut = ManagedAppConfig()

XCTAssertNil(sut.getConfigValue(forKey: "anything"))
}

// MARK: - Feedback getter / setter round-trip

func testUpdateValueCreatesFeedbackDictionaryWhenNoneExists() {
// Hits the branch where no feedback dictionary exists yet and a fresh one is created.
let sut = ManagedAppConfig()

sut.updateValue(42, forKey: "errorCount")

XCTAssertEqual(sut.getFeedbackValue(forKey: "errorCount") as? Int, 42)
}

func testUpdateValueMergesIntoExistingFeedbackDictionary() {
// Hits the branch where a feedback dictionary already exists and is updated in place.
let sut = ManagedAppConfig()

sut.updateValue(42, forKey: "errorCount")
sut.updateValue("running", forKey: "state")

XCTAssertEqual(sut.getFeedbackValue(forKey: "errorCount") as? Int, 42)
XCTAssertEqual(sut.getFeedbackValue(forKey: "state") as? String, "running")
}

func testUpdateValueOverwritesExistingValueForKey() {
let sut = ManagedAppConfig()

sut.updateValue(1, forKey: "errorCount")
sut.updateValue(2, forKey: "errorCount")

XCTAssertEqual(sut.getFeedbackValue(forKey: "errorCount") as? Int, 2)
}

func testGetFeedbackValueReturnsNilWhenNoDictionary() {
let sut = ManagedAppConfig()

XCTAssertNil(sut.getFeedbackValue(forKey: "errorCount"))
}

// MARK: - Change hooks

func testConfigChangedHookFiresWithConfigurationDictionary() {
let sut = ManagedAppConfig()
let expectation = expectation(description: "config hook called")
expectation.assertForOverFulfill = false

sut.addAppConfigChangedHook { config in
if config["title"] as? String == "hello" {
expectation.fulfill()
}
}

// Writing to standard defaults posts `UserDefaults.didChangeNotification`, which the
// observer set up in `ManagedAppConfig.init()` turns into a hook callback.
UserDefaults.standard.set(["title": "hello"], forKey: configurationKey)

wait(for: [expectation], timeout: 2.0)
}

func testFeedbackChangedHookFiresWithFeedbackDictionary() {
let sut = ManagedAppConfig()
let expectation = expectation(description: "feedback hook called")
expectation.assertForOverFulfill = false

sut.addAppFeedbackChangedHook { feedback in
if feedback["errorCount"] as? Int == 7 {
expectation.fulfill()
}
}

sut.updateValue(7, forKey: "errorCount")

wait(for: [expectation], timeout: 2.0)
}
}
Loading