Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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@v4
Comment thread
JacobHearst marked this conversation as resolved.
Outdated

- 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@v3
Comment thread
JacobHearst marked this conversation as resolved.
Outdated
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@v4
Comment thread
JacobHearst marked this conversation as resolved.
Outdated
6 changes: 3 additions & 3 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ jobs:
name: Run unit tests

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
Comment thread
JacobHearst marked this conversation as resolved.
Outdated
- 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: xcodebuild test -scheme ManagedAppConfigLib -destination "platform=iOS Simulator,name=iPhone 15,OS=latest"

- name: Unit Test (tvOS)
run: xcodebuild test -scheme ManagedAppConfigLib -sdk appletvsimulator16.1 -destination "OS=16.1,name=Apple TV"
run: xcodebuild test -scheme ManagedAppConfigLib -destination "platform=tvOS Simulator,name=Apple TV,OS=latest"
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
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// 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"])
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