Defaults/Sources/Defaults/Utilities.swift

149 lines
4.0 KiB
Swift

import Foundation
extension Decodable {
init?(jsonData: Data) {
guard let value = try? JSONDecoder().decode(Self.self, from: jsonData) else {
return nil
}
self = value
}
init?(jsonString: String) {
guard let data = jsonString.data(using: .utf8) else {
return nil
}
self.init(jsonData: data)
}
}
final class ObjectAssociation<T: Any> {
subscript(index: AnyObject) -> T? {
get {
objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T?
}
set {
objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
/**
Causes a given target object to live at least as long as a given owner object.
*/
final class LifetimeAssociation {
private class ObjectLifetimeTracker {
var object: AnyObject?
var deinitHandler: () -> Void
init(for weaklyHeldObject: AnyObject, deinitHandler: @escaping () -> Void) {
self.object = weaklyHeldObject
self.deinitHandler = deinitHandler
}
deinit {
deinitHandler()
}
}
private static let associatedObjects = ObjectAssociation<[ObjectLifetimeTracker]>()
private weak var wrappedObject: ObjectLifetimeTracker?
private weak var owner: AnyObject?
/**
Causes the given target object to live at least as long as either the given owner object or the resulting `LifetimeAssociation`, whichever is deallocated first.
When either the owner or the new `LifetimeAssociation` is destroyed, the given deinit handler, if any, is called.
```
class Ghost {
var association: LifetimeAssociation?
func haunt(_ host: Furniture) {
association = LifetimeAssociation(of: self, with: host) { [weak self] in
// Host has been deinitialized
self?.haunt(seekHost())
}
}
}
let piano = Piano()
Ghost().haunt(piano)
// The Ghost will remain alive as long as `piano` remains alive.
```
- Parameter target: The object whose lifetime will be extended.
- Parameter owner: The object whose lifetime extends the target object's lifetime.
- Parameter deinitHandler: An optional closure to call when either `owner` or the resulting `LifetimeAssociation` is deallocated.
*/
init(of target: AnyObject, with owner: AnyObject, deinitHandler: @escaping () -> Void = {}) {
let wrappedObject = ObjectLifetimeTracker(for: target, deinitHandler: deinitHandler)
let associatedObjects = LifetimeAssociation.associatedObjects[owner] ?? []
LifetimeAssociation.associatedObjects[owner] = associatedObjects + [wrappedObject]
self.wrappedObject = wrappedObject
self.owner = owner
}
/**
Invalidates the association, unlinking the target object's lifetime from that of the owner object. The provided deinit handler is not called.
*/
func cancel() {
wrappedObject?.deinitHandler = {}
invalidate()
}
deinit {
invalidate()
}
private func invalidate() {
guard
let owner = owner,
let wrappedObject = wrappedObject,
var associatedObjects = LifetimeAssociation.associatedObjects[owner],
let wrappedObjectAssociationIndex = associatedObjects.firstIndex(where: { $0 === wrappedObject })
else {
return
}
associatedObjects.remove(at: wrappedObjectAssociationIndex)
LifetimeAssociation.associatedObjects[owner] = associatedObjects
self.owner = nil
}
}
/// 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 {
/// This is useful as you can't compare `_OptionalType` to `nil`.
var isNil: Bool { get }
}
extension Optional: _DefaultsOptionalType {
public var isNil: Bool { self == nil }
}
func isOptionalType<T>(_ type: T.Type) -> Bool {
type is _DefaultsOptionalType.Type
}
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)
}
}
}