diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml index 91623b7..11232c1 100644 --- a/.github/workflows/publish_docs.yml +++ b/.github/workflows/publish_docs.yml @@ -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: | @@ -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 @@ -54,4 +54,4 @@ jobs: steps: - name: Deploy id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 7333c8c..66b0a54 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index ef60e43..672e216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/ManagedAppConfigLib.podspec b/ManagedAppConfigLib.podspec index 6d2ec6d..1451fcc 100644 --- a/ManagedAppConfigLib.podspec +++ b/ManagedAppConfigLib.podspec @@ -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 @@ -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}" } diff --git a/Package.swift b/Package.swift index 2652b5e..0ebd59b 100644 --- a/Package.swift +++ b/Package.swift @@ -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"), diff --git a/README.md b/README.md index a105f31..fb8deaf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Sources/ManagedAppConfigLib/AppConfig.swift b/Sources/ManagedAppConfigLib/AppConfig.swift index 4ffe9fc..5890244 100644 --- a/Sources/ManagedAppConfigLib/AppConfig.swift +++ b/Sources/ManagedAppConfigLib/AppConfig.swift @@ -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: DynamicProperty { // Very simple listener that observes AppConfig changes, and has a local copy of the AppConfig's value. private final class Listener: ObservableObject { diff --git a/Sources/ManagedAppConfigLib/AppConfigPlain.swift b/Sources/ManagedAppConfigLib/AppConfigPlain.swift index 373cca1..1a111d3 100644 --- a/Sources/ManagedAppConfigLib/AppConfigPlain.swift +++ b/Sources/ManagedAppConfigLib/AppConfigPlain.swift @@ -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 { // Very simple listener that observes AppConfig changes, and updates it's internal copy of the value as needed. private final class Listener { diff --git a/Sources/ManagedAppConfigLib/AppConfigService.swift b/Sources/ManagedAppConfigLib/AppConfigService.swift index 0826a67..e243e79 100644 --- a/Sources/ManagedAppConfigLib/AppConfigService.swift +++ b/Sources/ManagedAppConfigLib/AppConfigService.swift @@ -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() diff --git a/Sources/ManagedAppConfigLib/ManagedAppConfig.swift b/Sources/ManagedAppConfigLib/ManagedAppConfig.swift index 2109713..efb5b44 100644 --- a/Sources/ManagedAppConfigLib/ManagedAppConfig.swift +++ b/Sources/ManagedAppConfigLib/ManagedAppConfig.swift @@ -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" diff --git a/Tests/ManagedAppConfigLibTests/ManagedAppConfigTests.swift b/Tests/ManagedAppConfigLibTests/ManagedAppConfigTests.swift new file mode 100644 index 0000000..deccdf8 --- /dev/null +++ b/Tests/ManagedAppConfigLibTests/ManagedAppConfigTests.swift @@ -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) + } +}