Skip to content

Commit 1ad2eaf

Browse files
committed
Defaults witout MainActor
1 parent cc532c8 commit 1ad2eaf

6 files changed

Lines changed: 39 additions & 45 deletions

File tree

Source/Defaults.swift

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import Combine
22
import Foundation
33

4-
#if swift(>=6.0)
5-
@MainActor
6-
#endif
74
/// A property wrapper that stores and observes values in `UserDefaults` with support for encoding and decoding.
85
///
96
/// `Defaults` synchronizes with `UserDefaults`, enabling automatic storage, retrieval, and observation
@@ -93,16 +90,7 @@ public final class Defaults<Value: Codable & Equatable> {
9390
self.decoderGenerator = decoder ?? { .init() }
9491

9592
self.defaultsObserver = .init(key: key, userDefaults: userDefaults)
96-
defaultsObserver.updateHandler = { [weak self] _ in
97-
self?.syncMain()
98-
}
99-
100-
// sometimes KVO is not working
101-
self.notificationToken = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification,
102-
object: userDefaults,
103-
queue: .main) { [weak self] _ in
104-
self?.syncMain()
105-
}
93+
subscribe()
10694
}
10795

10896
deinit {
@@ -111,6 +99,24 @@ public final class Defaults<Value: Codable & Equatable> {
11199
}
112100
}
113101

102+
private func subscribe() {
103+
// swiftformat:disable:next all
104+
let unsafeSync = UnsafeSendable({ [weak self] in
105+
self?.syncMain()
106+
})
107+
108+
defaultsObserver.updateHandler = { [unsafeSync] _ in
109+
unsafeSync.value()
110+
}
111+
112+
// sometimes KVO is not working
113+
notificationToken = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification,
114+
object: userDefaults,
115+
queue: .main) { [unsafeSync] _ in
116+
unsafeSync.value()
117+
}
118+
}
119+
114120
private nonisolated func syncMain() {
115121
assert(Thread.isMainThread, "Should be used only in main thread")
116122

@@ -197,10 +203,6 @@ public extension Defaults where Value: ExpressibleByDictionaryLiteral, Value.Key
197203
}
198204
}
199205

200-
#if swift(>=6.0)
201-
extension Defaults: @unchecked Sendable {}
202-
#endif
203-
204206
private final class DefaultsObserver: NSObject {
205207
private let userDefaults: UserDefaults
206208
private let key: String
@@ -223,7 +225,7 @@ private final class DefaultsObserver: NSObject {
223225
userDefaults.removeObserver(self, forKeyPath: key, context: nil)
224226
}
225227

226-
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
228+
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
227229
guard let change, keyPath == key else {
228230
return
229231
}
@@ -239,20 +241,18 @@ private final class DefaultsObserver: NSObject {
239241
}
240242
}
241243

242-
#if swift(>=6.0)
243-
extension DefaultsObserver: @unchecked Sendable {}
244-
245-
private struct UnsafeSendable<T>: @unchecked Sendable {
244+
private struct UnsafeSendable<T> {
246245
let value: T
247246
}
248-
#else
249-
struct UnsafeSendable<T> {
250-
let value: T
251-
}
252-
#endif
253247

254248
extension UnsafeSendable {
255249
init(_ value: T) {
256250
self.value = value
257251
}
258252
}
253+
254+
#if swift(>=6.0)
255+
extension Defaults: @unchecked Sendable {}
256+
extension DefaultsObserver: @unchecked Sendable {}
257+
extension UnsafeSendable: @unchecked Sendable {}
258+
#endif

Source/Storage.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ public extension Storage {
124124
/// - Returns: A new `Storage` that reflects the combined state.
125125
@available(macOS 13, iOS 16, tvOS 16.0, watchOS 9.0, *)
126126
@inline(__always)
127-
public func zip<Value>(storages: [any Storage<Value>]) -> some Storage<Value>
128-
where Value: ExpressibleByNilLiteral & Equatable {
127+
public func zip<Value: ExpressibleByNilLiteral & Equatable>(storages: [any Storage<Value>]) -> some Storage<Value> {
129128
return StorageComposition(storages: storages).toAny()
130129
}
131130

@@ -137,7 +136,6 @@ where Value: ExpressibleByNilLiteral & Equatable {
137136
@available(macOS, deprecated: 13)
138137
@available(iOS, deprecated: 16)
139138
@inline(__always)
140-
public func zip<Value>(storages: [AnyStorage<Value>]) -> some Storage<Value>
141-
where Value: ExpressibleByNilLiteral & Equatable {
139+
public func zip<Value: ExpressibleByNilLiteral & Equatable>(storages: [AnyStorage<Value>]) -> some Storage<Value> {
142140
return StorageComposition(storages: storages).toAny()
143141
}

Source/StorageComposition.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal final class StorageComposition<Value: ExpressibleByNilLiteral & Equatab
1313
/// A publisher that emits the current value and any future changes across the combined storages.
1414
///
1515
/// Use this publisher to observe updates from any of the underlying storages.
16-
public private(set) lazy var eventier: ValuePublisher<Value> = subject.eraseToAnyPublisher()
16+
private(set) lazy var eventier: ValuePublisher<Value> = subject.eraseToAnyPublisher()
1717

1818
private let storages: [AnyStorage<Value>]
1919
private var observers: [AnyCancellable] = []

Source/Storages/FileStorage.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import Foundation
77
/// through Combine publishers.
88
///
99
/// The file is stored in the app's Caches directory under a "Storages" folder.
10-
public final class FileStorage<Value>: Storage
11-
where Value: ExpressibleByNilLiteral & Codable {
10+
public final class FileStorage<Value: ExpressibleByNilLiteral & Codable>: Storage {
1211
private lazy var subject: ValueSubject<Value> = .init(get())
1312
/// A publisher that emits the current value and all subsequent updates.
1413
///

Source/Storages/UserDefaultsStorage.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import Foundation
88
/// Use this class to manage lightweight settings, preferences, or any other persistable data.
99
///
1010
/// - Note: The `Value` must conform to `Codable` and `ExpressibleByNilLiteral`.
11-
public final class UserDefaultsStorage<Value>: Storage
12-
where Value: ExpressibleByNilLiteral & Codable {
11+
public final class UserDefaultsStorage<Value: ExpressibleByNilLiteral & Codable>: Storage {
1312
private lazy var subject: ValueSubject<Value> = .init(get())
1413
/// A publisher that emits the current value and all subsequent changes.
1514
///

Tests/DefaultsTests.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Foundation
33
import StorageKit
44
import XCTest
55

6-
@MainActor
76
final class DefaultsTests: XCTestCase {
87
fileprivate struct Custom: Codable, Equatable {
98
let value: Int
@@ -89,14 +88,13 @@ final class DefaultsTests: XCTestCase {
8988
}
9089

9190
extension DefaultsTests {
92-
private func run_test_value_type<T>(_ type: T.Type,
93-
defaultValue: T,
94-
values: [T],
95-
isPropertyListType: Bool = true, // UserDefaults error: Attempt to insert non-property list object
96-
userDefaults: UserDefaults?,
97-
file: StaticString = #filePath,
98-
line: UInt = #line)
99-
where T: Codable & Equatable & SafeSendable {
91+
private func run_test_value_type<T: Codable & Equatable & SafeSendable>(_: T.Type,
92+
defaultValue: T,
93+
values: [T],
94+
isPropertyListType: Bool = true, // UserDefaults error: Attempt to insert non-property list object
95+
userDefaults: UserDefaults?,
96+
file: StaticString = #filePath,
97+
line: UInt = #line) {
10098
let key = String(describing: T.self)
10199
let userDefaults: UserDefaults = userDefaults ?? .init(suiteName: "DefaultsTests_\(key)")!
102100
// clean user defaults before use

0 commit comments

Comments
 (0)