Minor tweaks

This commit is contained in:
Sindre Sorhus 2022-10-22 00:04:15 +07:00
parent bc1af5d872
commit 176aa63666
9 changed files with 90 additions and 37 deletions

View File

@ -1,5 +1,4 @@
only_rules: only_rules:
- anyobject_protocol
- array_init - array_init
- block_based_kvo - block_based_kvo
- class_delegate_protocol - class_delegate_protocol
@ -10,6 +9,7 @@ only_rules:
- collection_alignment - collection_alignment
- colon - colon
- comma - comma
- comma_inheritance
- compiler_protocol_init - compiler_protocol_init
- computed_accessors_order - computed_accessors_order
- conditional_returns_on_newline - conditional_returns_on_newline
@ -105,6 +105,7 @@ only_rules:
- required_enum_case - required_enum_case
- return_arrow_whitespace - return_arrow_whitespace
- return_value_from_void_function - return_value_from_void_function
- self_binding
- self_in_property_initialization - self_in_property_initialization
- shorthand_operator - shorthand_operator
- sorted_first_last - sorted_first_last
@ -136,9 +137,9 @@ only_rules:
- unused_setter_value - unused_setter_value
- valid_ibinspectable - valid_ibinspectable
- vertical_parameter_alignment - vertical_parameter_alignment
- vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces - vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces - vertical_whitespace_opening_braces
- void_function_in_ternary
- void_return - void_return
- xct_specific_matcher - xct_specific_matcher
- yoda_condition - yoda_condition
@ -192,3 +193,6 @@ custom_rules:
final_class: final_class:
regex: '^class [a-zA-Z\d]+[^{]+\{' 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`.' 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.'

View File

@ -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`. // Since `nil` cannot be assigned to `Any`, we use `Void` instead of `nil`.
public var isNil: Bool { value is Void } public var isNil: Bool { value is Void }
} }

View File

@ -53,7 +53,7 @@ public enum Defaults {
super.init(name: key, suite: suite) super.init(name: key, suite: suite)
if (defaultValue as? _DefaultsOptionalType)?.isNil == true { if (defaultValue as? _DefaultsOptionalProtocol)?.isNil == true {
return return
} }

View File

@ -4,8 +4,10 @@ import Combine
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Defaults { extension Defaults {
@MainActor
final class Observable<Value: Serializable>: ObservableObject { final class Observable<Value: Serializable>: ObservableObject {
private var cancellable: AnyCancellable? private var cancellable: AnyCancellable?
private var task: Task<Void, Never>?
private let key: Defaults.Key<Value> private let key: Defaults.Key<Value>
let objectWillChange = ObservableObjectPublisher() let objectWillChange = ObservableObjectPublisher()
@ -21,17 +23,35 @@ extension Defaults {
init(_ key: Key<Value>) { init(_ key: Key<Value>) {
self.key = key self.key = key
// 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
}
self.objectWillChange.send()
}
}
} else {
self.cancellable = Defaults.publisher(key, options: [.prior]) self.cancellable = Defaults.publisher(key, options: [.prior])
.sink { [weak self] change in .sink { [weak self] change in
guard change.isPrior else { guard change.isPrior else {
return return
} }
DispatchQueue.mainSafeAsync { Task { @MainActor in
self?.objectWillChange.send() self?.objectWillChange.send()
} }
} }
} }
}
deinit {
task?.cancel()
}
/** /**
Reset the key back to its default value. Reset the key back to its default value.
@ -77,7 +97,7 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
*/ */
public init(_ key: Defaults.Key<Value>) { public init(_ key: Defaults.Key<Value>) {
self.key = key self.key = key
self.observable = Defaults.Observable(key) self.observable = .init(key)
} }
public var wrappedValue: Value { public var wrappedValue: Value {
@ -178,7 +198,7 @@ extension Defaults {
public init(key: Defaults.Key<Bool>, @ViewBuilder label: @escaping () -> Label) { public init(key: Defaults.Key<Bool>, @ViewBuilder label: @escaping () -> Label) {
self.label = label self.label = label
self.observable = Defaults.Observable(key) self.observable = .init(key)
} }
public var body: some View { public var body: some View {
@ -194,7 +214,7 @@ extension Defaults {
extension Defaults.Toggle<Text> { extension Defaults.Toggle<Text> {
public init(_ title: some StringProtocol, key: Defaults.Key<Bool>) { public init(_ title: some StringProtocol, key: Defaults.Key<Bool>) {
self.label = { Text(title) } 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<Value: Serializable>(
_ key: Defaults.Key<Value>,
initial: Bool = true
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` 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, *) @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper @propertyWrapper
private struct ViewStorage<Value>: DynamicProperty { private struct ViewStorage<Value>: DynamicProperty {

View File

@ -10,7 +10,7 @@ extension UserDefaults {
} }
func _set<Value: Defaults.Serializable>(_ key: String, to value: Value) { func _set<Value: Defaults.Serializable>(_ key: String, to value: Value) {
if (value as? _DefaultsOptionalType)?.isNil == true { if (value as? _DefaultsOptionalProtocol)?.isNil == true {
removeObject(forKey: key) removeObject(forKey: key)
return return
} }

View File

@ -20,7 +20,7 @@ extension Decodable {
} }
final class ObjectAssociation<T: Any> { final class ObjectAssociation<T> {
subscript(index: AnyObject) -> T? { subscript(index: AnyObject) -> T? {
get { get {
objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? 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. - 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`. This is useful as you cannot compare `_OptionalType` to `nil`.
*/ */
var isNil: Bool { get } var isNil: Bool { get }
} }
extension Optional: _DefaultsOptionalType { extension Optional: _DefaultsOptionalProtocol {
public var isNil: Bool { self == nil } 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 { extension Sequence {
/** /**
Returns an array containing the non-nil elements. Returns an array containing the non-nil elements.

View File

@ -2,7 +2,7 @@ import Foundation
import Defaults import Defaults
import XCTest import XCTest
private enum FixtureCodableEnum: String, Defaults.Serializable & Codable & Hashable { private enum FixtureCodableEnum: String, Hashable, Codable, Defaults.Serializable {
case tenMinutes = "10 Minutes" case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes" case halfHour = "30 Minutes"
case oneHour = "1 Hour" case oneHour = "1 Hour"

View File

@ -349,7 +349,12 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
.collect(expectedArray.count) .collect(expectedArray.count)
.sink { result in .sink { result in
print("Result array: \(result)") 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 { inputArray.forEach {
@ -378,7 +383,12 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
.collect(expectedArray.count) .collect(expectedArray.count)
.sink { result in .sink { result in
print("Result array: \(result)") 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 { inputArray.forEach {

View File

@ -499,7 +499,12 @@ final class DefaultsTests: XCTestCase {
.collect(expectedArray.count) .collect(expectedArray.count)
.sink { result in .sink { result in
print("Result array: \(result)") 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 { inputArray.forEach {
@ -527,7 +532,12 @@ final class DefaultsTests: XCTestCase {
.collect(expectedArray.count) .collect(expectedArray.count)
.sink { result in .sink { result in
print("Result array: \(result)") 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 { inputArray.forEach {