Skip to content

Commit

Permalink
sometimes KVO is not working, thats why added Notification observer
Browse files Browse the repository at this point in the history
  • Loading branch information
NikSativa committed Oct 14, 2024
1 parent 927e87b commit a6dfb71
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 62 deletions.
158 changes: 98 additions & 60 deletions Source/Defaults.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import Combine
import Foundation

@MainActor
@propertyWrapper
public final class Defaults<Value: Codable> {
public final class Defaults<Value: Codable & Equatable> {
private let userDefaults: UserDefaults

private let key: String
private let defaultValue: Value
private let defaultsObserver: DefaultsObserver

#if swift(>=6.0)
private nonisolated(unsafe) var notificationToken: (any NSObjectProtocol)?
#else
private var notificationToken: (any NSObjectProtocol)?
#endif

private let decoderGenerator: () -> JSONDecoder
private lazy var decoder: JSONDecoder = decoderGenerator()
private let encoderGenerator: () -> JSONEncoder
private lazy var encoder: JSONEncoder = encoderGenerator()

private lazy var eventier: CurrentValueSubject<Value, Never> = .init(wrappedValue)
public lazy var projectedValue: AnyPublisher<Value, Never> = {
return eventier.eraseToAnyPublisher()
public private(set) lazy var projectedValue: AnyPublisher<Value, Never> = {
return eventier.removeDuplicates().eraseToAnyPublisher()
}()

public var wrappedValue: Value {
Expand Down Expand Up @@ -50,8 +57,8 @@ public final class Defaults<Value: Codable> {
}
}

public required init(_ key: String,
defaultValue: Value,
public required init(wrappedValue defaultValue: Value,
key: String,
decoder: (() -> JSONDecoder)? = nil,
encoder: (() -> JSONEncoder)? = nil,
userDefaults: UserDefaults = .standard) {
Expand All @@ -62,67 +69,59 @@ public final class Defaults<Value: Codable> {
self.decoderGenerator = decoder ?? { .init() }

self.defaultsObserver = .init(key: key, userDefaults: userDefaults)
defaultsObserver.updateHandler = { [weak self] new in
guard let self else {
return
}

let newRestored: Value
if let new = new as? Value {
newRestored = new
} else if new is NSNull {
newRestored = self.defaultValue
} else if let new = new as? Data {
do {
newRestored = try self.decoder.decode(Value.self, from: new)
} catch {
newRestored = self.defaultValue
}
} else {
assertionFailure("somehow value in defaults was overridden with wrong type")
newRestored = self.defaultValue
}
defaultsObserver.updateHandler = { [weak self] _ in
self?.syncMain()
}

eventier.send(newRestored)
// sometimes KVO is not working
self.notificationToken = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification,
object: userDefaults,
queue: .main) { [weak self] _ in
self?.syncMain()
}
}
}

private final class DefaultsObserver: NSObject {
private let userDefaults: UserDefaults
private let key: String

#if swift(>=6.0)
var updateHandler: (@Sendable (_ new: Any?) -> Void)?
#else
var updateHandler: ((_ new: Any?) -> Void)?
#endif

required init(key: String,
userDefaults: UserDefaults) {
self.key = key
self.userDefaults = userDefaults
super.init()
userDefaults.addObserver(self, forKeyPath: key, options: [.old, .new], context: nil)
deinit {
if let notificationToken {
NotificationCenter.default.removeObserver(notificationToken)
}
}

deinit {
userDefaults.removeObserver(self, forKeyPath: key, context: nil)
private nonisolated func syncMain() {
assert(Thread.isMainThread, "Should be used only in main thread")

#if swift(>=6.0)
MainActor.assumeIsolated {
notifyAboutChanges()
}
#else
notifyAboutChanges()

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.4 5.10

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.4 5.10

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.4 5.10

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.4 5.10

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.2 5.9

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.2 5.9

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.2 5.9

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_15.2 5.9

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_14.3 5.8

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_14.3 5.8

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context

Check failure on line 98 in Source/Defaults.swift

View workflow job for this annotation

GitHub Actions / macOS Xcode_14.3 5.8

call to main actor-isolated instance method 'notifyAboutChanges()' in a synchronous nonisolated context
#endif
}

override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let change, keyPath == key else {
private func notifyAboutChanges() {
guard let new = userDefaults.object(forKey: key) else {
eventier.send(defaultValue)
return
}

let new = UnsafeSendable(change[.newKey])
if Thread.isMainThread {
updateHandler?(new.value)
} else {
DispatchQueue.main.sync {
updateHandler?(new.value)
let newRestored: Value
if let new = new as? Value {
newRestored = new
} else if new is NSNull {
newRestored = defaultValue
} else if let new = new as? Data {
do {
newRestored = try decoder.decode(Value.self, from: new)
} catch {
newRestored = defaultValue
}
} else {
assertionFailure("somehow value in defaults was overridden with wrong type")
newRestored = defaultValue
}

eventier.send(newRestored)
}
}

Expand All @@ -131,8 +130,8 @@ public extension Defaults where Value: ExpressibleByNilLiteral {
decoder: (() -> JSONDecoder)? = nil,
encoder: (() -> JSONEncoder)? = nil,
userDefaults: UserDefaults = .standard) {
self.init(key,
defaultValue: nil,
self.init(wrappedValue: nil,
key: key,
decoder: decoder,
encoder: encoder,
userDefaults: userDefaults)
Expand All @@ -144,8 +143,8 @@ public extension Defaults where Value: ExpressibleByArrayLiteral, Value.ArrayLit
decoder: (() -> JSONDecoder)? = nil,
encoder: (() -> JSONEncoder)? = nil,
userDefaults: UserDefaults = .standard) {
self.init(key,
defaultValue: [],
self.init(wrappedValue: [],
key: key,
decoder: decoder,
encoder: encoder,
userDefaults: userDefaults)
Expand All @@ -157,8 +156,8 @@ public extension Defaults where Value: ExpressibleByDictionaryLiteral, Value.Key
decoder: (() -> JSONDecoder)? = nil,
encoder: (() -> JSONEncoder)? = nil,
userDefaults: UserDefaults = .standard) {
self.init(key,
defaultValue: [:],
self.init(wrappedValue: [:],
key: key,
decoder: decoder,
encoder: encoder,
userDefaults: userDefaults)
Expand All @@ -167,10 +166,49 @@ public extension Defaults where Value: ExpressibleByDictionaryLiteral, Value.Key

#if swift(>=6.0)
extension Defaults: @unchecked Sendable {}
extension DefaultsObserver: @unchecked Sendable {}
#endif

private final class DefaultsObserver: NSObject {
private let userDefaults: UserDefaults
private let key: String

#if swift(>=6.0)
var updateHandler: (@Sendable (_ new: Any?) -> Void)?
#else
var updateHandler: ((_ new: Any?) -> Void)?
#endif

required init(key: String,
userDefaults: UserDefaults) {
self.key = key
self.userDefaults = userDefaults
super.init()
userDefaults.addObserver(self, forKeyPath: key, options: [.old, .new], context: nil)
}

deinit {
userDefaults.removeObserver(self, forKeyPath: key, context: nil)
}

override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let change, keyPath == key else {
return
}

let new = UnsafeSendable(change[.newKey])
if Thread.isMainThread {
updateHandler?(new.value)
} else {
DispatchQueue.main.sync {
updateHandler?(new.value)
}
}
}
}

#if swift(>=6.0)
extension DefaultsObserver: @unchecked Sendable {}

private struct UnsafeSendable<T>: @unchecked Sendable {
let value: T
}
Expand Down
5 changes: 3 additions & 2 deletions Tests/DefaultsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
import StorageKit
import XCTest

@MainActor
final class DefaultsTests: XCTestCase {
fileprivate struct Custom: Codable, Equatable {
let value: Int
Expand Down Expand Up @@ -105,8 +106,8 @@ extension DefaultsTests {
let encoder = JSONEncoder()
var observers: Set<AnyCancellable> = []

@Defaults(key, defaultValue: defaultValue, userDefaults: userDefaults)
var varValue: T
@Defaults(key: key, userDefaults: userDefaults)
var varValue: T = defaultValue

$varValue.sink { new in
results.append(new)
Expand Down

0 comments on commit a6dfb71

Please sign in to comment.