├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Etcetera │ ├── BackgroundTasks │ ├── BackgroundTask.swift │ ├── UnsupportedBackgroundTask.swift │ └── iOSBackgroundTask.swift │ ├── Foundation │ ├── DispatchQueue.swift │ ├── FileManager.swift │ ├── Locking.swift │ ├── NotificationCenter.swift │ ├── NotificationObserver.swift │ ├── OperationQueue.swift │ └── ProcessInfo.swift │ ├── Global │ ├── AnyInstanceIdentifier.swift │ ├── Container+Context.swift │ ├── Container.swift │ ├── Global.swift │ ├── GloballyAvailable.swift │ ├── GloballyIdentifiable.swift │ └── InstanceResolver.swift │ ├── Networking │ └── Reachability.swift │ ├── Swift │ ├── Collections.swift │ └── Sequence.swift │ ├── UIKit │ ├── CGFloat.swift │ ├── UIApplication.swift │ ├── UIView.swift │ └── UIViewController.swift │ └── UnifiedLogging │ ├── OSActivity.swift │ └── OSLog.swift └── Tests └── EtceteraTests ├── ActivityShit.swift ├── GlobalTests.swift └── LockTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | .swiftpm 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | 70 | # Goddamnit 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jared Sinclair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Etcetera", 7 | platforms: [ 8 | .iOS(.v13), .watchOS(.v6) 9 | ], 10 | products: [ 11 | .library(name: "Etcetera", targets: ["Etcetera"]) 12 | ], 13 | targets: [ 14 | .target( 15 | name: "Etcetera", 16 | swiftSettings: [ .swiftLanguageMode(.v6) ] 17 | ), 18 | .testTarget( 19 | name: "EtceteraTests", 20 | dependencies: [ 21 | "Etcetera" 22 | ] 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Etcetera 2 | 3 | Apple platform utilities I need on almost every project but which, individually, are too small to exist on their own. 4 | 5 | ## So What 6 | 7 | Etcetera is a mish-mash of extensions and utility classes. Every file in this repository is (mostly) intended to stand on its own, requiring nothing except some first-party Apple frameworks. 8 | 9 | ## Usage 10 | 11 | Swift Package Manager is the _de rigeur_ solution these days. Adding a Swift package to an Xcode project is absurdly easy. I don't use Cocoapods or Carthage, and I have no interest in adding support for them. 12 | 13 | ## Acknowledgements 14 | 15 | - The `Activity` approach to the `os_activity` wrapper is based on work by [Zach Waldowski](https://gist.github.com/zwaldowski/49f61292757f86d7d036a529f2d04f0c). 16 | -------------------------------------------------------------------------------- /Sources/Etcetera/BackgroundTasks/BackgroundTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundTask.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | private typealias Internals = iOSBackgroundTask 11 | #else 12 | private typealias Internals = UnsupportedBackgroundTask 13 | #endif 14 | 15 | /// A cross-platform wrapper for requesting background execution time. 16 | public final class BackgroundTask: Sendable { 17 | 18 | /// Convenience for initializing a task with a default expiration handler. 19 | /// 20 | /// - returns: Returns `nil` if background task time was denied. 21 | @MainActor public static func start() -> BackgroundTask? { 22 | let task = BackgroundTask() 23 | let successful = task.start() 24 | return (successful) ? task : nil 25 | } 26 | 27 | /// Begins a background task with the system. 28 | /// 29 | /// - parameter handler: A block to be invoked if the task expires. Any 30 | /// cleanup necessary to recover from expired background time should be 31 | /// performed inside this block — synchronously, since the app will be 32 | /// suspended when the block returns. 33 | /// 34 | /// - returns: Returns `true` if background execution time was allotted. 35 | @MainActor public func start(withExpirationHandler handler: @escaping @Sendable () -> Void = {}) -> Bool { 36 | internals.start(withExpirationHandler: handler) 37 | } 38 | 39 | /// Ends the background task. 40 | public func end() { 41 | internals.end() 42 | } 43 | 44 | public init() {} 45 | 46 | private let internals = Internals() 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Etcetera/BackgroundTasks/UnsupportedBackgroundTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnsupportedBackgroundTask.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// For environments that do not support background tasks. 12 | final class UnsupportedBackgroundTask: Sendable { 13 | 14 | @MainActor static func start() -> UnsupportedBackgroundTask? { 15 | return nil 16 | } 17 | 18 | @MainActor func start(withExpirationHandler handler: (() -> Void)?) -> Bool { 19 | return false 20 | } 21 | 22 | func end() { 23 | // no op 24 | } 25 | 26 | init() {} 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Etcetera/BackgroundTasks/iOSBackgroundTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOSBackgroundTask.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | 11 | import UIKit 12 | 13 | /// A quality-of-life wrapper around requesting iOS background execution time. 14 | final class iOSBackgroundTask: Sendable { 15 | 16 | /// Convenience for initializing a task with a default expiration handler. 17 | /// 18 | /// - returns: Returns `nil` if background task time was denied. 19 | @MainActor static func start() -> iOSBackgroundTask? { 20 | let task = iOSBackgroundTask() 21 | let successful = task.start() 22 | return (successful) ? task : nil 23 | } 24 | 25 | /// Begins a background task with the system. 26 | /// 27 | /// - parameter handler: A block to be invoked if the task expires. Any 28 | /// cleanup necessary to recover from expired background time should be 29 | /// performed inside this block — synchronously, since the app will be 30 | /// suspended when the block returns. 31 | /// 32 | /// - returns: Returns `true` if background execution time was allotted. 33 | @MainActor func start(withExpirationHandler handler: @escaping @Sendable () -> Void = {}) -> Bool { 34 | self.taskId = UIApplication.shared.beginBackgroundTask { 35 | handler() 36 | self.end() 37 | } 38 | return (self.taskId != .invalid) 39 | } 40 | 41 | /// Ends the background task. 42 | func end() { 43 | guard self.taskId != .invalid else { return } 44 | let taskId = self.taskId 45 | self.taskId = .invalid 46 | DispatchQueue.main.async { 47 | UIApplication.shared.endBackgroundTask(taskId) 48 | } 49 | } 50 | 51 | init() {} 52 | 53 | private var taskId: UIBackgroundTaskIdentifier { 54 | get { _taskId.current } 55 | set { _taskId.current = newValue } 56 | } 57 | 58 | private let _taskId = Protected(UIBackgroundTaskIdentifier.invalid) 59 | 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/DispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension DispatchQueue { 12 | 13 | /// Because c'mon, really, how often do I ever need anything but this? 14 | public func after(_ seconds: TimeInterval, execute block: @escaping @Sendable () -> Void) { 15 | asyncAfter(deadline: .now() + seconds, execute: block) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Convenience methods extending FileManager. These swallow errors you don't 12 | /// care to know the details about, or force-unwrap things that should never be 13 | /// nil in practice (where a crash would be preferable to undefined behavior). 14 | public extension FileManager { 15 | 16 | func cachesDirectory() -> URL { 17 | #if os(iOS) || os(watchOS) 18 | return urls(for: .cachesDirectory, in: .userDomainMask).first! 19 | #elseif os(OSX) 20 | let library = urls(for: .libraryDirectory, in: .userDomainMask).first! 21 | return library.appendingPathComponent("Caches", isDirectory: true) 22 | #endif 23 | } 24 | 25 | func documentsDirectory() -> URL { 26 | return urls(for: .documentDirectory, in: .userDomainMask).first! 27 | } 28 | 29 | func createDirectory(at url: URL) -> Bool { 30 | do { 31 | try createDirectory( 32 | at: url, 33 | withIntermediateDirectories: true, 34 | attributes: nil 35 | ) 36 | return true 37 | } catch { 38 | return false 39 | } 40 | } 41 | 42 | func createSubdirectory(named name: String, atUrl url: URL) -> Bool { 43 | let subdirectoryUrl = url.appendingPathComponent(name, isDirectory: true) 44 | return createDirectory(at: subdirectoryUrl) 45 | } 46 | 47 | func removeDirectory(_ directory: URL) -> Bool { 48 | do { 49 | try removeItem(at: directory) 50 | return true 51 | } catch { 52 | return false 53 | } 54 | } 55 | 56 | func removeFile(at url: URL) -> Bool { 57 | do { 58 | try removeItem(at: url) 59 | return true 60 | } catch { 61 | return false 62 | } 63 | } 64 | 65 | func fileExists(at url: URL) -> Bool { 66 | return fileExists(atPath: url.path) 67 | } 68 | 69 | func moveFile(from: URL, to: URL) throws { 70 | if fileExists(at: to) { 71 | _ = try replaceItemAt(to, withItemAt: from) 72 | } else { 73 | _ = try moveItem(at: from, to: to) 74 | } 75 | } 76 | 77 | func removeFilesByDate(inDirectory directory: URL, untilWithinByteLimit limit: UInt) { 78 | 79 | struct Item { 80 | let url: NSURL 81 | let fileSize: UInt 82 | let dateModified: Date 83 | } 84 | 85 | let keys: [URLResourceKey] = [.fileSizeKey, .contentModificationDateKey] 86 | 87 | guard let urls = try? contentsOfDirectory( 88 | at: directory, 89 | includingPropertiesForKeys: keys, 90 | options: .skipsHiddenFiles 91 | ) else { return } 92 | 93 | let items: [Item] = (urls as [NSURL]) 94 | .compactMap { url -> Item? in 95 | guard let values = try? url.resourceValues(forKeys: keys) else { return nil } 96 | return Item( 97 | url: url, 98 | fileSize: (values[.fileSizeKey] as? NSNumber)?.uintValue ?? 0, 99 | dateModified: (values[.contentModificationDateKey] as? Date) ?? Date.distantPast 100 | ) 101 | } 102 | .sorted { $0.dateModified < $1.dateModified } 103 | 104 | var total = items.map { $0.fileSize }.reduce(0, +) 105 | var toDelete = [Item]() 106 | for item in items { 107 | guard total > limit else { break } 108 | total -= item.fileSize 109 | toDelete.append(item) 110 | } 111 | 112 | toDelete.forEach { 113 | _ = try? self.removeItem(at: $0.url as URL) 114 | } 115 | } 116 | 117 | } 118 | 119 | private func didntThrow(_ block: () throws -> Void) -> Bool { 120 | do{ try block(); return true } catch { return false } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/Locking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Locking.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import os.lock 10 | 11 | /// A high-performance lock supported by all Apple platforms. 12 | /// 13 | /// This lock is **not** recursive. 14 | public final class Lock: @unchecked Sendable { 15 | 16 | /// See WWDC 2016 Session 720. Using C struct locks like `pthread_mutex_t` 17 | /// or `os_unfair_lock` directly from Swift code is discouraged because of 18 | /// Swift's assumption that value types can be moved around freely in 19 | /// memory. Instead we have to manually manage memory here to ensure that 20 | /// the lock struct has a fixed address during its lifetime. 21 | private let lock = UnsafeMutablePointer.allocate(capacity: 1) 22 | 23 | public init() { 24 | lock.initialize(to: os_unfair_lock()) 25 | } 26 | 27 | deinit { 28 | lock.deinitialize(count: 1) 29 | lock.deallocate() 30 | } 31 | 32 | /// Performs `block` inside a balanced lock/unlock pair. 33 | public func locked(_ block: () throws -> T) rethrows -> T { 34 | os_unfair_lock_lock(lock) 35 | defer { os_unfair_lock_unlock(lock) } 36 | return try block() 37 | } 38 | } 39 | 40 | /// A generic wrapper around a given value, which is protected by a `Lock`. 41 | public final class Protected: @unchecked Sendable { 42 | 43 | /// Read/write access to the value as if you had used `access(_:)`. 44 | public var current: T { 45 | get { return access { $0 } } 46 | set { access { $0 = newValue } } 47 | } 48 | 49 | private let lock = Lock() 50 | private var value: T 51 | 52 | public init(_ value: T) { 53 | self.value = value 54 | } 55 | 56 | /// Accesses the protected value inside a balanced lock/unlock pair. 57 | /// 58 | /// - parameter block: Can either mutate the passed-in value or not, and can 59 | /// also return a value (or an implied Void). 60 | public func access(_ block: (inout T) throws -> Return) rethrows -> Return { 61 | return try lock.locked { 62 | try block(&value) 63 | } 64 | } 65 | } 66 | 67 | extension Protected { 68 | 69 | /// Convenience initializer that defaults to `nil`. 70 | public convenience init() where T == Optional { 71 | self.init(nil) 72 | } 73 | 74 | } 75 | 76 | /// A dictionary-like object that provides synchronized read/writes via an 77 | /// underlying `Protected` value. 78 | public final class ProtectedDictionary: Sendable { 79 | 80 | private let protected: Protected<[Key: Value]> 81 | 82 | public init(_ contents: [Key: Value] = [:]) { 83 | self.protected = Protected(contents) 84 | } 85 | 86 | /// Read/write access to the underlying dictionary storage as if you had 87 | /// used the `access(_:)` method to subscript the dictionary directly. 88 | public subscript(key: Key) -> Value? { 89 | get { return protected.access { $0[key] } } 90 | set { protected.access { $0[key] = newValue } } 91 | } 92 | 93 | /// Accesses the protected value inside a balanced lock/unlock pair. 94 | /// 95 | /// - parameter block: Can either mutate the passed-in value or not, and can 96 | /// also return a value (or an implied Void). 97 | public func access(_ block: (inout [Key: Value]) throws -> Return) rethrows -> Return { 98 | return try protected.access(block) 99 | } 100 | 101 | public func removeAll() { 102 | protected.access { $0.removeAll() } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/NotificationCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenter.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Quality-of-life extension of NotificationCenter. 12 | extension NotificationCenter { 13 | 14 | /// Posts a notification using the default center. 15 | /// 16 | /// - parameter name: The name of the notification to post. 17 | public static func post(_ name: Notification.Name) { 18 | NotificationCenter.default.post(name: name, object: nil) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/NotificationObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationObserver.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Convenience for observing NSNotifications. 12 | /// 13 | /// NotificationObserver removes all observers upon deinit, which means that the 14 | /// developer using a NotificationObserver can simply declare a property like: 15 | /// 16 | /// private var observer = NotificationObserver() 17 | /// 18 | /// and trust ARC to release the observer at the appropriate time, which will 19 | /// remove all observations. This assumes, of course, that all blocks passed to 20 | /// `when(_:perform:)` do not strongly capture `self`. 21 | public final class NotificationObserver: NSObject, Sendable { 22 | 23 | // MARK: - Typealiases 24 | 25 | /// Signature for the block which callers can use to remove an existing 26 | /// observer during the NotificationObserver's lifetime. 27 | public typealias Unobserver = @Sendable () -> Void 28 | 29 | // MARK: - Private Properties 30 | 31 | /// If `true`, and if `object` is `nil`, then the target object passed into 32 | /// the initializer is presumed to have been released (it is a weak ref). 33 | private let wasInitializedWithTargetObject: Bool 34 | 35 | /// The target queue for all observation callbacks. 36 | private let queue: OperationQueue 37 | 38 | /// The target object to be used when observing notifications. 39 | private var object: AnyObject? { 40 | get { _object.access { 41 | $0.object 42 | }} 43 | set { _object.access { 44 | $0.object = newValue 45 | }} 46 | } 47 | 48 | /// The backing storage for `object`. 49 | private let _object: Protected 50 | 51 | /// A tote bag of observation tokens. 52 | private let tokens = Protected<[Token]>([]) 53 | 54 | // MARK: - Sigh, Concurrency 55 | 56 | private struct Token: @unchecked Sendable { 57 | let wrapped: NSObjectProtocol 58 | } 59 | 60 | // MARK: - Init/Deinit 61 | 62 | /// Designated initializer. 63 | /// 64 | /// - parameter object: Optional. A target object to use with observations. 65 | /// 66 | /// - parameter queue: The target queue for all observation callbacks. 67 | public init(object: AnyObject? = nil, queue: OperationQueue = .main) { 68 | self._object = Protected(WeakReferencingBox(object: object)) 69 | self.wasInitializedWithTargetObject = (object != nil) 70 | self.queue = queue 71 | } 72 | 73 | deinit { 74 | tokens.current.forEach { 75 | NotificationCenter.default.removeObserver($0) 76 | } 77 | } 78 | 79 | // MARK: - Public Methods 80 | 81 | /// Adds an observation for a given notification. 82 | /// 83 | /// This method's signature is designed for succinct clarity at the call 84 | /// site compared with the usual boilerplate, to wit: 85 | /// 86 | /// observer.when(UIApplication.DidBecomeActive) { note in 87 | /// // do something with `note` 88 | /// } 89 | /// 90 | /// Which is especially useful in classes that require observing more than 91 | /// one notification name. 92 | /// 93 | /// - parameter name: The notification name to observe. 94 | /// 95 | /// - parameter block: The block to be performed upon each notification. 96 | /// This block will always be called on `queue`. 97 | /// 98 | /// - returns: Returns a block which can be used to remove the observation 99 | /// later on, if desired. This is not necessary for general use, however. 100 | @discardableResult 101 | public func when(_ name: Notification.Name, perform block: @escaping @Sendable (Notification) -> Void) -> Unobserver { 102 | guard wasInitializedWithTargetObject == false || object != nil else { return {} } 103 | let token = Token(wrapped: NotificationCenter.default.addObserver( 104 | forName: name, 105 | object: object, 106 | queue: queue, 107 | using: block 108 | )) 109 | let unobserve: Unobserver = { 110 | NotificationCenter.default.removeObserver(token) 111 | } 112 | queue.asap { [weak self] in 113 | if let this = self { 114 | this.tokens.access { 115 | $0.append(token) 116 | } 117 | } else { 118 | unobserve() 119 | } 120 | } 121 | return unobserve 122 | } 123 | 124 | /// An alternative to the above method which does not pass a reference to 125 | /// the notification in the block argument, which can spare you a `_ in`. 126 | @discardableResult 127 | public func when(_ name: Notification.Name, perform block: @escaping @Sendable () -> Void) -> Unobserver { 128 | return when(name, perform: { _ in block() }) 129 | } 130 | 131 | // MARK: - Nested Types 132 | 133 | private final class WeakReferencingBox { 134 | weak var object: AnyObject? 135 | 136 | init(object: AnyObject? = nil) { 137 | self.object = object 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/OperationQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Quality-of-life extension of OperationQueue. 12 | extension OperationQueue { 13 | 14 | /// Is `true` if called from the main queue. 15 | public static var isMain: Bool { 16 | return OperationQueue.main.isCurrent 17 | } 18 | 19 | /// Performs a block on the main queue as soon as possible without blocking 20 | /// execution on the current queue, if it isn't the main queue itself. 21 | /// 22 | /// - parameter block: The block to be performed. 23 | public static func onMain(_ block: @escaping @Sendable () -> Void) { 24 | OperationQueue.main.asap(block) 25 | } 26 | 27 | /// Initializes and returns a new queue with a max concurrent operation 28 | /// count of 1. 29 | public static func serialQueue() -> OperationQueue { 30 | let queue = OperationQueue() 31 | queue.maxConcurrentOperationCount = 1 32 | return queue 33 | } 34 | 35 | /// - returns: Returns `true` if called from `queue`. 36 | public var isCurrent: Bool { 37 | return OperationQueue.current === self 38 | } 39 | 40 | /// Performs a block on the receiver as soon as possible without blocking 41 | /// execution on the current queue, if it isn't the receiver itself. 42 | /// 43 | /// - parameter block: The block to be performed. 44 | public func asap(_ block: @escaping @Sendable () -> Void) { 45 | if OperationQueue.current === self { 46 | block() 47 | } else { 48 | addOperation(block) 49 | } 50 | } 51 | 52 | /// Adds an array of operations to the receiver. 53 | /// 54 | /// The operations added will be executed asynchronously, according to the 55 | /// standard 56 | /// 57 | /// - parameter operations: An array of operations to be added, in order. 58 | public func add(_ operations: [Operation]) { 59 | addOperations(operations, waitUntilFinished: false) 60 | } 61 | 62 | public func sync(_ block: @escaping @Sendable () -> Void) { 63 | let operation = BlockOperation(block: block) 64 | addOperations([operation], waitUntilFinished: true) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Etcetera/Foundation/ProcessInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessInfo.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | // swiftlint:disable nesting - Seriously, why do we even Swift then. 9 | 10 | import Foundation 11 | 12 | /// Quality-of-life extension of ProcessInfo. 13 | extension ProcessInfo { 14 | 15 | /// Represents an process info argument. 16 | /// 17 | /// Define your own custom arguments in an extension of Argument: 18 | /// 19 | /// extension ProcessInfo.Argument { 20 | /// static let resetCachesOnLaunch = "resetCachesOnLaunch" 21 | /// } 22 | /// 23 | /// If you want to prefix your launch arguments, then as early as possible 24 | /// during app launch, provide a value for the common prefix: 25 | /// 26 | /// ProcessInfo.Argument.commonPrefix = "-com.domain.MyApp" 27 | /// 28 | /// Next, edit your target's Xcode scheme to add the following for each of 29 | /// your custom launch arguments: 30 | /// 31 | /// -com.domain.MyApp.resetCachesOnLaunch 1 32 | /// 33 | /// Check for the presence of a process argument via: 34 | /// 35 | /// if ProcessInfo.isArgumentEnabled(.resetCachesOnLaunch) { ... } 36 | /// 37 | /// or: 38 | /// 39 | /// if ProcessInfo.Argument.resetCachesOnLaunch.isEnabled { ... } 40 | public struct Argument: RawRepresentable, ExpressibleByStringLiteral, Sendable { 41 | 42 | /// Supply your own "-com.domain.MyApp." prefix which must be present in 43 | /// all the custom arguments defined in your target's scheme editor. 44 | public static nonisolated var commonPrefix: String { 45 | get { _commonPrefix.current } 46 | set { _commonPrefix.current = newValue } 47 | } 48 | 49 | /// Backing storage for `commonPrefix`. 50 | private static let _commonPrefix = Protected("") 51 | 52 | /// Required by `RawRepresentable`. 53 | public typealias RawValue = String 54 | 55 | /// Required by `ExpressibleByStringLiteral` 56 | public typealias StringLiteralType = String 57 | 58 | /// The portion of the argument excluding any common prefix. 59 | public let rawValue: String 60 | 61 | /// - returns: Returns `true` if the argument is found among the process 62 | /// info launch arguments. 63 | public var isEnabled: Bool { 64 | return ProcessInfo.isArgumentEnabled(self) 65 | } 66 | 67 | /// Required by `RawRepresentable`. 68 | public init(rawValue: String) { 69 | self.rawValue = rawValue 70 | } 71 | 72 | /// Required by `RawRepresentable`. 73 | public init(stringLiteral value: String) { 74 | self.rawValue = value 75 | } 76 | 77 | } 78 | 79 | /// - returns: Returns `true` if `argument` is found among the arguments. 80 | public static nonisolated func isArgumentEnabled(_ argument: Argument) -> Bool { 81 | let string = Argument.commonPrefix + argument.rawValue 82 | return processInfo.arguments.contains(string) 83 | } 84 | 85 | /// Returns `true` if the app is running as a test runner for unit tests. 86 | public static nonisolated var isRunningInUnitTests: Bool { 87 | processInfo.environment["XCTestConfigurationFilePath"] != nil 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/AnyInstanceIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyInstanceIdentifier.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | /// Used to provide the same behavior as `GloballyIdentifiable.InstanceIdentifier` 10 | /// but for `@Global`-wrapped types that participate in injection via developer- 11 | /// provided extensions of `Global`. 12 | /// 13 | /// - SeeAlso: Global.init(instanceIdentifier:initializer) 14 | @usableFromInline internal struct AnyInstanceIdentifier: Hashable { 15 | 16 | /// Identifies a particular instance of `T`. 17 | let instanceIdentifier: InstanceIdentifier 18 | 19 | /// Identifies all instances of `T` as a group. 20 | let metatypeIdentifier = ObjectIdentifier(T.self) 21 | 22 | /// Initializes an identifier. 23 | @usableFromInline init(_ instanceIdentifier: InstanceIdentifier) { 24 | self.instanceIdentifier = instanceIdentifier 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/Container+Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container+Context.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | extension Container { 10 | 11 | /// Defines the context within which the container is being used. 12 | public enum Context: Hashable { 13 | 14 | /// Running in the typical manner. 15 | case running 16 | 17 | /// Running within a unit test. 18 | case unitTesting 19 | 20 | /// Running within a user interface test. 21 | /// 22 | /// - Note: It is not possible for the container to detect this context 23 | /// automatically. You must configure this value yourself by overriding 24 | /// the value of `DependencyContainer.context`. 25 | case userInterfaceTesting 26 | 27 | /// Running in a custom context. 28 | case custom(AnyHashable) 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Resolves and caches global dependencies. 12 | @MainActor public class Container { 13 | 14 | // MARK: - Public (Static) 15 | 16 | /// The current context. 17 | @inlinable public static var context: Context { shared.context } 18 | 19 | /// A developer-provided context to override the underlying default context. 20 | /// 21 | /// In most cases there's no need to override this value. You might do so 22 | /// when your app launches for UI testing, since this scenario cannot be 23 | /// detected through a generalizable solution: 24 | /// 25 | /// For example, in your UI test you might have code like this: 26 | /// 27 | /// let app = XCUIApplication() 28 | /// app.launchArguments.append("IS_UI_TESTING") 29 | /// app.launch() 30 | /// 31 | /// Then in your app delegate, you would forward this to the dependency 32 | /// container like so: 33 | /// 34 | /// if ProcessInfo.processInfo.arguments.contains("IS_UI_TESTING") { 35 | /// Container.overrideContext = .userInterfaceTesting 36 | /// } 37 | /// 38 | /// Lastly, if your application has dependencies that need to be configured 39 | /// differently for UI tests versus production, you can switch on the 40 | /// context as needed. Protocols can only be used with `@Global` through an 41 | /// extension of the `Global` struct that your app provides: 42 | /// 43 | /// extension Global where Wrapped == WebServiceProtocol { 44 | /// init() { 45 | /// self.init(initializer: { container in 46 | /// switch container.context { 47 | /// case .userInterfaceTesting: 48 | /// return MockService() 49 | /// default: 50 | /// return ProdService() 51 | /// } 52 | /// }) 53 | /// } 54 | /// } 55 | /// 56 | /// This setup will allow your dependent types to receive the correct 57 | /// implementation of a protocol for production versus UI tests: 58 | /// 59 | /// @Global() var webService: WebServiceProtocol 60 | /// 61 | public static var overrideContext: Context? { 62 | get { shared.overrideContext } 63 | set { shared.overrideContext = newValue } 64 | } 65 | 66 | /// Removes all previously-resolved dependencies. 67 | /// 68 | /// This method is safe to call from any queue, though it is difficult to 69 | /// imagine a scenario where it is not preferrable to call it from `.main`. 70 | @inlinable public static func removeAll() { 71 | shared.removeAll() 72 | } 73 | 74 | // MARK: - Public (Instance) 75 | 76 | /// The current context. The override context is used if the developer 77 | /// provides one, otherwise it will fall back to the default context. 78 | @inlinable public var context: Context { overrideContext ?? defaultContext } 79 | 80 | /// Resolves an instance of `T`. 81 | /// 82 | /// Use this method to resolve dependencies of a type that is itself a 83 | /// global dependency. For example, you might use it in an implementation of 84 | /// the `GloballyAvailable` protocol: 85 | /// 86 | /// extension Foo: GloballyAvailable { 87 | /// static func make(container: Container) -> Self { 88 | /// let bar = container.resolveInstance(of: Bar.self) 89 | /// return Foo(bar: bar) 90 | /// } 91 | /// } 92 | /// 93 | /// Or you might use it in a custom extension of `Global`, perhaps to allow 94 | /// a protocol type to participate: 95 | /// 96 | /// extension Global where Wrapped == SomeProtocol { 97 | /// init() { 98 | /// self.init(initializer: { 99 | /// Foo(bar: $0.resolveInstance()) 100 | /// }) 101 | /// } 102 | /// } 103 | /// 104 | /// - Note: Please take note that both available `resolveInstance` methods, 105 | /// (this one and the other one) **do not support types unless they are 106 | /// concrete types that conform to either GloballyAvailable or GloballyIdentifiable**. 107 | /// This is because the container needs a compile-time guaranteed way of 108 | /// resolving any dependency it is asked for with these methods. Only a 109 | /// concrete type conforming to one of these protocols is able to provide a 110 | /// compile-time guarantee. 111 | @inlinable public func resolveInstance(of type: T.Type = T.self) -> T { 112 | instanceResolver(for: T.self).resolved(container: self) 113 | } 114 | 115 | /// Same as `resolveInstance(for identifier: T.InstanceIdentifier, of type: T.Type = T.self) -> T { 119 | instanceResolver(for: T.self).resolved(container: self, identifier: identifier) 120 | } 121 | 122 | // MARK: - Internal (Static) 123 | 124 | /// The shared container. 125 | /// 126 | /// Do **not** expose this as a public member. Use of this class is very 127 | /// intentionally limited to a handful of officially-supported workflows. 128 | @usableFromInline internal static let shared = Container() 129 | 130 | // MARK: - Internal (Instance) 131 | 132 | /// The fallback context when no `overrideContext` is set. 133 | @usableFromInline internal let defaultContext: Context = { 134 | ProcessInfo.isRunningInUnitTests ? .unitTesting : .running 135 | }() 136 | 137 | /// The developer-provided context, if any. 138 | @usableFromInline internal var overrideContext: Context? 139 | 140 | /// Thread-safe storage of instance resolvers (see extended developer 141 | /// comment inside `resolveInstance(of:)`. 142 | @usableFromInline internal var storage = Protected<[ObjectIdentifier: InstanceResolver]>([:]) 143 | 144 | /// Initializes a dependency container 145 | @usableFromInline internal init() {} 146 | 147 | /// Resolves an instance of a type that is **not** constrained by either the 148 | /// `GloballyAvailable` or `GloballyIdentifiable` protocol. This method must 149 | /// not exposed at a public scope. It's only to be used by the `Global` 150 | /// struct in its "designated" initializers, which are called from 151 | /// developer-provided convenience initializers in extensions. 152 | @usableFromInline internal func resolveInstance(via initializer: @MainActor (Container) -> T) -> T { 153 | instanceResolver(for: T.self).resolved(via: initializer, container: self) 154 | } 155 | 156 | /// Same as `resolveInstance(via:)` except it also takes an instance identifier. 157 | @usableFromInline internal func resolveInstance(for identifier: InstanceIdentifier, via initializer: @MainActor (Container) -> T) -> T { 158 | instanceResolver(for: T.self).resolved(for: identifier, via: initializer, container: self) 159 | } 160 | 161 | /// Finds (or creates) an instance resolver responsible for resolving one or 162 | /// more instances of `T`. 163 | @usableFromInline internal func instanceResolver(for type: T.Type) -> InstanceResolver { 164 | storage.access { storage -> InstanceResolver in 165 | let resolverKey = ObjectIdentifier(T.self) 166 | if let existing = storage[resolverKey] { 167 | return existing 168 | } else { 169 | let new = InstanceResolver() 170 | storage[resolverKey] = new 171 | return new 172 | } 173 | } 174 | } 175 | 176 | /// Removes all resolved dependencies. 177 | /// 178 | /// This method is safe to call from any queue, though it is difficult to 179 | /// imagine a scenario where it is not preferrable to call it from `.main`. 180 | @usableFromInline internal func removeAll() { 181 | storage.access { 182 | $0.removeAll() 183 | } 184 | } 185 | 186 | } 187 | 188 | // MARK: - Automated Testing 189 | 190 | extension Container { 191 | 192 | /// Seeds a resolved value of `T` into the global container. 193 | /// 194 | /// Use this method as an alternative to `make(container:)` to supercede 195 | /// what that method would have returned to the container. Care must 196 | /// be taken when using this method: any `@Global` property wrappers that 197 | /// attempt to resolve a value of `T` must not begin resolving their values 198 | /// until **after** you have called `seed(value:)`. 199 | /// 200 | /// Use of this method is strongly discouraged except outside of automated 201 | /// testing because **it will overwrite any previously resolved value**. 202 | @inlinable public static func seed(value: T) { 203 | shared.storage.access { 204 | let key = ObjectIdentifier(T.self) 205 | $0[key] = InstanceResolver(resolvedValue: value) 206 | } 207 | } 208 | 209 | /// The same as `seed(value:)`, except with per-instance identifiers. 210 | /// 211 | /// Use of this method is strongly discouraged except outside of automated 212 | /// testing because **it will overwrite any previously resolved values**. 213 | @inlinable public static func seed(values: [T.InstanceIdentifier: T]) { 214 | shared.storage.access { 215 | let key = ObjectIdentifier(T.self) 216 | $0[key] = InstanceResolver(resolvedValues: values) 217 | } 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/Global.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | /// Property wrapper that transparently resolves global dependencies via a 10 | /// shared dependency container. 11 | /// 12 | /// Please consult the README in this repository for usage and cautions. 13 | @propertyWrapper public struct Global { 14 | 15 | /// The wrapped value (required by `@propertyWrapper`). 16 | @MainActor public var wrappedValue: Wrapped { 17 | mutating get { 18 | if let existing = _wrappedValue { 19 | return existing 20 | } else { 21 | let new = resolver() 22 | _wrappedValue = new 23 | return new 24 | } 25 | } 26 | } 27 | 28 | /// A closure that resolves the wrapped dependency using the shared 29 | /// dependency container. 30 | private let resolver: @MainActor () -> Wrapped 31 | 32 | /// Backing property for `wrappedValue`. 33 | private var _wrappedValue: Wrapped? 34 | 35 | /// Initializes a Global using a closure that returns a wrapped value. The 36 | /// closure is only evaluated if an existing cached value cannot be found 37 | /// in the shared container. If that happens, the value returned from the 38 | /// closure is cached in the shared container for future re-use. 39 | /// 40 | /// It is very, very unlikely that you would ever use this initializer 41 | /// directly when declaring an `@Global`-wrapped property. Instead, Global 42 | /// wrappers are regularly initialized via init methods declared in 43 | /// extensions (see the README for details). 44 | public init(initializer: @MainActor @escaping @Sendable (Container) -> Wrapped) { 45 | resolver = { 46 | Container.shared.resolveInstance(via: initializer) 47 | } 48 | } 49 | 50 | /// Initializes a Global using a closure that returns a wrapped value. The 51 | /// closure is only evaluated if an existing cached value cannot be found 52 | /// in the shared container. If that happens, the value returned from the 53 | /// closure is cached in the shared container for future re-use. 54 | /// 55 | /// Unlike `init(initializer:)`, this method augments the caching strategy 56 | /// with an identifier (`instanceIdentifier`) that identifies a particular 57 | /// instance of `Wrapped` in the cache. The `initializer` closure is 58 | /// evaluated whenever a cached value for `instanceIdentifier` cannot be 59 | /// found in the cache. 60 | /// 61 | /// It is very, very unlikely that you would ever use this initializer 62 | /// directly when declaring an `@Global`-wrapped property. Instead, Global 63 | /// wrappers are regularly initialized via init methods declared in 64 | /// extensions (see the README for details). 65 | public init(instanceIdentifier: InstanceIdentifier, initializer: @MainActor @escaping @Sendable (Container) -> Wrapped) { 66 | resolver = { 67 | let someKey = AnyInstanceIdentifier(instanceIdentifier) 68 | return Container.shared.resolveInstance(for: someKey, via: initializer) 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/GloballyAvailable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GloballyAvailable.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | /// A protocol to which most concrete types used with `@Global` must conform. 10 | /// 11 | /// Only concrete types can be used with `GloballyAvailable`. Protocol types 12 | /// can participate in `@Global` only by extensions of the `Global` struct that 13 | /// provide `init()` methods for each protocol type. 14 | /// 15 | /// - Note: If you have a type that needs to have multiple instances available 16 | /// to `@Global`, use the `GloballyIdentifiable` protocol instead. 17 | public protocol GloballyAvailable { 18 | 19 | /// Produces an instance of `Self` on demand. This method is called from the 20 | /// shared dependency container when a cached instance of `Self` cannot be 21 | /// found in the cache. 22 | @MainActor static func make(container: Container) -> Self 23 | 24 | } 25 | 26 | extension Global where Wrapped: GloballyAvailable { 27 | 28 | /// Initializes a Global, wrapping any type that conforms to GloballyAvailable. 29 | /// 30 | /// Here is some example usage: 31 | /// 32 | /// class MyClass: GloballyAvailable { ... } 33 | /// 34 | /// @Global() var myClass: MyClass 35 | /// 36 | /// Don't forget the trailing parentheses! 37 | public init() { 38 | self.init { container in 39 | container.resolveInstance() 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/GloballyIdentifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GloballyIdentifiable.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | /// A protocol to which concrete types used with `@Global` must conform, if your 10 | /// application requires multiple concurrent instances of the type to be made 11 | /// available to `@Global` property wrappers, keyed by per-instance identifiers. 12 | /// 13 | /// Only concrete types can be used with `GloballyAvailable`. Protocol types 14 | /// can participate in `@Global` only by extensions of the `Global` struct that 15 | /// provide `init()` methods for each protocol type. 16 | /// 17 | /// See the README for more information on when and how to use this protocol. 18 | public protocol GloballyIdentifiable { 19 | 20 | /// The type to be used for per-instance identifiers. 21 | associatedtype InstanceIdentifier: Hashable & Sendable 22 | 23 | /// Produces an instance of `Self` on demand. This method is called from the 24 | /// shared dependency container when a cached instance of `Self` cannot be 25 | /// found in the cache for the given `identifier`. 26 | /// 27 | /// Since the `InstanceIdentifier` associated type can be anything that 28 | /// conforms to `Hashable`, you can use a type that contains any information 29 | /// necessary to initialize a `Self` for that identifier. 30 | @MainActor static func make(container: Container, identifier: InstanceIdentifier) -> Self 31 | 32 | } 33 | 34 | extension Global where Wrapped: GloballyIdentifiable { 35 | 36 | /// Initializes a Global property wrapper, wrapping any type that conforms 37 | /// to `GloballyIdentifiable`. 38 | /// 39 | /// See the documentation in the README for example usage. 40 | public init(_ identifier: Wrapped.InstanceIdentifier) { 41 | self.init { container in 42 | container.resolveInstance(for: identifier) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Etcetera/Global/InstanceResolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstanceResolver.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | /// Stores a resolver block and one or more resolved instances of a given type. 10 | @MainActor @usableFromInline final class InstanceResolver { 11 | 12 | /// A single resolved instance, not keyed by any instance identifier. 13 | private var resolvedInstance: Any? 14 | 15 | /// Instances keyed by instance identifiers. 16 | private var resolvedInstances: [AnyHashable: Any] = [:] 17 | 18 | /// Initializes a resolver with an initial value. 19 | @usableFromInline init(resolvedValue: T) { 20 | resolvedInstance = resolvedValue 21 | } 22 | 23 | /// Initializes a resolver with an initial value for `identifier`. 24 | @usableFromInline init(resolvedValues: [T.InstanceIdentifier: T]) { 25 | resolvedInstances = resolvedValues 26 | } 27 | 28 | /// Initializes an empty resolver. 29 | @usableFromInline init() {} 30 | 31 | /// Resolves an instance of `T` using `GlobalContainer`. 32 | @usableFromInline func resolved(container: Container) -> T { 33 | if let existing = resolvedInstance as? T { 34 | return existing 35 | } else { 36 | let new = T.make(container: container) 37 | resolvedInstance = new 38 | return new 39 | } 40 | } 41 | 42 | /// Resolves a specific instance of `T` for a given identifier. 43 | @usableFromInline func resolved(container: Container, identifier: T.InstanceIdentifier) -> T { 44 | let key = AnyHashable(identifier) 45 | if let existing = resolvedInstances[key] as? T { 46 | return existing 47 | } else { 48 | let new = T.make(container: container, identifier: identifier) 49 | resolvedInstances[key] = new 50 | return new 51 | } 52 | } 53 | 54 | /// Resolves an instance of `T` using `GlobalContainer`. 55 | @usableFromInline func resolved(via initializer: @MainActor (Container) -> T, container: Container) -> T { 56 | if let existing = resolvedInstance as? T { 57 | return existing 58 | } else { 59 | let new = initializer(container) 60 | resolvedInstance = new 61 | return new 62 | } 63 | } 64 | 65 | /// Resolves an instance of `T` using `GlobalContainer`. 66 | @usableFromInline func resolved(for identifier: InstanceIdentifier, via initializer: @MainActor (Container) -> T, container: Container) -> T { 67 | let key = AnyHashable(identifier) 68 | if let existing = resolvedInstances[key] as? T { 69 | return existing 70 | } else { 71 | let new = initializer(container) 72 | resolvedInstances[key] = new 73 | return new 74 | } 75 | } 76 | 77 | /// Removes a previously-resolved instance for `identifier`, if any. 78 | @usableFromInline func removeInstance(of type: T.Type, for identifier: T.InstanceIdentifier) { 79 | let key = AnyHashable(identifier) 80 | resolvedInstances[key] = nil 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Etcetera/Networking/Reachability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reachability.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SystemConfiguration 11 | 12 | extension Notification.Name { 13 | public static let ReachabilityChanged = Notification.Name(rawValue: "ReachabilityChanged") 14 | } 15 | 16 | /// A class that reports whether or not the network is currently reachable. 17 | public final class Reachability: NSObject, Sendable { 18 | 19 | /// A more accurate alternative to using a Bool 20 | public enum Status { 21 | case probablyNotButWhoKnows 22 | case itWorkedThatOneTimeRecently 23 | } 24 | 25 | /// Shared instance. You're not obligated to use this. 26 | public static let shared = Reachability() 27 | 28 | /// Synchronous evaluation of the current flags using the shared instance. 29 | public static var status: Status { 30 | return shared.status 31 | } 32 | 33 | /// Synchronous evaluation of the current flags. 34 | public var status: Status { 35 | if let flags = flags, flags.contains(.reachable) { 36 | if flags.isDisjoint(with: [.connectionRequired, .interventionRequired]) { 37 | return .itWorkedThatOneTimeRecently 38 | } 39 | } 40 | return .probablyNotButWhoKnows 41 | } 42 | 43 | private let reachability: Protected 44 | private let _flags = Protected() 45 | 46 | private var flags: SCNetworkReachabilityFlags? { 47 | get { 48 | _flags.current 49 | } 50 | set { 51 | _flags.current = newValue 52 | NotificationCenter.default.post(name: .ReachabilityChanged, object: nil) 53 | } 54 | } 55 | 56 | public init(host: String = "www.google.com") { 57 | let optionalReachability = SCNetworkReachabilityCreateWithName(nil, host) 58 | self.reachability = Protected(optionalReachability) 59 | super.init() 60 | guard let reachability = optionalReachability else { return } 61 | 62 | // Populate the current flags asap. 63 | var flags = SCNetworkReachabilityFlags() 64 | SCNetworkReachabilityGetFlags(reachability, &flags) 65 | self.flags = flags 66 | 67 | // Then configure the callback. 68 | let callback: SCNetworkReachabilityCallBack = { (_, flags, infoPtr) in 69 | guard let info = infoPtr else { return } 70 | let this = Unmanaged.fromOpaque(info).takeUnretainedValue() 71 | this.flags = flags 72 | } 73 | let selfPtr = Unmanaged.passUnretained(self).toOpaque() 74 | var context = SCNetworkReachabilityContext( 75 | version: 0, 76 | info: selfPtr, 77 | retain: nil, 78 | release: nil, 79 | copyDescription: nil 80 | ) 81 | SCNetworkReachabilitySetCallback(reachability, callback, &context) 82 | SCNetworkReachabilitySetDispatchQueue(reachability, .main) 83 | } 84 | 85 | } 86 | 87 | extension Reachability.Status: CustomDebugStringConvertible { 88 | 89 | public var debugDescription: String { 90 | switch self { 91 | case .itWorkedThatOneTimeRecently: return ".itWorkedThatOneTimeRecently" 92 | case .probablyNotButWhoKnows: return ".probablyNotButWhoKnows" 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Etcetera/Swift/Collections.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collections.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | // swiftlint:disable line_length - I dislike multi-line function signatures. 9 | 10 | /// Quality-of-life extension of RandomAccessCollection. 11 | extension RandomAccessCollection { 12 | 13 | /// A flavor of subscripting that returns an optional element if the index 14 | /// is out-of-bounds. 15 | public subscript(optional index: Index) -> Element? { 16 | return indices.contains(index) ? self[index] : nil 17 | } 18 | 19 | } 20 | 21 | /// Quality-of-life extension of RangeReplaceableCollection. 22 | extension RangeReplaceableCollection where Element : AnyObject { 23 | 24 | /// Removes the first element that has the same identity as `element`. 25 | public mutating func removeFirstInstance(of element: Element) { 26 | if let index = firstIndex(where: { $0 === element }) { 27 | remove(at: index) 28 | } 29 | } 30 | 31 | } 32 | 33 | /// Quality-of-life extension of Sequence. 34 | public extension Sequence { 35 | 36 | /// Maps the receiver to a dictionary using a block to generate keys. 37 | /// 38 | /// - parameter block: A block that returns the key for a given element. 39 | /// 40 | /// - parameter firstWins: If `false` the last key/value pair overwrites any 41 | /// previous entries with the same key. 42 | /// 43 | /// - returns: Returns a dictionary of elements using the chosen keys. 44 | func map(usingKeysFrom block: (Element) -> Key, firstWins: Bool = false) -> [Key: Element] { 45 | let keyValues: [(Key, Element)] = map { (block($0), $0) } 46 | return Dictionary(keyValues, uniquingKeysWith: { first, last in firstWins ? first : last }) 47 | } 48 | 49 | /// Maps the receiver to a dictionary using key paths to generate keys. 50 | /// 51 | /// - parameter keyPath: The keyPath to the key for a given element. 52 | /// 53 | /// - parameter firstWins: If `false` the last key/value pair overwrites any 54 | /// previous entries with the same key. 55 | /// 56 | /// - returns: Returns a dictionary of elements using the chosen keys. 57 | func map(using keyPath: KeyPath, firstWins: Bool = false) -> [Key: Element] { 58 | let keyValues: [(Key, Element)] = map { ($0[keyPath: keyPath], $0) } 59 | return Dictionary(keyValues, uniquingKeysWith: { first, last in firstWins ? first : last }) 60 | } 61 | 62 | } 63 | 64 | /// Quality-of-life extension of Swift.Dictionary. 65 | extension Dictionary { 66 | 67 | /// Initializes a new dictionary with the elements of a sequence, creating 68 | /// keys via a block argument. 69 | /// 70 | /// - parameter sequence: The source sequence of elements. 71 | /// 72 | /// - parameter block: A block which can generate a key from any given 73 | /// element in `sequence`. 74 | /// 75 | /// - parameter firstWins: If `false`, the last key/value pair overwrites 76 | /// any previous pair for the same key. 77 | public init(_ sequence: S, usingKeysFrom block: (Value) -> Key, firstWins: Bool = false) where S.Element == Value { 78 | self = sequence.map(usingKeysFrom: block, firstWins: firstWins) 79 | } 80 | 81 | public init(_ sequence: S, using keyPath: KeyPath, firstWins: Bool = false) where S.Element == Value { 82 | self = sequence.map(using: keyPath, firstWins: firstWins) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Etcetera/Swift/Sequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | extension Sequence { 10 | 11 | public func map(_ keyPath: KeyPath) -> [T] { 12 | return map { $0[keyPath: keyPath] } 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Etcetera/UIKit/CGFloat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGFloat { 12 | 13 | public var half: CGFloat { self * 0.5 } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Etcetera/UIKit/UIApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIApplication { 12 | 13 | /// - returns: Returns `true` if the application is running during unit tests. 14 | public var isRunningFromTests: Bool { 15 | return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Etcetera/UIKit/UIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | // swiftlint:disable force_cast 9 | 10 | import UIKit 11 | 12 | extension UIView { 13 | 14 | public static func newFromNib() -> T { 15 | let name = String(describing: T.self) 16 | let nib = UINib(nibName: name, bundle: nil) 17 | let array = nib.instantiate(withOwner: nil) 18 | return array[0] as! T 19 | } 20 | 21 | public func constrainToFill() { 22 | autoresizingMask = [.flexibleWidth, .flexibleHeight] 23 | translatesAutoresizingMaskIntoConstraints = true 24 | } 25 | 26 | public func constrain(to view: UIView, insetBy insets: UIEdgeInsets = .zero) { 27 | leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left).isActive = true 28 | topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top).isActive = true 29 | trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -insets.right).isActive = true 30 | bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -insets.bottom).isActive = true 31 | } 32 | 33 | public func addConstrainedSubview(_ subview: UIView, insetBy insets: UIEdgeInsets = .zero) { 34 | subview.translatesAutoresizingMaskIntoConstraints = false 35 | addSubview(subview) 36 | subview.frame = { 37 | var frame = self.bounds 38 | frame.origin.y += insets.top 39 | frame.origin.x += insets.left 40 | frame.size.width -= (insets.left + insets.right) 41 | frame.size.height -= (insets.top + insets.bottom) 42 | return frame 43 | }() 44 | subview.constrain(to: self, insetBy: insets) 45 | } 46 | 47 | public func addCenteredSubview(_ subview: UIView, size: CGSize) { 48 | subview.translatesAutoresizingMaskIntoConstraints = false 49 | addSubview(subview) 50 | subview.widthAnchor.constraint(equalToConstant: size.width).isActive = true 51 | subview.heightAnchor.constraint(equalToConstant: size.height).isActive = true 52 | center(subview) 53 | } 54 | 55 | public func center(_ subview: UIView) { 56 | centerXAnchor.constraint(equalTo: subview.centerXAnchor).isActive = true 57 | centerYAnchor.constraint(equalTo: subview.centerYAnchor).isActive = true 58 | } 59 | 60 | public func roundCornersToMatchSuperllipticalDisplayCorners() { 61 | var key = NSString(format: "%@%@%@", "display", "Corner", "Radius") 62 | guard let value = UIScreen.main.traitCollection.value(forKey: key as String) else { 63 | roundCorners(toRadius: 39) 64 | return 65 | } 66 | guard let radius = value as? CGFloat else { 67 | roundCorners(toRadius: 39) 68 | return 69 | } 70 | layer.cornerRadius = radius 71 | key = NSString(format: "%@%@", "continuous", "Corners") 72 | layer.setValue(true, forKey: key as String) 73 | clipsToBounds = true 74 | } 75 | 76 | public func roundCorners(toRadius radius: CGFloat) { 77 | layer.cornerRadius = radius 78 | clipsToBounds = true 79 | } 80 | 81 | } 82 | 83 | extension CGRect { 84 | 85 | public func outset(by offset: CGFloat) -> CGRect { 86 | return insetBy(dx: -offset, dy: -offset) 87 | } 88 | 89 | } 90 | 91 | extension UIView { 92 | 93 | public var transformSafeFrame: CGRect { 94 | let left = self.left 95 | let top = self.top 96 | let width = self.width 97 | let height = self.height 98 | return CGRect(x: left, y: top, width: width, height: height) 99 | } 100 | 101 | public var top: CGFloat { 102 | get { 103 | return self.center.y - self.halfHeight 104 | } 105 | set { 106 | var center = self.center 107 | center.y = newValue + self.halfHeight 108 | self.center = center 109 | } 110 | } 111 | 112 | public var left: CGFloat { 113 | get { 114 | return self.center.x - self.halfWidth 115 | } 116 | set { 117 | var center = self.center 118 | center.x = newValue + self.halfWidth 119 | self.center = center 120 | } 121 | } 122 | 123 | public var bottom: CGFloat { 124 | get { 125 | return self.center.y + self.halfHeight 126 | } 127 | set { 128 | var center = self.center 129 | center.y = newValue - self.halfHeight 130 | self.center = center 131 | } 132 | } 133 | 134 | public var right: CGFloat { 135 | get { 136 | return self.center.x + self.halfWidth 137 | } 138 | set { 139 | var center = self.center 140 | center.x = newValue - self.halfWidth 141 | self.center = center 142 | } 143 | } 144 | 145 | public var height: CGFloat { 146 | get { 147 | return self.bounds.height 148 | } 149 | set { 150 | var bounds = self.bounds 151 | let previousHeight = bounds.height 152 | bounds.size.height = newValue 153 | self.bounds = bounds 154 | 155 | let delta = previousHeight - newValue 156 | var center = self.center 157 | center.y += delta / 2.0 158 | self.center = center 159 | } 160 | } 161 | 162 | public var width: CGFloat { 163 | get { 164 | return self.bounds.width 165 | } 166 | set { 167 | var bounds = self.bounds 168 | let previousWidth = bounds.width 169 | bounds.size.width = newValue 170 | self.bounds = bounds 171 | 172 | let delta = previousWidth - newValue 173 | var center = self.center 174 | center.x += delta / 2.0 175 | self.center = center 176 | } 177 | } 178 | 179 | public var internalCenter: CGPoint { 180 | return CGPoint(x: self.halfWidth, y: self.halfHeight) 181 | } 182 | 183 | // MARK: Private 184 | 185 | private var halfHeight: CGFloat { 186 | return self.bounds.height / 2.0 187 | } 188 | 189 | private var halfWidth: CGFloat { 190 | return self.bounds.width / 2.0 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /Sources/Etcetera/UIKit/UIViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | // swiftlint:disable force_cast 9 | 10 | import UIKit 11 | import SwiftUI 12 | 13 | extension UIViewController { 14 | 15 | public func addHostedChild(for root: Root) { 16 | let host = UIHostingController(rootView: root) 17 | addConstrainedChild(host) 18 | } 19 | 20 | public func addConstrainedChild(_ child: UIViewController) { 21 | addChild(child) 22 | view.addConstrainedSubview(child.view) 23 | child.didMove(toParent: self) 24 | } 25 | 26 | public static func instantiateFromStoryboard(of type: T.Type = T.self) -> T { 27 | let identifier = String(describing: type) 28 | let storyboard = UIStoryboard(name: identifier, bundle: nil) 29 | let viewController = storyboard.instantiateViewController(withIdentifier: identifier) 30 | return viewController as! T 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Etcetera/UnifiedLogging/OSActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSActivity.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | // swiftlint:disable line_length - I dislike multi-line function signatures. 9 | 10 | import os.activity 11 | 12 | /// Swift-native, quality-of-life wrapper around os.activity. 13 | /// 14 | /// ## Recommended Usage 15 | /// 16 | /// It is recommended that you do **not** re-use an instance of a given 17 | /// Activity across more than one activity "session", where "session" is 18 | /// understood within the domain knowledge of your application. Rather, 19 | /// initialize a new activity at the start of a discrete unit of that 20 | /// activity, `enter()`-ing the activity at the beginning, and performing 21 | /// the `Leave` block at the end: 22 | /// 23 | /// extension Activity { 24 | /// static func processImage() -> Activity { 25 | /// return Activity("ImageLib processImage") 26 | /// } 27 | /// } 28 | /// 29 | /// func process(_ image: Image) -> Image { 30 | /// let leave = Activity.processImage().enter() 31 | /// defer { leave() } 32 | /// // process the image... 33 | /// return result 34 | /// } 35 | public struct Activity: @unchecked Sendable { 36 | 37 | // MARK: - Typealiases 38 | 39 | /// The signature of the closure returned from `enter()`. 40 | public typealias Leave = @Sendable () -> Void 41 | 42 | // MARK: - Nested Types 43 | 44 | /// Swift-native type that corresponds to OS_ACTIVITY_FLAGs. 45 | public struct Options: OptionSet, Sendable { 46 | public let rawValue: UInt32 47 | 48 | public init(rawValue: UInt32) { 49 | self.rawValue = rawValue 50 | } 51 | 52 | public var flagValue: os_activity_flag_t { os_activity_flag_t(rawValue) } 53 | 54 | /// Equivalent to `OS_ACTIVITY_FLAG_DETACHED`. 55 | public static let detached = Options(rawValue: OS_ACTIVITY_FLAG_DETACHED.rawValue) 56 | 57 | /// Equivalent to `OS_ACTIVITY_FLAG_IF_NONE_PRESENT`. 58 | public static let ifNonePresent = Options(rawValue: OS_ACTIVITY_FLAG_IF_NONE_PRESENT.rawValue) 59 | } 60 | 61 | // MARK: - Private Properties 62 | 63 | /// The underlying activity. 64 | private let reference: os_activity_t 65 | 66 | // MARK: - Init/Factory 67 | 68 | /// Initializes a new activity. 69 | /// 70 | /// - parameter label: The label to use when creating the underlying activity. 71 | /// 72 | /// - parameter parent: The parent activity to which the new underlying 73 | /// activity will be related. Defaults to `os_activity_current()`. 74 | /// 75 | /// - parameter options: The flags to use when creating the underlying 76 | /// activity. Consult the os.activity documentation for more information 77 | /// about the usage of these flags. 78 | /// 79 | /// - parameter dso: A `__dso_handle` from the calling module. There is no 80 | /// reason for you to override the default argument, as passing the 81 | /// incorrect handle will prevent activity information from appearing in the 82 | /// the console output. The default argument is, per the Swift compiler's 83 | /// fundamentals, always defined using the value from the caller's context, 84 | /// not the callee's context. 85 | public init(_ label: StaticString, parent: Activity = .current(), options: Options = [], dso: UnsafeRawPointer = #dsohandle) { 86 | self.init(label.withUTF8Buffer { buffer -> os_activity_t in 87 | guard let base = buffer.baseAddress else { 88 | fatalError("Unable to acquire a buffer pointer to the `label`.") 89 | } 90 | return base.withMemoryRebound(to: Int8.self, capacity: buffer.count, { pointer in 91 | _os_activity_create(.init(mutating: dso), pointer, parent.reference, options.flagValue) 92 | }) 93 | }) 94 | } 95 | 96 | /// - returns: Returns a new Activity instance wrapping `OS_ACTIVITY_NONE`. 97 | public static func none() -> Activity { 98 | let OS_ACTIVITY_NONE = unsafeBitCast(dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_os_activity_none"), to: Unmanaged.self) 99 | return Activity(OS_ACTIVITY_NONE.takeUnretainedValue()) 100 | } 101 | 102 | /// - returns: Returns a new Activity instance wrapping `OS_ACTIVITY_CURRENT`. 103 | public static func current() -> Activity { 104 | let OS_ACTIVITY_CURRENT = unsafeBitCast(dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_os_activity_current"), to: Unmanaged.self) 105 | return Activity(OS_ACTIVITY_CURRENT.takeUnretainedValue()) 106 | } 107 | 108 | /// Designated initializer, wrapping the underlying activity. 109 | /// 110 | /// - parameter reference: The underyling activity. 111 | private init(_ reference: os_activity_t) { 112 | self.reference = reference 113 | } 114 | 115 | // MARK: - Public Methods 116 | 117 | /// Label an activity that is auto-generated by AppKit/UIKit with a name 118 | /// that is useful for debugging macro-level user actions. 119 | /// 120 | /// Effectively a proxy for `os_activity_label_useraction`. See the docs 121 | /// for that function for more information on appropriate usage, since the 122 | /// same guidelines generally apply to this method. 123 | /// 124 | /// - parameter userAction: A static string like "share button pressed". 125 | /// 126 | /// - parameter dso: A `__dso_handle` from the calling module. There is no 127 | /// reason for you to override the default argument, as passing the 128 | /// incorrect handle will prevent activity information from appearing in the 129 | /// the console output. The default argument is, per the Swift compiler's 130 | /// fundamentals, always defined using the value from the caller's context, 131 | /// not the callee's context. 132 | public static func labelUserAction(_ userAction: StaticString, fromContainingBinary dso: UnsafeRawPointer = #dsohandle) { 133 | userAction.withUTF8Buffer { buffer in 134 | guard let base = buffer.baseAddress else { 135 | fatalError("Unable to acquire a buffer pointer to the `label`.") 136 | } 137 | base.withMemoryRebound(to: Int8.self, capacity: buffer.count, { pointer in 138 | _os_activity_label_useraction(.init(mutating: dso), pointer) 139 | }) 140 | } 141 | } 142 | 143 | /// Enters an activity. 144 | /// 145 | /// This method **must** be balanced by performing the `Leave` closure: 146 | /// 147 | /// let leave = myActivity.enter() 148 | /// // do things... 149 | /// leave() 150 | /// 151 | /// - returns: Returns a closure that leaves the activity. 152 | public func enter() -> Leave { 153 | let scope = ActivityScope(reference) 154 | scope.enter() 155 | return { 156 | scope.leave() 157 | } 158 | } 159 | 160 | } 161 | 162 | private final class ActivityScope: @unchecked Sendable { 163 | 164 | private let reference: os_activity_t 165 | private let state = UnsafeMutablePointer.allocate(capacity: 1) 166 | 167 | init(_ reference: os_activity_t) { 168 | self.reference = reference 169 | state.initialize(to: os_activity_scope_state_s()) 170 | } 171 | 172 | deinit { 173 | state.deinitialize(count: 1) 174 | state.deallocate() 175 | } 176 | 177 | func enter() { 178 | os_activity_scope_enter(reference, state) 179 | } 180 | 181 | func leave() { 182 | os_activity_scope_leave(state) 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /Sources/Etcetera/UnifiedLogging/OSLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSLog.swift 3 | // Etcetera 4 | // 5 | // Created by Jared Sinclair on 8/15/15. 6 | // Copyright © 2015 Nice Boy LLC. All rights reserved. 7 | // 8 | // swiftlint:disable file_length - This is intentionally a one-file, a drop-in. 9 | // swiftlint:disable identifier_name - Clarity! 10 | // swiftlint:disable line_length - I dislike multi-line function signatures. 11 | // swiftlint:disable function_parameter_count - Some problems have lots o' variables. 12 | 13 | import Foundation 14 | import os.log 15 | 16 | /// Quality-of-life extension of OSLog. 17 | /// 18 | /// ## Initial Setup 19 | /// 20 | /// First initialize an OSLog instance, ideally for reuse across all applicable 21 | /// logging scenarios: 22 | /// 23 | /// static let MyAppLog = OSLog(subsystem: "com.domain.MyApp", category: "App") 24 | /// 25 | /// In lieu of any official naming guidelines, here are my recommendations: 26 | /// 27 | /// - Your subsystem should use reverse-dns style `com.domain.Reversed`. 28 | /// 29 | /// - If your app is broken up into frameworks, include the framework name in 30 | /// the subsystem: "com.domain.MyAppLib" or "com.domain.MyApp.MyLib", etc. 31 | /// 32 | /// - Use the category name to describe the general subject matter of the code. 33 | /// This may or may not cut across subsystems. For example, if you are logging 34 | /// all of your CoreData errors, it might be a good idea to use "CoreData" as 35 | /// the category. The category name will appear in [Brackets] in the console 36 | /// output inside Xcode, so keep it short and human-readable. Camel-casing or 37 | /// word capitalization is preferable. 38 | /// 39 | /// ## General Usage 40 | /// 41 | /// Use the extension methods below wherever you would otherwise use an 42 | /// `os_log()` call or — gasp — a `print()` call: 43 | /// 44 | /// MyAppLog.log("Something happened.") 45 | /// MyAppLog.debug("For British eyes only.") 46 | /// MyAppLog.info("Red leather, yellow leather.") 47 | /// MyAppLog.error("Uh oh.") 48 | /// MyAppLog.fault("It's not my...") 49 | /// 50 | /// ## Privacy 51 | /// 52 | /// Use caution when logging. You don't want to accidentally reveal your app's 53 | /// secrets in log output. You can control the level of security used via the 54 | /// `Privacy` type below. The `.redacted` level will redact content besides 55 | /// static strings and scalar values (see note below for a caveat). The 56 | /// `.visible` level will not redact any logged content. 57 | /// 58 | /// - Configure the security privacy on an app-wide basis by setting the value 59 | /// of Options.defaultPrivacy. The default for release builds is `.redacted`. 60 | /// For debug builds, it defaults to `.visible`. 61 | /// 62 | /// - Configure the security privacy on a per-call basis by overriding the 63 | /// default argument passed to one of the methods below. 64 | /// 65 | /// Using the `.redacted` privacy level is not necessarily enough to redact 66 | /// sensitive console output. Additional environmental configuration may allow 67 | /// sensitive content to appear in plain text, such as when the application is 68 | /// connected to a debugger. 69 | /// 70 | /// - SeeAlso: https://developer.apple.com/documentation/os/logging 71 | /// 72 | /// ## Source Location 73 | /// 74 | /// When called from Objective-C code, os_log will display the caller's source 75 | /// location (function, file, and line number) in the console. Not so with 76 | /// Swift. As a stopgap until Apple fixes this deficiency, you can optionally 77 | /// enable source location when using the "foo(value:" logging methods below: 78 | /// 79 | /// - Configure the setting on an app-wide basis by setting the value of 80 | /// `Options.includeSourceLocationInValueLogs` to `true`. For debug builds, 81 | /// the default is `true` because nothing is worse than noisy, disembodied 82 | /// console logs while debugging. 83 | /// 84 | /// - Configure the setting on a per-call basis by overriding the default 85 | /// argument passed to the logging method. 86 | /// 87 | /// - Note: The `foo(format:args:)` logging methods below will not include any 88 | /// source location info unless you have included it manually in your format 89 | /// string and arguments. 90 | /// 91 | /// ## Customize Log Output 92 | /// 93 | /// If you have a type whose log output needs special finessing, extend it to 94 | /// conform to `CustomLogRepresentable`. See below for documentation. 95 | extension OSLog { 96 | 97 | // MARK: - Nested Types 98 | 99 | /// The default arguments passed to the methods below. 100 | public enum Options { 101 | 102 | /// The default Privacy setting to use. 103 | public static var defaultPrivacy: Privacy { 104 | get { _defaultPrivacy.current } 105 | set { _defaultPrivacy.current = newValue } 106 | } 107 | 108 | /// Backing for `defaultPrivacy`. 109 | private static let _defaultPrivacy = Protected(.preferredDefault) 110 | 111 | /// If `true`, the function, file, and line number will be included in 112 | /// messages logged using the "foo(value:)" log methods below. 113 | public static var includeSourceLocationInValueLogs: Bool { 114 | get { _includeSourceLocationInValueLogs.current } 115 | set { _includeSourceLocationInValueLogs.current = newValue } 116 | } 117 | 118 | /// Backing for `includeSourceLocationInValueLogs`. 119 | private static let _includeSourceLocationInValueLogs = Protected({ 120 | #if DEBUG 121 | return true 122 | #else 123 | return false 124 | #endif 125 | }()) 126 | 127 | } 128 | 129 | /// Controls whether log message content is redacted or visible. 130 | public enum Privacy { 131 | 132 | /// No values will be redacted. 133 | case visible 134 | 135 | /// Values besides static strings and scalars will be redacted. 136 | case redacted 137 | } 138 | 139 | // MARK: - Default 140 | 141 | /// Logs a developer-formatted message using the `.default` type. 142 | /// 143 | /// The caller is responsible for including public/private formatting in the 144 | /// format string, as well as any source location info (line number, etc.). 145 | /// 146 | /// - parameter format: A C-style format string. 147 | /// 148 | /// - parameter args: A list of arguments to the format string (if any). 149 | @inlinable 150 | public func log(format: StaticString, args: CVarArg...) { 151 | // Use the `(format:array:)` variant to prevent the compiler from 152 | // wrapping a single argument in an array it thinks you implied. 153 | let representation = LogMessage(format: format, array: args) 154 | _etcetera_log(representation: representation, type: .default) 155 | } 156 | 157 | /// Logs a message using the `.default` type. 158 | /// 159 | /// - parameter value: The value to be logged. If the value does not already 160 | /// conform to CustomLogRepresentable, a default implementation will used. 161 | @inlinable 162 | public func log(_ value: Any, privacy: Privacy = Options.defaultPrivacy, includeSourceLocation: Bool = Options.includeSourceLocationInValueLogs, file: String = #file, function: String = #function, line: Int = #line) { 163 | _etcetera_log(value: value, privacy: privacy, includeSourceLocation: includeSourceLocation, file: file, function: function, line: line, type: .default) 164 | } 165 | 166 | // MARK: - Info 167 | 168 | /// Logs a developer-formatted message using the `.info` type. 169 | /// 170 | /// The caller is responsible for including public/private formatting in the 171 | /// format string, as well as any source location info (line number, etc.). 172 | /// 173 | /// - parameter format: A C-style format string. 174 | /// 175 | /// - parameter args: A list of arguments to the format string (if any). 176 | @inlinable 177 | public func info(format: StaticString, args: CVarArg...) { 178 | // Use the `(format:array:)` variant to prevent the compiler from 179 | // wrapping a single argument in an array it thinks you implied. 180 | let representation = LogMessage(format: format, array: args) 181 | #if targetEnvironment(simulator) 182 | // @workaround for simulator bug in Xcode 10.2 and earlier: 183 | // https://forums.developer.apple.com/thread/82736#348090 184 | let type = OSLogType.default 185 | #else 186 | let type = OSLogType.info 187 | #endif 188 | _etcetera_log(representation: representation, type: type) 189 | } 190 | 191 | /// Logs a message using the `.info` type. 192 | /// 193 | /// - parameter value: The value to be logged. If the value does not already 194 | /// conform to CustomLogRepresentable, a default implementation will used. 195 | @inlinable 196 | public func info(_ value: Any, privacy: Privacy = Options.defaultPrivacy, includeSourceLocation: Bool = Options.includeSourceLocationInValueLogs, file: String = #file, function: String = #function, line: Int = #line) { 197 | #if targetEnvironment(simulator) 198 | // @workaround for simulator bug in Xcode 10.2 and earlier: 199 | // https://forums.developer.apple.com/thread/82736#348090 200 | let type = OSLogType.default 201 | #else 202 | let type = OSLogType.info 203 | #endif 204 | _etcetera_log(value: value, privacy: privacy, includeSourceLocation: includeSourceLocation, file: file, function: function, line: line, type: type) 205 | } 206 | 207 | // MARK: - Debug 208 | 209 | /// Logs the source location of the call site using the `.debug` type. 210 | @inlinable 211 | public func trace(file: String = #file, function: String = #function, line: Int = #line) { 212 | #if targetEnvironment(simulator) 213 | // @workaround for simulator bug in Xcode 10.2 and earlier: 214 | // https://forums.developer.apple.com/thread/82736#348090 215 | let type = OSLogType.default 216 | #else 217 | let type = OSLogType.debug 218 | #endif 219 | _etcetera_log(value: "", privacy: .visible, includeSourceLocation: true, file: file, function: function, line: line, type: type) 220 | } 221 | 222 | /// Logs a developer-formatted message using the `.debug` type. 223 | /// 224 | /// The caller is responsible for including public/private formatting in the 225 | /// format string, as well as any source location info (line number, etc.). 226 | /// 227 | /// - parameter format: A C-style format string. 228 | /// 229 | /// - parameter args: A list of arguments to the format string (if any). 230 | @inlinable 231 | public func debug(format: StaticString, args: CVarArg...) { 232 | // Use the `(format:array:)` variant to prevent the compiler from 233 | // wrapping a single argument in an array it thinks you implied. 234 | let representation = LogMessage(format: format, array: args) 235 | #if targetEnvironment(simulator) 236 | // @workaround for simulator bug in Xcode 10.2 and earlier: 237 | // https://forums.developer.apple.com/thread/82736#348090 238 | let type = OSLogType.default 239 | #else 240 | let type = OSLogType.debug 241 | #endif 242 | _etcetera_log(representation: representation, type: type) 243 | } 244 | 245 | /// Logs a message using the `.debug` type. 246 | /// 247 | /// - parameter value: The value to be logged. If the value does not already 248 | /// conform to CustomLogRepresentable, a default implementation will used. 249 | @inlinable 250 | public func debug(_ value: Any, privacy: Privacy = Options.defaultPrivacy, includeSourceLocation: Bool = Options.includeSourceLocationInValueLogs, file: String = #file, function: String = #function, line: Int = #line) { 251 | #if targetEnvironment(simulator) 252 | // @workaround for simulator bug in Xcode 10.2 and earlier: 253 | // https://forums.developer.apple.com/thread/82736#348090 254 | let type = OSLogType.default 255 | #else 256 | let type = OSLogType.debug 257 | #endif 258 | _etcetera_log(value: value, privacy: privacy, includeSourceLocation: includeSourceLocation, file: file, function: function, line: line, type: type) 259 | } 260 | 261 | // MARK: - Error 262 | 263 | /// Logs a developer-formatted message using the `.error` type. 264 | /// 265 | /// The caller is responsible for including public/private formatting in the 266 | /// format string, as well as any source location info (line number, etc.). 267 | /// 268 | /// - parameter format: A C-style format string. 269 | /// 270 | /// - parameter args: A list of arguments to the format string (if any). 271 | @inlinable 272 | public func error(format: StaticString, args: CVarArg...) { 273 | // Use the `(format:array:)` variant to prevent the compiler from 274 | // wrapping a single argument in an array it thinks you implied. 275 | let representation = LogMessage(format: format, array: args) 276 | _etcetera_log(representation: representation, type: .error) 277 | } 278 | 279 | /// Logs a message using the `.error` type. 280 | /// 281 | /// - parameter value: The value to be logged. If the value does not already 282 | /// conform to CustomLogRepresentable, a default implementation will used. 283 | @inlinable 284 | public func error(_ value: Any, privacy: Privacy = Options.defaultPrivacy, includeSourceLocation: Bool = Options.includeSourceLocationInValueLogs, file: String = #file, function: String = #function, line: Int = #line) { 285 | _etcetera_log(value: value, privacy: privacy, includeSourceLocation: includeSourceLocation, file: file, function: function, line: line, type: .error) 286 | } 287 | 288 | // MARK: - Fault 289 | 290 | /// Logs a developer-formatted message using the `.fault` type. 291 | /// 292 | /// The caller is responsible for including public/private formatting in the 293 | /// format string, as well as any source location info (line number, etc.). 294 | /// 295 | /// - parameter format: A C-style format string. 296 | /// 297 | /// - parameter args: A list of arguments to the format string (if any). 298 | @inlinable 299 | public func fault(format: StaticString, args: CVarArg...) { 300 | // Use the `(format:array:)` variant to prevent the compiler from 301 | // wrapping a single argument in an array it thinks you implied. 302 | let representation = LogMessage(format: format, array: args) 303 | _etcetera_log(representation: representation, type: .fault) 304 | } 305 | 306 | /// Logs a message using the `.fault` type. 307 | /// 308 | /// - parameter value: The value to be logged. If the value does not already 309 | /// conform to CustomLogRepresentable, a default implementation will used. 310 | @inlinable 311 | public func fault(_ value: Any, privacy: Privacy = Options.defaultPrivacy, includeSourceLocation: Bool = Options.includeSourceLocationInValueLogs, file: String = #file, function: String = #function, line: Int = #line) { 312 | _etcetera_log(value: value, privacy: privacy, includeSourceLocation: includeSourceLocation, file: file, function: function, line: line, type: .fault) 313 | } 314 | 315 | // MARK: - Internal 316 | 317 | @usableFromInline 318 | internal func _etcetera_log(value: Any, privacy: Privacy, includeSourceLocation: Bool, file: String, function: String, line: Int, type: OSLogType) { 319 | let loggable = (value as? CustomLogRepresentable) ?? AnyLoggable(value) 320 | let representation = loggable.logRepresentation(includeSourceLocation: includeSourceLocation, privacy: privacy, file: file, function: function, line: line) 321 | _etcetera_log(representation: representation, type: type) 322 | } 323 | 324 | @usableFromInline 325 | internal func _etcetera_log(representation: LogMessage, type: OSLogType) { 326 | // http://www.openradar.me/33203955 327 | // Sigh... 328 | // or should I say 329 | // sigh, sigh, sigh, sigh, sigh, sigh, sigh, sigh, sigh 330 | let f = representation.format 331 | let a = representation.args 332 | switch a.count { 333 | case 9: os_log(f, log: self, type: type, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]) 334 | case 8: os_log(f, log: self, type: type, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]) 335 | case 7: os_log(f, log: self, type: type, a[0], a[1], a[2], a[3], a[4], a[5], a[6]) 336 | case 6: os_log(f, log: self, type: type, a[0], a[1], a[2], a[3], a[4], a[5]) 337 | case 5: os_log(f, log: self, type: type, a[0], a[1], a[2], a[3], a[4]) 338 | case 4: os_log(f, log: self, type: type, a[0], a[1], a[2], a[3]) 339 | case 3: os_log(f, log: self, type: type, a[0], a[1], a[2]) 340 | case 2: os_log(f, log: self, type: type, a[0], a[1]) 341 | case 1: os_log(f, log: self, type: type, a[0]) 342 | default: os_log(f, log: self, type: type) 343 | } 344 | } 345 | 346 | } 347 | 348 | //------------------------------------------------------------------------------ 349 | // MARK: - CustomLogRepresentable 350 | //------------------------------------------------------------------------------ 351 | 352 | /// If you have a type whose log output needs special finessing, extend it to 353 | /// conform to `CustomLogRepresentable`. 354 | /// 355 | /// The format string you provide in a returned LogMessage will be passed 356 | /// directly into the underlying `os_log` call, so all the same privacy and 357 | /// formatting rules apply as if you had called into `os_log` yourself. 358 | /// 359 | /// ## Sample Implementation 360 | /// 361 | /// struct PorExemplo: CustomLogRepresentable { 362 | /// let id: uuid_t 363 | /// let token: String 364 | /// 365 | /// func logRepresentation(privacy: OSLog.Privacy) -> 366 | /// LogMessage { 367 | /// var id = self.id 368 | /// return withUnsafePointer(to: &id) { ptr in 369 | /// ptr.withMemoryRebound(to: UInt8.self, capacity: 370 | /// MemoryLayout.size) { bytePtr -> 371 | /// LogMessage in 372 | /// LogMessage("PorExemplo(id: %{public, 373 | /// uuid_t}.16P, token: %{private}@)", 374 | /// bytePtr, token) 375 | /// } 376 | /// } 377 | /// } 378 | /// 379 | /// func logRepresentation(privacy: OSLog.Privacy, file: 380 | /// String, function: String, line: Int) -> LogMessage 381 | /// { 382 | /// var id = self.id 383 | /// return withUnsafePointer(to: &id) { ptr in 384 | /// ptr.withMemoryRebound(to: UInt8.self, capacity: 385 | /// MemoryLayout.size) { bytePtr -> 386 | /// LogMessage in 387 | /// LogMessage("%{public}@ %{public}@ Line %ld: 388 | /// PorExemplo(id: %{public, uuid_t}.16P, 389 | /// token: %{private}@)", file, function, 390 | /// line, bytePtr, token) 391 | /// } 392 | /// } 393 | /// } 394 | /// } 395 | /// 396 | /// You can ignore the `privacy` arguments passed to the protocol methods if 397 | /// they are not applicable to your type's log message representation. 398 | /// Otherwise, your implementation should vary the format strings used based on 399 | /// the indicated privacy setting. 400 | /// 401 | /// The method variant that includes source location arguments will be called if 402 | /// the log method (or the global setting) included the option to reveal source 403 | /// location in the logs. This is a stopgap measure until OSLog supports showing 404 | /// source locations in logs originating from Swift code. 405 | public protocol CustomLogRepresentable { 406 | func logRepresentation(privacy: OSLog.Privacy) -> LogMessage 407 | func logRepresentation(privacy: OSLog.Privacy, file: String, function: String, line: Int) -> LogMessage 408 | } 409 | 410 | /// The customized representation of a type, used when configuring os_log inputs. 411 | public struct LogMessage { 412 | 413 | /// The C-style format string. 414 | public let format: StaticString 415 | 416 | /// The argument list (what you would otherwise pass as a comma-delimited 417 | /// list of variadic arguments to `os_log`). 418 | /// 419 | /// - Warning: Due to Earth's atmosphere, CustomLogRepresentable can only be 420 | /// initialized with an arg list containing up to nine items. Attempting to 421 | /// initialize with more than nine arguments will trip an assertion. 422 | public let args: [CVarArg] 423 | 424 | /// Primary Initializer 425 | public init(_ format: StaticString, _ args: CVarArg...) { 426 | assert(args.count < 10, "The Swift overlay of os_log prevents this OSLog extension from accepting an unbounded number of args.") 427 | self.format = format 428 | self.args = args 429 | } 430 | 431 | /// Convenience initializer. 432 | /// 433 | /// Use this initializer if you are forwarding a `CVarArg...` list from 434 | /// a calling Swift function and need to prevent the compiler from treating 435 | /// a single value as an implied array containing that single value, e.g. 436 | /// from an infering `Array` from a single `Int` argument. 437 | public init(format: StaticString, array args: [CVarArg]) { 438 | assert(args.count < 10, "The Swift overlay of os_log prevents this OSLog extension from accepting an unbounded number of args.") 439 | self.format = format 440 | self.args = args 441 | } 442 | 443 | } 444 | 445 | extension CustomLogRepresentable { 446 | 447 | @inlinable 448 | public func logRepresentation(privacy: OSLog.Privacy) -> LogMessage { 449 | switch privacy { 450 | case .visible: 451 | return LogMessage("%{public}@", logDescription) 452 | case .redacted: 453 | return LogMessage("%{private}@", logDescription) 454 | } 455 | } 456 | 457 | @inlinable 458 | public func logRepresentation(privacy: OSLog.Privacy, file: String, function: String, line: Int) -> LogMessage { 459 | switch privacy { 460 | case .visible: 461 | return LogMessage("%{public}@ %{public}@ Line %ld: %{public}@", file, function, line, logDescription) 462 | case .redacted: 463 | return LogMessage("%{public}@ %{public}@ Line %ld: %{private}@", file, function, line, logDescription) 464 | } 465 | } 466 | 467 | @usableFromInline 468 | func logRepresentation(includeSourceLocation: Bool, privacy: OSLog.Privacy, file: String, function: String, line: Int) -> LogMessage { 469 | if includeSourceLocation { 470 | let filename = file.split(separator: "/").last.flatMap { String($0) } ?? file 471 | return logRepresentation(privacy: privacy, file: filename, function: function, line: line) 472 | } else { 473 | return logRepresentation(privacy: privacy) 474 | } 475 | } 476 | 477 | @usableFromInline 478 | var logDescription: String { 479 | let value: Any = (self as? AnyLoggable)?.loggableValue ?? self 480 | if let string = value as? String { 481 | return string 482 | } 483 | #if DEBUG 484 | if let custom = value as? CustomDebugStringConvertible { 485 | return custom.debugDescription 486 | } 487 | #endif 488 | if let convertible = value as? CustomStringConvertible { 489 | return convertible.description 490 | } 491 | return "\(value)" 492 | } 493 | 494 | } 495 | 496 | private struct AnyLoggable: CustomLogRepresentable { 497 | let loggableValue: Any 498 | 499 | init(_ value: Any) { 500 | loggableValue = value 501 | } 502 | } 503 | 504 | private extension OSLog.Privacy { 505 | 506 | static var preferredDefault: OSLog.Privacy { 507 | #if DEBUG 508 | .visible 509 | #else 510 | .redacted 511 | #endif 512 | } 513 | 514 | } 515 | 516 | extension NSError: CustomLogRepresentable { } 517 | extension String: CustomLogRepresentable { } 518 | extension Bool: CustomLogRepresentable { } 519 | extension Int: CustomLogRepresentable { } 520 | extension UInt: CustomLogRepresentable { } 521 | -------------------------------------------------------------------------------- /Tests/EtceteraTests/ActivityShit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTests.swift 3 | // EtceteraTests 4 | // 5 | // Created by Jared Sinclair on 5/11/20. 6 | // 7 | 8 | import XCTest 9 | import Etcetera 10 | import os.activity 11 | 12 | final class ActivityTests: XCTestCase { 13 | 14 | func test_teenySmokeTest() { 15 | let leave = Activity("testin stuff").enter() 16 | leave() 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Tests/EtceteraTests/GlobalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalTests.swift 3 | // EtceteraTests 4 | // 5 | // Created by Jared Sinclair on 01/24/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Etcetera 11 | 12 | @MainActor class GlobalTests: XCTestCase { 13 | 14 | func testTheBasics() { 15 | let object = MyClass() 16 | XCTAssertEqual(object.colorProvider.accentColor, .blue) 17 | XCTAssertEqual(object.postsProvider.fetchSomething(), "Posts") 18 | XCTAssertEqual(object.usersProvider.fetchSomething(), "Users") 19 | XCTAssertEqual(object.roomsProvider.fetchSomething(), "Rooms") 20 | } 21 | 22 | func testThatRecursiveResolutionDoesntDeadlock() { 23 | struct SomethingNeedsAnimal { 24 | @Global() var animal: Animal 25 | } 26 | _ = SomethingNeedsAnimal() 27 | } 28 | 29 | } 30 | 31 | // MARK: - 32 | 33 | protocol ColorProviding { 34 | var accentColor: UIColor { get } 35 | } 36 | 37 | struct ProductionColorProvider: ColorProviding { 38 | var accentColor: UIColor { .red } 39 | } 40 | 41 | struct TestColorProvider: ColorProviding { 42 | var accentColor: UIColor { .blue } 43 | } 44 | 45 | extension Global where Wrapped == ColorProviding { 46 | init() { 47 | self.init { container in 48 | switch container.context { 49 | case .unitTesting, .userInterfaceTesting: 50 | return TestColorProvider() 51 | case .running, .custom: 52 | return ProductionColorProvider() 53 | } 54 | } 55 | } 56 | } 57 | 58 | // MARK: - 59 | 60 | protocol WebServiceProviding { 61 | func fetchSomething() -> String 62 | } 63 | 64 | enum Microservice: String { 65 | case users 66 | case posts 67 | case rooms 68 | } 69 | 70 | class UsersService: WebServiceProviding { 71 | func fetchSomething() -> String { "Users" } 72 | } 73 | 74 | class PostsService: WebServiceProviding { 75 | func fetchSomething() -> String { "Posts" } 76 | } 77 | 78 | class RoomsService: WebServiceProviding { 79 | func fetchSomething() -> String { "Rooms" } 80 | } 81 | 82 | extension Global where Wrapped == WebServiceProviding { 83 | init(_ microservice: Microservice) { 84 | self.init(instanceIdentifier: microservice) { _ in 85 | switch microservice { 86 | case .posts: return PostsService() 87 | case .users: return UsersService() 88 | case .rooms: return RoomsService() 89 | } 90 | } 91 | } 92 | } 93 | 94 | // MARK: - 95 | 96 | struct Water: GloballyAvailable { 97 | static func make(container: Container) -> Self { 98 | Water() 99 | } 100 | } 101 | 102 | struct Plant: GloballyAvailable { 103 | let water: Water 104 | 105 | static func make(container: Container) -> Self { 106 | Plant(water: container.resolveInstance()) 107 | } 108 | } 109 | 110 | struct Animal: GloballyAvailable { 111 | let water: Water 112 | let plant: Plant 113 | 114 | static func make(container: Container) -> Animal { 115 | Animal(water: container.resolveInstance(), plant: container.resolveInstance()) 116 | } 117 | } 118 | 119 | // MARK: - 120 | 121 | class MyClass { 122 | @Global() var colorProvider: ColorProviding 123 | @Global(.posts) var postsProvider: WebServiceProviding 124 | @Global(.users) var usersProvider: WebServiceProviding 125 | @Global(.rooms) var roomsProvider: WebServiceProviding 126 | } 127 | -------------------------------------------------------------------------------- /Tests/EtceteraTests/LockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockTests.swift 3 | // EtceteraTests 4 | // 5 | // Created by Jared Sinclair on 4/18/20. 6 | // Copyright © 2020 Nice Boy LLC. All rights reserved. 7 | // 8 | // Based on code by Peter Steinberger from: 9 | // https://gist.github.com/steipete/36350a8a60693d440954b95ea6cbbafc 10 | 11 | import os.lock 12 | import XCTest 13 | import Etcetera 14 | 15 | final class LockTests: XCTestCase { 16 | 17 | func testLock() { 18 | let lock = Lock() 19 | executeLockTest { (block) in 20 | lock.locked { 21 | block() 22 | } 23 | } 24 | } 25 | 26 | private func executeLockTest(performLocked lockingClosure: @escaping (_ block:() -> Void) -> Void) { 27 | let dispatchBlockCount = 16 28 | let iterationCountPerBlock = 100_000 29 | let queues = [ 30 | DispatchQueue.global(qos: .userInteractive), 31 | DispatchQueue.global(qos: .default), 32 | DispatchQueue.global(qos: .utility), 33 | ] 34 | self.measure { 35 | var value = 0 // Value must be defined here because `measure` is repeated. 36 | let group = DispatchGroup() 37 | for block in 0..