diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5b7a9a..04029a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,8 @@ jobs: DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' strategy: matrix: - xcode: [ 15.4, 16.4 ] + xcode: [ 16.4 ] include: - - xcode: 15.4 - macos: macos-14 - xcode: 16.4 macos: macos-15 steps: @@ -37,11 +35,8 @@ jobs: DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' strategy: matrix: - xcode: [ 15.4, 16.4 ] + xcode: [ 16.4 ] include: - - xcode: 15.4 - macos: macos-14 - destination: "platform=iOS Simulator,name=iPhone 15,OS=17.5" - xcode: 16.4 macos: macos-15 destination: "platform=iOS Simulator,name=iPhone 16,OS=18.5" @@ -56,11 +51,8 @@ jobs: runs-on: ${{ matrix.macos }} strategy: matrix: - xcode: [ 15.4, 16.4 ] + xcode: [ 16.4 ] include: - - xcode: 15.4 - macos: macos-14 - destination: "platform=tvOS Simulator,name=Apple TV,OS=17.5" - xcode: 16.4 macos: macos-15 destination: "platform=tvOS Simulator,name=Apple TV,OS=18.5" @@ -77,11 +69,8 @@ jobs: runs-on: ${{ matrix.macos }} strategy: matrix: - xcode: [ 15.4, 16.4 ] + xcode: [ 16.4 ] include: - - xcode: 15.4 - macos: macos-14 - destination: "platform=watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=10.5" - xcode: 16.4 macos: macos-15 destination: "platform=watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.5" @@ -107,6 +96,8 @@ jobs: cocoapods: name: CocoaPods runs-on: macos-15 + env: + DEVELOPER_DIR: '/Applications/Xcode_16.4.app/Contents/Developer' steps: - name: Checkout Repo uses: actions/checkout@v4 diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 8d3e01b..5625016 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -285,7 +285,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -340,7 +340,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -359,7 +359,7 @@ DEVELOPMENT_ASSET_PATHS = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -380,7 +380,7 @@ DEVELOPMENT_ASSET_PATHS = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Example/ExampleKit/Package.swift b/Example/ExampleKit/Package.swift index 67a97b2..fbddb51 100644 --- a/Example/ExampleKit/Package.swift +++ b/Example/ExampleKit/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "ExampleKit", platforms: [ - .iOS(.v14) + .iOS(.v15) ], products: [ .library(name: "ExampleKit", targets: ["ExampleKit"]) diff --git a/Package.swift b/Package.swift index 00ffeec..c484ffc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "swift-user-defaults", platforms: [ - .macOS(.v10_13), - .iOS(.v12), + .macOS(.v15), + .iOS(.v15), .watchOS(.v7), - .tvOS(.v12) + .tvOS(.v15) ], products: [ .library(name: "SwiftUserDefaults", targets: ["SwiftUserDefaults"]), diff --git a/Sources/SwiftUserDefaults/UserDefaults+Key.swift b/Sources/SwiftUserDefaults/UserDefaults+Key.swift index 02796a4..3515772 100644 --- a/Sources/SwiftUserDefaults/UserDefaults+Key.swift +++ b/Sources/SwiftUserDefaults/UserDefaults+Key.swift @@ -65,7 +65,7 @@ public extension UserDefaults { /// /// let rawValue = UserDefaults.standard.string(forKey: UserDefaults.Key.userState.rawValue) /// ``` - struct Key: RawRepresentable, Hashable { + struct Key: RawRepresentable, Hashable, Sendable { /// The underlying string value that is used for assigning a value against within the user defaults. public let rawValue: String diff --git a/Sources/SwiftUserDefaults/UserDefaults+Observation.swift b/Sources/SwiftUserDefaults/UserDefaults+Observation.swift index 139f75d..df6061e 100644 --- a/Sources/SwiftUserDefaults/UserDefaults+Observation.swift +++ b/Sources/SwiftUserDefaults/UserDefaults+Observation.swift @@ -22,8 +22,6 @@ import Foundation -private var userDefaultsObserverContext = 0 - public extension UserDefaults { /// Observes changes to the object associated with the specified key. /// @@ -59,6 +57,7 @@ public extension UserDefaults { let userDefaults: UserDefaults let keyPath: String let handler: (Change) -> Void + private var userDefaultsObserverContext = 0 private(set) var isRegistered: Bool = false diff --git a/Tests/SwiftUserDefaultsTests/UserDefaultTests.swift b/Tests/SwiftUserDefaultsTests/UserDefaultTests.swift index 36df1c9..6fc95d5 100644 --- a/Tests/SwiftUserDefaultsTests/UserDefaultTests.swift +++ b/Tests/SwiftUserDefaultsTests/UserDefaultTests.swift @@ -85,13 +85,11 @@ final class UserDefaultTests: XCTestCase { XCTAssertNil(userDefaults.object(forKey: "BoolKey")) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) func testObserver() { let wrapper = UserDefault(.init("StringKey"), store: userDefaults, defaultValue: "") var changes: [UserDefaults.Change] = [] let observer = wrapper.addObserver { changes.append($0) } - addTeardownBlock(observer.invalidate) wrapper.wrappedValue = "One" wrapper.reset() @@ -99,9 +97,9 @@ final class UserDefaultTests: XCTestCase { userDefaults.x.set("Three", forKey: .init("StringKey")) XCTAssertEqual(changes, [.initial(""), .update("One"), .update(""), .update("Two"), .update("Three")]) + observer.invalidate() } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) func testCodableWithDefault() { let key = UserDefaults.Key("CodableKey") let wrapper = UserDefault(key, strategy: .json, store: userDefaults, defaultValue: Subject(value: "default")) @@ -109,7 +107,6 @@ final class UserDefaultTests: XCTestCase { // Observe changes var changes: [Subject] = [] let token = wrapper.addObserver(handler: { changes.append($0.value) }) - addTeardownBlock(token.invalidate) // Uses default XCTAssertNil(userDefaults.object(forKey: key.rawValue)) @@ -135,9 +132,9 @@ final class UserDefaultTests: XCTestCase { "default", "default" ]) + token.invalidate() } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) func testCodable() { let key = UserDefaults.Key("CodableKey") let wrapper = UserDefault(key, strategy: .json, store: userDefaults) @@ -145,7 +142,6 @@ final class UserDefaultTests: XCTestCase { // Observe changes var changes: [Subject?] = [] let token = wrapper.addObserver(handler: { changes.append($0.value) }) - addTeardownBlock(token.invalidate) // nil when unset XCTAssertNil(userDefaults.object(forKey: key.rawValue)) @@ -178,9 +174,9 @@ final class UserDefaultTests: XCTestCase { "value", nil ]) + token.invalidate() } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) func testRawRepresentableWithDefault() { let key = UserDefaults.Key("RawRepresentableKey") let wrapper = UserDefault(key, store: userDefaults, defaultValue: .foo) @@ -188,7 +184,6 @@ final class UserDefaultTests: XCTestCase { // Observe changes var changes: [RawSubject] = [] let token = wrapper.addObserver(handler: { changes.append($0.value) }) - addTeardownBlock(token.invalidate) // Uses default XCTAssertNil(userDefaults.object(forKey: key.rawValue)) @@ -214,9 +209,9 @@ final class UserDefaultTests: XCTestCase { .foo, .foo ]) + token.invalidate() } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) func testRawRepresentable() { let key = UserDefaults.Key("RawRepresentableKey") let wrapper = UserDefault(key, store: userDefaults) @@ -224,7 +219,6 @@ final class UserDefaultTests: XCTestCase { // Observe changes var changes: [RawSubject?] = [] let token = wrapper.addObserver(handler: { changes.append($0.value) }) - addTeardownBlock(token.invalidate) // Uses default XCTAssertNil(userDefaults.object(forKey: key.rawValue)) @@ -256,5 +250,6 @@ final class UserDefaultTests: XCTestCase { nil, .baz ]) + token.invalidate() } } diff --git a/Tests/SwiftUserDefaultsTests/UserDefaultsObservationTests.swift b/Tests/SwiftUserDefaultsTests/UserDefaultsObservationTests.swift index dedd3f2..c30610e 100644 --- a/Tests/SwiftUserDefaultsTests/UserDefaultsObservationTests.swift +++ b/Tests/SwiftUserDefaultsTests/UserDefaultsObservationTests.swift @@ -54,6 +54,27 @@ final class UserDefaultsObservationTests: XCTestCase { XCTAssertEqual(changes.map(\.value) as NSArray, [nil, "Test", nil, "Default", 1] as NSArray) XCTAssertEqual(changes.map(\.label), [.initial, .update, .update, .update, .update]) } + + func testInvalidateOnDeinit() { + // Given an observer is registered + var changes: [UserDefaults.Change] = [] + + var observer: UserDefaults.Observation? = userDefaults.observeObject(forKey: "TestKey") { change in + changes.append(change) + } + _ = observer + + userDefaults.set("Test", forKey: "TestKey") + + // When the observer is deallocated + observer = nil + + // Then no further changes should be recorded + userDefaults.set("NewTest", forKey: "TestKey") + + XCTAssertEqual(changes.map(\.value) as NSArray, [nil, "Test"] as NSArray) + XCTAssertEqual(changes.map(\.label), [.initial, .update]) + } } private extension UserDefaults.Change { diff --git a/swift-user-defaults.podspec b/swift-user-defaults.podspec index 064d90b..68a4994 100644 --- a/swift-user-defaults.podspec +++ b/swift-user-defaults.podspec @@ -9,10 +9,10 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/cookpad/swift-user-defaults.git", :tag => "#{s.version}" } s.source_files = "Sources/**/*.{swift}" s.resource_bundles = {'SwiftUserDefaults' => ['Sources/SwiftUserDefaults/PrivacyInfo.xcprivacy']} - s.swift_version = "5.3" + s.swift_version = "6.0" - s.ios.deployment_target = '12.0' - s.osx.deployment_target = '10.13' + s.ios.deployment_target = '15.0' + s.osx.deployment_target = '15.0' # Run Unit Tests s.test_spec 'Tests' do |test_spec|