From 176aa63666012cf127909ad77267a32c21aa928e Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 22 Oct 2022 00:04:15 +0700 Subject: [PATCH] Minor tweaks --- .swiftlint.yml | 8 ++- .../Defaults/Defaults+AnySerializable.swift | 2 +- Sources/Defaults/Defaults.swift | 2 +- Sources/Defaults/SwiftUI.swift | 63 ++++++++++++++++--- Sources/Defaults/UserDefaults.swift | 2 +- Sources/Defaults/Utilities.swift | 20 +----- .../DefaultsCodableEnumTests.swift | 2 +- .../DefaultsNSSecureCodingTests.swift | 14 ++++- Tests/DefaultsTests/DefaultsTests.swift | 14 ++++- 9 files changed, 90 insertions(+), 37 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 2ac73b5..5f9554e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,4 @@ only_rules: - - anyobject_protocol - array_init - block_based_kvo - class_delegate_protocol @@ -10,6 +9,7 @@ only_rules: - collection_alignment - colon - comma + - comma_inheritance - compiler_protocol_init - computed_accessors_order - conditional_returns_on_newline @@ -105,6 +105,7 @@ only_rules: - required_enum_case - return_arrow_whitespace - return_value_from_void_function + - self_binding - self_in_property_initialization - shorthand_operator - sorted_first_last @@ -136,9 +137,9 @@ only_rules: - unused_setter_value - valid_ibinspectable - vertical_parameter_alignment - - vertical_parameter_alignment_on_call - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces + - void_function_in_ternary - void_return - xct_specific_matcher - yoda_condition @@ -192,3 +193,6 @@ custom_rules: final_class: regex: '^class [a-zA-Z\d]+[^{]+\{' message: 'Classes should be marked as final whenever possible. If you actually need it to be subclassable, just add `// swiftlint:disable:next final_class`.' + no_alignment_center: + regex: '\b\(alignment: .center\b' + message: 'This alignment is the default.' diff --git a/Sources/Defaults/Defaults+AnySerializable.swift b/Sources/Defaults/Defaults+AnySerializable.swift index a791f97..55b35de 100644 --- a/Sources/Defaults/Defaults+AnySerializable.swift +++ b/Sources/Defaults/Defaults+AnySerializable.swift @@ -191,7 +191,7 @@ extension Defaults.AnySerializable: ExpressibleByDictionaryLiteral { } } -extension Defaults.AnySerializable: _DefaultsOptionalType { +extension Defaults.AnySerializable: _DefaultsOptionalProtocol { // Since `nil` cannot be assigned to `Any`, we use `Void` instead of `nil`. public var isNil: Bool { value is Void } } diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index f99bb68..c87b56f 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -53,7 +53,7 @@ public enum Defaults { super.init(name: key, suite: suite) - if (defaultValue as? _DefaultsOptionalType)?.isNil == true { + if (defaultValue as? _DefaultsOptionalProtocol)?.isNil == true { return } diff --git a/Sources/Defaults/SwiftUI.swift b/Sources/Defaults/SwiftUI.swift index 93edc54..a85f2a3 100644 --- a/Sources/Defaults/SwiftUI.swift +++ b/Sources/Defaults/SwiftUI.swift @@ -4,8 +4,10 @@ import Combine @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Defaults { + @MainActor final class Observable: ObservableObject { private var cancellable: AnyCancellable? + private var task: Task? private let key: Defaults.Key let objectWillChange = ObservableObjectPublisher() @@ -21,16 +23,34 @@ extension Defaults { init(_ key: Key) { self.key = key - self.cancellable = Defaults.publisher(key, options: [.prior]) - .sink { [weak self] change in - guard change.isPrior else { - return - } + // We only use this on the latest OSes (as of adding this) since the backdeploy library has a lot of bugs. + if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { + // The `@MainActor` is important as the `.send()` method doesn't inherit the `@MainActor` from the class. + self.task = .detached(priority: .userInitiated) { @MainActor [weak self] in + for await _ in Defaults.events(key) { + guard let self else { + return + } - DispatchQueue.mainSafeAsync { - self?.objectWillChange.send() + self.objectWillChange.send() } } + } else { + self.cancellable = Defaults.publisher(key, options: [.prior]) + .sink { [weak self] change in + guard change.isPrior else { + return + } + + Task { @MainActor in + self?.objectWillChange.send() + } + } + } + } + + deinit { + task?.cancel() } /** @@ -77,7 +97,7 @@ public struct Default: DynamicProperty { */ public init(_ key: Defaults.Key) { self.key = key - self.observable = Defaults.Observable(key) + self.observable = .init(key) } public var wrappedValue: Value { @@ -178,7 +198,7 @@ extension Defaults { public init(key: Defaults.Key, @ViewBuilder label: @escaping () -> Label) { self.label = label - self.observable = Defaults.Observable(key) + self.observable = .init(key) } public var body: some View { @@ -194,7 +214,7 @@ extension Defaults { extension Defaults.Toggle { public init(_ title: some StringProtocol, key: Defaults.Key) { self.label = { Text(title) } - self.observable = Defaults.Observable(key) + self.observable = .init(key) } } @@ -209,6 +229,29 @@ extension Defaults.Toggle { } } +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Defaults { + // TODO: Expose this publicly at some point. + private static func events( + _ key: Defaults.Key, + initial: Bool = true + ) -> AsyncStream { // TODO: Make this `some AsyncSequence` when Swift 6 is out. + .init { continuation in + let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in + // TODO: Use the `.deserialize` method directly. + let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue + continuation.yield(value) + } + + observation.start(options: initial ? [.initial] : []) + + continuation.onTermination = { _ in + observation.invalidate() + } + } + } +} + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @propertyWrapper private struct ViewStorage: DynamicProperty { diff --git a/Sources/Defaults/UserDefaults.swift b/Sources/Defaults/UserDefaults.swift index 4a5db25..703d0bc 100644 --- a/Sources/Defaults/UserDefaults.swift +++ b/Sources/Defaults/UserDefaults.swift @@ -10,7 +10,7 @@ extension UserDefaults { } func _set(_ key: String, to value: Value) { - if (value as? _DefaultsOptionalType)?.isNil == true { + if (value as? _DefaultsOptionalProtocol)?.isNil == true { removeObject(forKey: key) return } diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index 5c86555..452378e 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -20,7 +20,7 @@ extension Decodable { } -final class ObjectAssociation { +final class ObjectAssociation { subscript(index: AnyObject) -> T? { get { objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? @@ -124,32 +124,18 @@ A protocol for making generic type constraints of optionals. - Note: It's intentionally not including `associatedtype Wrapped` as that limits a lot of the use-cases. */ -public protocol _DefaultsOptionalType: ExpressibleByNilLiteral { +public protocol _DefaultsOptionalProtocol: ExpressibleByNilLiteral { /** This is useful as you cannot compare `_OptionalType` to `nil`. */ var isNil: Bool { get } } -extension Optional: _DefaultsOptionalType { +extension Optional: _DefaultsOptionalProtocol { public var isNil: Bool { self == nil } } -extension DispatchQueue { - /** - Performs the `execute` closure immediately if we're on the main thread or asynchronously puts it on the main thread otherwise. - */ - static func mainSafeAsync(execute work: @escaping () -> Void) { - if Thread.isMainThread { - work() - } else { - main.async(execute: work) - } - } -} - - extension Sequence { /** Returns an array containing the non-nil elements. diff --git a/Tests/DefaultsTests/DefaultsCodableEnumTests.swift b/Tests/DefaultsTests/DefaultsCodableEnumTests.swift index 70f07d7..94ea8f5 100644 --- a/Tests/DefaultsTests/DefaultsCodableEnumTests.swift +++ b/Tests/DefaultsTests/DefaultsCodableEnumTests.swift @@ -2,7 +2,7 @@ import Foundation import Defaults import XCTest -private enum FixtureCodableEnum: String, Defaults.Serializable & Codable & Hashable { +private enum FixtureCodableEnum: String, Hashable, Codable, Defaults.Serializable { case tenMinutes = "10 Minutes" case halfHour = "30 Minutes" case oneHour = "1 Hour" diff --git a/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift b/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift index 7d84926..bba19e1 100644 --- a/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift +++ b/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift @@ -349,7 +349,12 @@ final class DefaultsNSSecureCodingTests: XCTestCase { .collect(expectedArray.count) .sink { result in print("Result array: \(result)") - result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + + if result == expectedArray { + expect.fulfill() + } else { + XCTFail("Expected Array is not matched") + } } inputArray.forEach { @@ -378,7 +383,12 @@ final class DefaultsNSSecureCodingTests: XCTestCase { .collect(expectedArray.count) .sink { result in print("Result array: \(result)") - result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + + if result == expectedArray { + expect.fulfill() + } else { + XCTFail("Expected Array is not matched") + } } inputArray.forEach { diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index c7034a7..57ffe6e 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -499,7 +499,12 @@ final class DefaultsTests: XCTestCase { .collect(expectedArray.count) .sink { result in print("Result array: \(result)") - result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + + if result == expectedArray { + expect.fulfill() + } else { + XCTFail("Expected Array is not matched") + } } inputArray.forEach { @@ -527,7 +532,12 @@ final class DefaultsTests: XCTestCase { .collect(expectedArray.count) .sink { result in print("Result array: \(result)") - result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + + if result == expectedArray { + expect.fulfill() + } else { + XCTFail("Expected Array is not matched") + } } inputArray.forEach {