├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── CodeMonkeyApple │ ├── Activity Item │ └── ActivityItem.swift │ ├── Array Builder │ ├── ArrayBuilder.swift │ └── ArrayBuilderProtocol.swift │ ├── Codable File │ └── CodableFile.swift │ ├── Conditional Value │ └── Conditional.swift │ ├── Dispatch Queue Factory │ ├── DispatchQueueFactory.swift │ └── Extensions │ │ └── Foundation │ │ ├── DispatchQueue+Constants.swift │ │ └── DispatchQueue+DispatchQueueFactory.swift │ ├── Domain Name │ ├── DomainName.swift │ ├── DomainNameProtocol.swift │ ├── Extensions │ │ └── Standard Library │ │ │ ├── Array+DomainName.swift │ │ │ └── StringProtocol+DomainName.swift │ ├── ReverseDomainName.swift │ └── Subdomain.swift │ ├── Enum Case Name │ └── EnumCaseName.swift │ ├── Extensions │ ├── AppKit │ │ └── NSImage+CodeMonkeyApple.swift │ ├── Combine │ │ ├── ObservableObject+Propagate.swift │ │ ├── Publisher+Assign.swift │ │ ├── Publisher+CodeMonkeyApple.swift │ │ └── Publisher+Propagate.swift │ ├── Core Graphics │ │ ├── CGImage+CodeMonkeyApple.swift │ │ ├── CGRect+CodeMonkeyApple.swift │ │ └── CGSize+CodeMonkeyApple.swift │ ├── Core Location │ │ ├── CLLocationCoordinate2D+Codable.swift │ │ ├── CLLocationCoordinate2D+Conversions.swift │ │ ├── CLLocationCoordinate2D+Equatable.swift │ │ ├── CLLocationCoordinate2D+Hashable.swift │ │ └── CLLocationDegrees+Conversions.swift │ ├── Foundation │ │ ├── JSONDecoder+Constants.swift │ │ ├── JSONEncoder+Constants.swift │ │ ├── NSOrderedSet+Bridge.swift │ │ ├── NSRegularExpression+CodeMonkeyApple.swift │ │ ├── NSSet+Bridge.swift │ │ ├── Thread+Number.swift │ │ ├── TimeInterval+Calendar.swift │ │ ├── TimeInterval+Constants.swift │ │ └── UserDefaults+Migrate.swift │ ├── HealthKit │ │ └── HKHealthStore+HeadKit.swift │ ├── Standard Library │ │ ├── Bool+CodeMonkeyApple.swift │ │ ├── RawRepresentable+Comparable.swift │ │ ├── Result+CodeMonkeyApple.swift │ │ ├── String+Constants.swift │ │ ├── String+SnakeCase.swift │ │ ├── Task+Background.swift │ │ └── Task+Sleep.swift │ ├── SwiftUI │ │ ├── Binding+Change.swift │ │ ├── Binding+Compose.swift │ │ ├── Binding+Unidirectional.swift │ │ ├── Button+Concurrency.swift │ │ ├── Color+Codable.swift │ │ ├── Color+Constants.swift │ │ ├── Color+Equality.swift │ │ ├── Color+Storable.swift │ │ ├── EdgeInsets+Zero.swift │ │ ├── Image+Constants.swift │ │ ├── NavigationLink+Empty.swift │ │ ├── View+AppLifecycle.swift │ │ ├── View+AppStore.swift │ │ ├── View+Concurrency.swift │ │ ├── View+Conditional.swift │ │ ├── View+Preview.swift │ │ ├── View+Share.swift │ │ ├── View+Snapshot.swift │ │ └── View+SpecificModifiers.swift │ └── UIKit │ │ ├── UIActivityViewController+CodeMonkeyApple.swift │ │ ├── UIBezierPath+HeadKit.swift │ │ ├── UIColor+Equality.swift │ │ ├── UIContentSizeCategory+Relative.swift │ │ ├── UIImage+CodeMonkeyApple.swift │ │ ├── UIScreen+iPodTouch.swift │ │ └── UIViewController+CodeMonkeyApple.swift │ ├── Frame Rate │ └── FrameRate.swift │ ├── Global │ ├── Global+Animation.swift │ ├── Global+Conditional.swift │ ├── Global+DoOnce.swift │ └── Global+Logging.swift │ ├── Haptic Feedback Generator │ ├── HapticFeedbackGenerator+Constants.swift │ ├── HapticFeedbackGenerator+StorageKey.swift │ └── HapticFeedbackGenerator.swift │ ├── Hash │ ├── DerivedHash.swift │ ├── Hash.swift │ └── PushedOriginalHash.swift │ ├── HealthKit Error │ └── HealthKitError.swift │ ├── Identified │ └── Identified.swift │ ├── Image Processing │ ├── Image Filters │ │ ├── Dither │ │ │ ├── ImageFilters+Dither+AppKit.swift │ │ │ ├── ImageFilters+Dither+UIKit.swift │ │ │ └── ImageFilters+Dither.swift │ │ └── ImageFilters.swift │ └── VectorImageError.swift │ ├── Info.swift │ ├── Linked List │ └── AsyncDoublyLinkedList.swift │ ├── Loadable │ └── Loadable.swift │ ├── Notification Center Delegate │ └── NotificationCenterDelegate.swift │ ├── Orderable │ ├── Orderable.swift │ └── OrdinalMap.swift │ ├── Property Wrappers │ ├── Build-Dependent Value │ │ └── BuildDependentValue.swift │ └── Screen-Scale-Dependent Value │ │ └── ScreenScaleDependentValue.swift │ ├── Relative Direction │ └── ZRelativeDirection.swift │ ├── Resources │ ├── String+Resources.swift │ ├── de.lproj │ │ └── Localizable.strings │ └── en.lproj │ │ └── Localizable.strings │ ├── Runtime Accent Color │ └── RuntimeAccentColor.swift │ ├── Runtime Color Scheme │ └── RuntimeColorScheme.swift │ ├── Storage Keys │ └── StorageKey+Debug.swift │ ├── Storage │ ├── CompositeStorable.swift │ ├── CompositeStorageKey.swift │ ├── DebugStorageKey.swift │ ├── Extensions │ │ └── UIContentSizeCategory+Storable.swift │ ├── Implementations │ │ ├── InMemoryStorage.swift │ │ ├── UbiquitousStorage.swift │ │ └── UserDefaultsStorage.swift │ ├── Storable.swift │ ├── StorableCodableWrapper.swift │ ├── Storage.swift │ ├── StorageKey.swift │ ├── StorageKeyBuilder.swift │ ├── StorageKeyObserver.swift │ ├── StorageKeyProtocol.swift │ ├── StoredValue.swift │ └── UserDefaultsRegistrationBuilder.swift │ ├── SwiftUI │ ├── Button Styles │ │ └── Circle Bordered Button Style │ │ │ └── CircleBorderedButtonStyle.swift │ ├── Close Button │ │ └── CloseButton.swift │ ├── Environment Values │ │ └── HasHomeIndicatorEnvironmentValue.swift │ ├── For Each With Index │ │ └── ForEachWithIndex.swift │ ├── Geometry Preference Reader │ │ ├── AppendValuePreferenceKey.swift │ │ ├── GeometryPreferenceReader.swift │ │ ├── GeometrySizePreferenceKey.swift │ │ └── Preference.swift │ ├── Grid │ │ └── Grid.swift │ ├── Multi-Column Picker │ │ └── MultiColumnPickerView.swift │ ├── Particle Emitter │ │ ├── ParticleEmitter.swift │ │ ├── ParticleEmitterCell.swift │ │ └── ParticleEmitterCellBuilder.swift │ ├── Preference Keys │ │ └── MaxValuePreferenceKey.swift │ ├── Selectively-Rounded Rectangle │ │ ├── CornerRadii.swift │ │ ├── Path+ContinuouslyRoundedRectangle.swift │ │ └── SelectivelyRoundedRectangle.swift │ ├── Time Interval Picker │ │ └── TimeIntervalPicker.swift │ └── View Modifiers │ │ ├── Device Did Shake │ │ └── DeviceDidShakeViewModifier.swift │ │ ├── Measure and Set To Max Height │ │ └── MeasureAndSetToMaxHeightViewModifier.swift │ │ └── View Controller Springboard │ │ └── ViewControllerSpringboard.swift │ ├── Synthesized Identifiable │ └── SynthesizedIdentifiable.swift │ ├── Task Executor │ └── TaskExecutor.swift │ ├── Time Interval Selection │ ├── TimeIntervalSelection.swift │ ├── TimeIntervalSelectionVersion.swift │ └── TimeIntervalSelectionVersionableWrapper.swift │ ├── Tuple Values │ ├── Tuple2.swift │ └── Tuple3.swift │ ├── UIConstants.swift │ ├── User Activity │ ├── EmptyUserActivity.swift │ ├── Extensions │ │ └── View+UserActivity.swift │ └── UserActivity.swift │ ├── User Notifications │ └── UserNotificationUserInfoKeys.swift │ └── Versionable │ ├── CodableVersionableEnvelope.swift │ ├── CodableVersionableWrapper.swift │ ├── Errors │ ├── VersionMigrationError.swift │ └── VersionableEnvelopeDecodingError.swift │ ├── Version.swift │ ├── Versionable.swift │ └── VersionableWrapper.swift └── Tests ├── CodeMonkeyAppleTests ├── Domain Name │ └── DomainNameTests.swift ├── Extensions │ └── Core Location │ │ ├── CLLocationCoordinate2DTests.swift │ │ └── CLLocationDegreesTests.swift ├── Hash │ ├── DerivedHashTests.swift │ └── MockHash.swift ├── Linked List │ └── AsyncDoublyLinkedListTests.swift ├── Storage │ └── Implementations │ │ └── UserDefaultsStorageTests.swift ├── Utilities │ ├── ModelV1.swift │ ├── ModelV2.swift │ ├── ModelV3.swift │ ├── ModelVersion.swift │ ├── ModelWrapper.swift │ ├── StorableTestHarness.swift │ ├── TestCodableStorable.swift │ ├── TestRawRepresentableStorable.swift │ └── XCTAssertAsync.swift └── Versionable │ └── VersionableTests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kyle Hughes 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:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "code-monkey-apple", 7 | defaultLocalization: "en", 8 | platforms: [ 9 | .iOS(.v16), 10 | .macOS(.v12), 11 | .watchOS(.v8) 12 | ], 13 | products: [ 14 | .library( 15 | name: "CodeMonkeyApple", 16 | targets: [ 17 | "CodeMonkeyApple" 18 | ] 19 | ), 20 | ], 21 | dependencies: [], 22 | targets: [ 23 | .target( 24 | name: "CodeMonkeyApple", 25 | dependencies: [] 26 | ), 27 | .testTarget( 28 | name: "CodeMonkeyAppleTests", 29 | dependencies: [ 30 | "CodeMonkeyApple" 31 | ] 32 | ), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Monkey Apple 2 | 3 | This is a project I use in all of my personal iOS apps. It contains many small features and extensions. 4 | 5 | Everything is MIT licensed. Feel free to copy any individual features. 6 | 7 | I do not recommend taking a dependency on this package. It has an audience of one (me). 8 | 9 | ## Localization 10 | 11 | ### Manual 12 | 13 | Resources: 14 | 15 | - [Apple: Maintaining Your Own String Files](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/MaintaingYourOwnStringsFiles/MaintaingYourOwnStringsFiles.html) 16 | 17 | #### Generate Strings File 18 | 19 | For the development language: 20 | 21 | ```sh 22 | find Sources/CodeMonkeyApple/. -name \*.swift -print0 | xargs -0 genstrings -o Sources/CodeMonkeyApple/Resources/en.lproj 23 | ``` 24 | 25 | For other supported languages: 26 | 27 | ```sh 28 | find Sources/CodeMonkeyApple/. -name \*.swift -print0 | xargs -0 genstrings -o Sources/CodeMonkeyApple/Resources/de.lproj 29 | ``` 30 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Activity Item/ActivityItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityItem.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/15/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ActivityItem: Equatable { 11 | /// The object that will be passed to the `UIActivityViewController`. 12 | var platformActivityItem: Any { get } 13 | } 14 | 15 | // MARK: - Extension for URL 16 | 17 | extension URL: ActivityItem { 18 | // MARK: Internal Instance Interface 19 | 20 | public var platformActivityItem: Any { 21 | self 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Array Builder/ArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayBuilder.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/20/22. 6 | // 7 | 8 | @resultBuilder 9 | public struct ArrayBuilder: ArrayBuilderProtocol { 10 | // NO-OP 11 | } 12 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Array Builder/ArrayBuilderProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayBuilder.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/20/22. 6 | // 7 | 8 | public protocol ArrayBuilderProtocol { 9 | associatedtype Element 10 | } 11 | 12 | // MARK: - Statement Blocks 13 | 14 | extension ArrayBuilderProtocol { 15 | // MARK: Public Static Interface 16 | 17 | public static func buildBlock(_ elements: [Element]...) -> [Element] { 18 | elements.flatMap { $0 } 19 | } 20 | } 21 | 22 | // MARK: - Optionals 23 | 24 | extension ArrayBuilderProtocol { 25 | // MARK: Public Static Interface 26 | 27 | public static func buildIf(_ component: [Element]?) -> [Element] { 28 | component ?? [] 29 | } 30 | } 31 | 32 | // MARK: - Conditionals 33 | 34 | extension ArrayBuilderProtocol { 35 | // MARK: Public Static Interface 36 | 37 | public static func buildEither(first: [Element]) -> [Element] { 38 | first 39 | } 40 | 41 | public static func buildEither(second: [Element]) -> [Element] { 42 | second 43 | } 44 | } 45 | 46 | // MARK: - Arrays 47 | 48 | extension ArrayBuilderProtocol { 49 | // MARK: Public Static Interface 50 | 51 | public static func buildArray(_ elements: [[Element]]) -> [Element] { 52 | elements.flatMap{ $0 } 53 | } 54 | } 55 | 56 | // MARK: - Expressions 57 | 58 | extension ArrayBuilderProtocol { 59 | // MARK: Public Static Interface 60 | 61 | public static func buildExpression(_ element: Element) -> [Element] { 62 | [element] 63 | } 64 | 65 | public static func buildExpression(_ elements: [Element]) -> [Element] { 66 | elements 67 | } 68 | 69 | public static func buildExpression( 70 | _ elements: Other 71 | ) -> [Element] where Other: Sequence, Other.Element == Element { 72 | [Element].init(elements) 73 | } 74 | } 75 | 76 | // MARK: - Final Results 77 | 78 | extension ArrayBuilderProtocol { 79 | // NO-OP, can be implemented by consumer if desired 80 | } 81 | 82 | // MARK: - Limited Availability 83 | 84 | extension ArrayBuilderProtocol { 85 | // MARK: Public Static Interface 86 | 87 | public static func buildLimitedAvailability(_ elements: [Element]) -> [Element] { 88 | elements 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Codable File/CodableFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableFile.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/16/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class CodableFile where Value: Codable { 11 | public var value: Value { 12 | didSet { 13 | try? encoder.encode(value).write(to: url) 14 | } 15 | } 16 | 17 | private let decoder: JSONDecoder 18 | private let encoder: JSONEncoder 19 | private let fileManager: FileManager 20 | private let url: URL 21 | 22 | // MARK: Public Initialization 23 | 24 | public init( 25 | url: URL, 26 | defaultValue: @autoclosure () -> Value, 27 | decoder: JSONDecoder = .default, 28 | encoder: JSONEncoder = .default, 29 | fileManager: FileManager = .default 30 | ) { 31 | self.url = url 32 | self.decoder = decoder 33 | self.encoder = encoder 34 | self.fileManager = fileManager 35 | 36 | do { 37 | if fileManager.fileExists(atPath: url.path) { 38 | value = try decoder.decode(Value.self, from: Data(contentsOf: url)) 39 | } else { 40 | let defaultValue = defaultValue() 41 | 42 | try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) 43 | try fileManager.createFile(atPath: url.path, contents: encoder.encode(defaultValue)) 44 | 45 | value = defaultValue 46 | } 47 | } catch { 48 | value = defaultValue() 49 | } 50 | } 51 | 52 | // MARK: Public Instance Interface 53 | 54 | public func modify(_ perform: (inout Value) -> Void) { 55 | perform(&value) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Dispatch Queue Factory/DispatchQueueFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueueFactory.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 2/4/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class DispatchQueueFactory { 11 | public let domain: ReverseDomainName 12 | 13 | // MARK: Public Initialization 14 | 15 | public init(domain: ReverseDomainName) { 16 | self.domain = domain 17 | } 18 | 19 | // MARK: Public Instance Interface 20 | 21 | public func makeQueue( 22 | subdomain: String, 23 | targeting targetQueue: DispatchQueue, 24 | qos: DispatchQoS = .unspecified, 25 | attributes: DispatchQueue.Attributes = [], 26 | autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit 27 | ) -> DispatchQueue { 28 | let targetDomain = ReverseDomainName(rawValue: targetQueue.label) 29 | let label = targetDomain.adding(subdomain: subdomain) 30 | 31 | return DispatchQueue( 32 | label: label, 33 | qos: qos, 34 | attributes: attributes, 35 | autoreleaseFrequency: autoreleaseFrequency, 36 | target: targetQueue 37 | ) 38 | } 39 | 40 | public func makeOvercommittingRootQueue( 41 | subdomain: String, 42 | qos: DispatchQoS = .unspecified, 43 | attributes: DispatchQueue.Attributes = [], 44 | autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit 45 | ) -> DispatchQueue { 46 | let label = domain.adding(subdomain: subdomain) 47 | 48 | return DispatchQueue( 49 | label: label, 50 | qos: qos, 51 | attributes: attributes, 52 | autoreleaseFrequency: autoreleaseFrequency, 53 | target: nil 54 | ) 55 | } 56 | 57 | public func makeRootQueue( 58 | subdomain: String, 59 | qos: DispatchQoS = .unspecified, 60 | attributes: DispatchQueue.Attributes = [], 61 | autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit 62 | ) -> DispatchQueue { 63 | let label = domain.adding(subdomain: subdomain) 64 | 65 | return DispatchQueue( 66 | label: label, 67 | qos: qos, 68 | attributes: attributes, 69 | autoreleaseFrequency: autoreleaseFrequency, 70 | target: .global() 71 | ) 72 | } 73 | } 74 | 75 | // MARK: - Internal Singleton 76 | 77 | extension DispatchQueueFactory { 78 | static let shared = DispatchQueueFactory(domain: Info.id.adding(subdomain: "gcd")) 79 | } 80 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Dispatch Queue Factory/Extensions/Foundation/DispatchQueue+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+Constants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/3/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DispatchQueue { 11 | // MARK: Root Queues 12 | 13 | public static let diskIO = DispatchQueueFactory.shared.makeRootQueue(subdomain: "disk-io") 14 | public static let networkIO = DispatchQueueFactory.shared.makeRootQueue(subdomain: "network-io") 15 | public static let ui = DispatchQueue.main 16 | public static let uiWork = DispatchQueueFactory.shared.makeRootQueue(subdomain: "ui-work") 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Dispatch Queue Factory/Extensions/Foundation/DispatchQueue+DispatchQueueFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+DispatchQueueFactory.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 1/24/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DispatchQueue { 11 | // MARK: Public Initialization 12 | 13 | public convenience init( 14 | label: ReverseDomainName, 15 | qos: DispatchQoS = .unspecified, 16 | attributes: DispatchQueue.Attributes = [], 17 | autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, 18 | target: DispatchQueue? = nil 19 | ) { 20 | self.init( 21 | label: label.rawValue, 22 | qos: qos, 23 | attributes: attributes, 24 | autoreleaseFrequency: autoreleaseFrequency, 25 | target: target 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Domain Name/DomainName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReverseDomainName.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/15/21. 6 | // 7 | 8 | public struct DomainName: Equatable, Hashable, Codable, RawRepresentable { 9 | // MARK: Public Instance Properties 10 | 11 | public private(set) var rawValue: String 12 | public private(set) var subdomains: [Subdomain] 13 | } 14 | 15 | // MARK: - Constants 16 | 17 | extension DomainName { 18 | public static let es = DomainName(rawValue: "es") 19 | public static let kylehugh_es = es.adding(subdomain: "kylehugh") 20 | } 21 | 22 | // MARK: - DomainNameProtocol Extension 23 | 24 | extension DomainName: DomainNameProtocol { 25 | // MARK: Public Initialization 26 | 27 | public init(rawValue: String) { 28 | self.rawValue = rawValue 29 | 30 | subdomains = rawValue.makeSubdomains() 31 | } 32 | 33 | public init(subdomains: [Subdomain]) { 34 | self.subdomains = subdomains 35 | 36 | rawValue = subdomains.makeRawValue() 37 | } 38 | 39 | // MARK: Public Instance Interface 40 | 41 | public mutating func add(other: Other) where Other: DomainNameProtocol { 42 | subdomains.insert(contentsOf: other.subdomains, at: 0) 43 | rawValue = subdomains.makeRawValue() 44 | } 45 | 46 | public mutating func add(subdomain: Subdomain) { 47 | subdomains.insert(subdomain, at: 0) 48 | rawValue = subdomains.makeRawValue() 49 | } 50 | 51 | public func adding(other: Other) -> Self where Other: DomainNameProtocol { 52 | var newSubdomains = subdomains 53 | newSubdomains.insert(contentsOf: other.subdomains, at: 0) 54 | 55 | return Self(subdomains: newSubdomains) 56 | } 57 | 58 | public func adding(subdomain: Subdomain) -> Self { 59 | var newSubdomains = subdomains 60 | newSubdomains.insert(subdomain, at: 0) 61 | 62 | return Self(subdomains: newSubdomains) 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Domain Name/DomainNameProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainNameProtocol.swift 3 | // CodeMonkeyAple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | public protocol DomainNameProtocol: Hashable, Codable, RawRepresentable { 9 | // MARK: Public Initialization 10 | 11 | init(rawValue: String) 12 | init(subdomains: [Subdomain]) 13 | 14 | // MARK: Public Instance Interface 15 | 16 | var subdomains: [Subdomain] { get } 17 | 18 | mutating func add(other: Other) where Other: DomainNameProtocol 19 | mutating func add(subdomain: Subdomain) 20 | func adding(other: Other) -> Self where Other: DomainNameProtocol 21 | func adding(subdomain: Subdomain) -> Self 22 | } 23 | 24 | // MARK: - Default Implementation 25 | 26 | extension DomainNameProtocol { 27 | // MARK: Public Instance Interface 28 | 29 | public mutating func add(subdomain: String) { 30 | add(subdomain: Subdomain(rawValue: subdomain)) 31 | } 32 | 33 | public mutating func add(subdomain: Substring) { 34 | add(subdomain: String(subdomain)) 35 | } 36 | 37 | public func adding(subdomain: String) -> Self { 38 | adding(subdomain: Subdomain(rawValue: subdomain)) 39 | } 40 | 41 | public func adding(subdomain: Substring) -> Self { 42 | adding(subdomain: String(subdomain)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Domain Name/Extensions/Standard Library/Array+DomainName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | // MARK: - Where Element == Subdomain 9 | 10 | extension Collection where Element == Subdomain { 11 | // MARK: Public Instance Interface 12 | 13 | public func makeRawValue() -> String { 14 | map(\.rawValue) 15 | .joined(separator: ".") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Domain Name/Extensions/Standard Library/StringProtocol+DomainName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringProtocol+DomainName.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | extension StringProtocol { 9 | // MARK: Public Instance Interface 10 | 11 | public func makeSubdomains() -> [Subdomain] { 12 | lazy 13 | .split(separator: ".") 14 | .map(String.init) 15 | .map(Subdomain.init(rawValue:)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Domain Name/ReverseDomainName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReverseDomainName.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 1/24/21. 6 | // 7 | 8 | public struct ReverseDomainName: Equatable, Hashable, Codable, RawRepresentable { 9 | // MARK: Public Instance Properties 10 | 11 | public private(set) var rawValue: String 12 | public private(set) var subdomains: [Subdomain] 13 | } 14 | 15 | // MARK: - Constants 16 | 17 | extension ReverseDomainName { 18 | public static let es = ReverseDomainName(rawValue: "es") 19 | public static let es_computertechniqu = es.adding(subdomain: "computertechniqu") 20 | public static let es_computertechniqu_rankThings = es_computertechniqu.adding(subdomain: "rank-things") 21 | public static let es_computertechniqu_superHeadache = es_computertechniqu.adding(subdomain: "super-headache") 22 | public static let es_kylehugh = es.adding(subdomain: "kylehugh") 23 | public static let es_kylehugh_codeMonkey = es_kylehugh.adding(subdomain: "code-monkey") 24 | public static let es_kylehugh_codeMonkey_apple = es_kylehugh_codeMonkey.adding(subdomain: "apple") 25 | public static let es_kylehugh_informationSuperhighway = es_kylehugh.adding(subdomain: "information-superhighway") 26 | } 27 | 28 | // MARK: - DomainNameProtocol Extension 29 | 30 | extension ReverseDomainName: DomainNameProtocol { 31 | // MARK: Public Initialization 32 | 33 | public init(rawValue: String) { 34 | self.rawValue = rawValue 35 | 36 | subdomains = rawValue.makeSubdomains() 37 | } 38 | 39 | public init(subdomains: [Subdomain]) { 40 | self.subdomains = subdomains 41 | 42 | rawValue = subdomains.makeRawValue() 43 | } 44 | 45 | // MARK: Public Instance Interface 46 | 47 | public mutating func add(other: Other) where Other: DomainNameProtocol { 48 | subdomains.append(contentsOf: other.subdomains) 49 | rawValue = subdomains.makeRawValue() 50 | } 51 | 52 | public mutating func add(subdomain: Subdomain) { 53 | subdomains.append(subdomain) 54 | rawValue = subdomains.makeRawValue() 55 | } 56 | 57 | public func adding(other: Other) -> Self where Other: DomainNameProtocol { 58 | var newSubdomains = subdomains 59 | newSubdomains.append(contentsOf: other.subdomains) 60 | 61 | return ReverseDomainName(subdomains: newSubdomains) 62 | } 63 | 64 | public func adding(subdomain: Subdomain) -> Self { 65 | var newSubdomains = subdomains 66 | newSubdomains.append(subdomain) 67 | 68 | return Self(subdomains: newSubdomains) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Domain Name/Subdomain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subdomain.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | public struct Subdomain: Equatable, Hashable, Codable, RawRepresentable { 9 | public let rawValue: String 10 | 11 | // MARK: Public Initialization 12 | 13 | public init(rawValue: String) { 14 | self.rawValue = rawValue 15 | } 16 | 17 | public init(rawValue: Substring) { 18 | self.rawValue = String(rawValue) 19 | } 20 | 21 | // MARK: Public Instance Interface 22 | 23 | public func adding(subdomain: String) -> DomainName { 24 | adding(subdomain: Subdomain(rawValue: subdomain)) 25 | } 26 | 27 | public func adding(subdomain: Substring) -> DomainName { 28 | adding(subdomain: Subdomain(rawValue: subdomain)) 29 | } 30 | 31 | public func adding(subdomain: Subdomain) -> DomainName { 32 | DomainName(subdomains: [self, subdomain]) 33 | } 34 | } 35 | 36 | // MARK: - ExpressibleByExtendedGraphemeClusterLiteral Extension 37 | 38 | extension Subdomain: ExpressibleByExtendedGraphemeClusterLiteral { 39 | // MARK: Public Initialization 40 | 41 | public init(extendedGraphemeClusterLiteral value: String) { 42 | self.init(rawValue: value) 43 | } 44 | } 45 | 46 | // MARK: - ExpressibleByStringLiteral Extension 47 | 48 | extension Subdomain: ExpressibleByStringLiteral { 49 | // MARK: Public Initialization 50 | 51 | public init(stringLiteral value: String) { 52 | self.init(rawValue: value) 53 | } 54 | } 55 | 56 | // MARK: - ExpressibleByUnicodeScalarLiteral Extension 57 | 58 | extension Subdomain: ExpressibleByUnicodeScalarLiteral { 59 | // MARK: Public Initialization 60 | 61 | public init(unicodeScalarLiteral value: String) { 62 | self.init(rawValue: value) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Enum Case Name/EnumCaseName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+EnumCase.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/1/22. 6 | // 7 | 8 | #if DEBUG 9 | @_silgen_name("swift_EnumCaseName") 10 | internal func _getEnumCaseName(_ value: T) -> UnsafePointer? 11 | 12 | public func getEnumCaseName(for value: T) -> String? { 13 | guard let stringPtr = _getEnumCaseName(value) else { 14 | return nil 15 | } 16 | 17 | return String(validatingUTF8: stringPtr) 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/AppKit/NSImage+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 9 | 10 | import AppKit 11 | 12 | extension NSImage { 13 | // MARK: Public Instance Interface 14 | 15 | public var hasAlphaChannel: Bool { 16 | guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { 17 | return false 18 | } 19 | 20 | return cgImage.hasAlphaChannel 21 | } 22 | 23 | public func dithering() throws -> NSImage { 24 | try ImageFilters.Dither.apply(to: self) 25 | } 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Combine/ObservableObject+Propagate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableObject+Propagate.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/16/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher { 12 | // MARK: Public Instance Interface 13 | 14 | public func observeAndPropagateChanges( 15 | from other: Other, 16 | storedIn set: inout Set 17 | ) where Other: ObservableObject { 18 | other 19 | .objectWillChange 20 | .sink { [weak self] _ in 21 | self?.objectWillChange.send() 22 | } 23 | .store(in: &set) 24 | } 25 | 26 | public func observeAndPropagateChangesForUI( 27 | from other: Other, 28 | storedIn set: inout Set 29 | ) where Other: ObservableObject { 30 | other 31 | .objectWillChange 32 | .receiveForUI() 33 | .sink { [weak self] _ in 34 | self?.objectWillChange.send() 35 | } 36 | .store(in: &set) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Combine/Publisher+Assign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Assign.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/29/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftUI 11 | 12 | // MARK: - Extension where Output equals Never 13 | 14 | extension Publisher where Self.Failure == Never { 15 | // MARK: Public Instance Interface 16 | 17 | public func assignWithoutRetaining( 18 | to keyPath: ReferenceWritableKeyPath, 19 | on object: Root 20 | ) -> AnyCancellable where Root: AnyObject { 21 | sink { [weak object] value in 22 | object?[keyPath: keyPath] = value 23 | } 24 | } 25 | 26 | public func assignWithoutRetaining( 27 | to keyPath: ReferenceWritableKeyPath, 28 | on object: Root, 29 | animatedWith animation: Animation 30 | ) -> AnyCancellable where Root: AnyObject { 31 | sink(animatedWith: animation) { [weak object] value in 32 | object?[keyPath: keyPath] = value 33 | } 34 | } 35 | 36 | public func sink( 37 | animatedWith animation: Animation, 38 | receiveValue: @escaping ((Self.Output) -> Void) 39 | ) -> AnyCancellable { 40 | sink { value in 41 | withAnimation(animation) { 42 | receiveValue(value) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Combine/Publisher+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 1/24/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension Publisher { 12 | // MARK: Mapping Elements 13 | 14 | public func mapToNil() -> Publishers.Map> { 15 | map { _ in 16 | .none 17 | } 18 | } 19 | 20 | public func mapToNil(as: NewOutput.Type) -> Publishers.Map> { 21 | map { _ in 22 | .none 23 | } 24 | } 25 | 26 | public func mapToOptional() -> Publishers.Map> { 27 | map(Optional.init) 28 | } 29 | 30 | // MARK: Filtering Elements 31 | 32 | public func skipNil( 33 | ) -> Publishers.CompactMap where Output == Optional { 34 | compactMap { $0 } 35 | } 36 | 37 | // MARK: Specifying Schedulers 38 | 39 | public func doWorkForUIAndReceiveForUI( 40 | _ publisherProvider: (Publishers.ReceiveOn) -> WorkPublisher 41 | ) -> Publishers.ReceiveOn where WorkPublisher: Publisher { 42 | publisherProvider(receiveForWorkForUI()) 43 | .receiveForUI() 44 | } 45 | 46 | public func doWorkForUIAndReceiveForUI( 47 | _ publisherProvider: (Publishers.ReceiveOn) -> WorkPublisher 48 | ) -> Publishers.ReceiveOn, DispatchQueue> 49 | where 50 | WorkPublisher: Publisher, 51 | WorkPublisher.Output: Equatable 52 | { 53 | publisherProvider(receiveForWorkForUI()) 54 | .receiveForUI() 55 | } 56 | 57 | public func receiveForUI() -> Publishers.ReceiveOn { 58 | receive(on: DispatchQueue.ui) 59 | } 60 | 61 | public func receiveForWorkForUI() -> Publishers.ReceiveOn { 62 | receive(on: DispatchQueue.uiWork) 63 | } 64 | } 65 | 66 | // MARK: - Extension where Output is Equatable 67 | 68 | extension Publisher where Output: Equatable { 69 | // MARK: Specifying Schedulers 70 | 71 | public func receiveForUI() -> Publishers.ReceiveOn, DispatchQueue> { 72 | removeDuplicates() 73 | .receiveForUI() 74 | } 75 | } 76 | 77 | // MARK: - Extension for Structured Concurrency Interoperability 78 | 79 | @available(iOS 15, macOS 12, watchOS 8, *) 80 | extension Publisher { 81 | // MARK: Public Instance Interface 82 | 83 | public func asyncMap( 84 | _ transform: @escaping (Output) async -> T 85 | ) -> Publishers.FlatMap, Self> { 86 | flatMap { value in 87 | Future { promise in 88 | Task { 89 | let output = await transform(value) 90 | promise(.success(output)) 91 | } 92 | } 93 | } 94 | } 95 | 96 | public func asyncMap( 97 | _ transform: @escaping (Output) async throws -> T 98 | ) -> Publishers.FlatMap, Self> { 99 | flatMap { value in 100 | Future { promise in 101 | Task { 102 | do { 103 | let output = try await transform(value) 104 | promise(.success(output)) 105 | } catch { 106 | promise(.failure(error)) 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | public func asyncMap( 114 | _ transform: @escaping (Output) async throws -> T 115 | ) -> Publishers.FlatMap, Publishers.SetFailureType> { 116 | flatMap { value in 117 | Future { promise in 118 | Task { 119 | do { 120 | let output = try await transform(value) 121 | promise(.success(output)) 122 | } catch { 123 | promise(.failure(error)) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Combine/Publisher+Propagate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Propagate.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/18/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension Publisher { 12 | // MARK: Public Instance Interface 13 | 14 | @inlinable 15 | public func mapToObservableObjectWillChange( 16 | at keyPath: KeyPath 17 | ) -> some Publisher where 18 | Nested: ObservableObject, 19 | Nested.ObjectWillChangePublisher == ObservableObjectPublisher, 20 | Nested.ObjectWillChangePublisher.Failure == Failure 21 | { 22 | mapToNestedPublisher(keyPath.appending(path: \.objectWillChange)) 23 | } 24 | 25 | @inlinable 26 | public func mapToNestedPublisher( 27 | _ keyPath: KeyPath 28 | ) -> some Publisher where Nested: Publisher, Nested.Failure == Failure { 29 | map(keyPath) 30 | .switchToLatest() 31 | } 32 | 33 | @inlinable 34 | public func mapToObservableObjectDidChange( 35 | _ keyPath: KeyPath 36 | ) -> some Publisher where 37 | Nested: ObservableObject, 38 | Nested.ObjectWillChangePublisher == ObservableObjectPublisher, 39 | Nested.ObjectWillChangePublisher.Failure == Failure 40 | { 41 | map { object in 42 | let nestedObject = object[keyPath: keyPath] 43 | 44 | return nestedObject[keyPath: \.objectWillChange] 45 | .map { nestedObject } 46 | } 47 | .switchToLatest() 48 | // We need to skip ahead one tick to actually get the new value that triggered the `objectWillChange` stream. 49 | .receive(on: RunLoop.current) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Core Graphics/CGImage+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage+CodeMonkeyApple.swift 3 | // code-monkey-apple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGImage { 11 | // MARK: Public Instance Interface 12 | 13 | public var hasAlphaChannel: Bool { 14 | alphaInfo != .none && alphaInfo != .noneSkipFirst && alphaInfo != .noneSkipLast 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Core Graphics/CGRect+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/28/21. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGRect { 11 | // MARK: Public Instance Interface 12 | 13 | public var center: CGPoint { 14 | CGPoint(x: midX, y: midY) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Core Graphics/CGSize+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/28/21. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGSize { 11 | public static let one = CGSize(dimension: 1) 12 | 13 | // MARK: Public Initialization 14 | 15 | public init(dimension: CGFloat) { 16 | self.init(width: dimension, height: dimension) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Core Location/CLLocationCoordinate2D+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+Codable 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/10/23. 6 | // 7 | 8 | import CoreLocation 9 | 10 | // MARK: - CLLocationCoordinate2D.CodingKeys Definition 11 | 12 | extension CLLocationCoordinate2D { 13 | public enum CodingKeys: String, CodingKey { 14 | case latitude 15 | case longitude 16 | } 17 | } 18 | 19 | // MARK: - Decodable Extension 20 | 21 | extension CLLocationCoordinate2D: Decodable { 22 | // MARK: Public Initialization 23 | 24 | public init(from decoder: Decoder) throws { 25 | let container = try decoder.container(keyedBy: CodingKeys.self) 26 | 27 | let latitude = try container.decode(CLLocationDegrees.self, forKey: .latitude) 28 | let longitude = try container.decode(CLLocationDegrees.self, forKey: .longitude) 29 | 30 | self.init(latitude: latitude, longitude: longitude) 31 | } 32 | } 33 | 34 | // MARK: - Encodable Extension 35 | 36 | extension CLLocationCoordinate2D: Encodable { 37 | // MARK: Public Instance Interface 38 | 39 | public func encode(to encoder: Encoder) throws { 40 | var container = encoder.container(keyedBy: CodingKeys.self) 41 | 42 | try container.encode(latitude, forKey: .latitude) 43 | try container.encode(longitude, forKey: .longitude) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Core Location/CLLocationCoordinate2D+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+Equatable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/10/23. 6 | // 7 | 8 | import CoreLocation 9 | 10 | // MARK: - Equatable Extension 11 | 12 | extension CLLocationCoordinate2D: Equatable { 13 | // MARK: Public Static Interface 14 | 15 | public static func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 16 | lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Core Location/CLLocationCoordinate2D+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+Hashable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/10/23. 6 | // 7 | 8 | import CoreLocation 9 | 10 | // MARK: - Hashable Extension 11 | 12 | extension CLLocationCoordinate2D: Hashable { 13 | // MARK: Public Static Interface 14 | 15 | public func hash(into hasher: inout Hasher) { 16 | hasher.combine(latitude) 17 | hasher.combine(longitude) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/JSONDecoder+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDecoder+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Constants 11 | 12 | extension JSONDecoder { 13 | public static let `default` = JSONDecoder() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/JSONEncoder+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONEncoder+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Constants 11 | 12 | extension JSONEncoder { 13 | public static let `default` = JSONEncoder() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/NSOrderedSet+Bridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSOrderedSet+Bridge.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 5/26/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSOrderedSet { 11 | // MARK: Public Instance Interface 12 | 13 | public func typed() -> [Element] { 14 | typed(as: Element.self) 15 | } 16 | 17 | public func typed(as type: Element.Type) -> [Element] { 18 | guard let array = array as? [Element] else { 19 | return [] 20 | } 21 | 22 | return array 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/NSRegularExpression+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSRegularExpression+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/28/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSRegularExpression { 11 | // MARK: Public Initialization 12 | 13 | public convenience init(_ pattern: String) { 14 | do { 15 | try self.init(pattern: pattern) 16 | } catch { 17 | preconditionFailure("Illegal regular expression: \(pattern)") 18 | } 19 | } 20 | 21 | // MARK: Public Instance Interface 22 | 23 | public func enumerateMatches( 24 | in string: String, 25 | using block: (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer) -> Void 26 | ) { 27 | enumerateMatches(in: string, options: [], range: Self.makeFullRange(for: string), using: block) 28 | } 29 | 30 | public func firstMatch(in string: String) -> NSTextCheckingResult? { 31 | firstMatch(in: string, options: [], range: Self.makeFullRange(for: string)) 32 | } 33 | 34 | public func matches(_ string: String) -> Bool { 35 | firstMatch(in: string, options: [], range: Self.makeFullRange(for: string)) != nil 36 | } 37 | 38 | // MARK: Private Static Interface 39 | 40 | private static func makeFullRange(for string: String) -> NSRange { 41 | NSRange(string.startIndex..., in: string) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/NSSet+Bridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSSet+Bridge.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 5/30/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSSet { 11 | // MARK: Public Instance Interface 12 | 13 | public func typed() -> Set { 14 | typed(as: Element.self) 15 | } 16 | 17 | public func typed(as type: Element.Type) -> Set { 18 | guard let set = self as? Set else { 19 | return Set() 20 | } 21 | 22 | return set 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/Thread+Number.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Thread+Number.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/30/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Thread { 11 | // MARK: Public Static Interface 12 | 13 | public static var number: Int { 14 | current.number 15 | } 16 | 17 | // MARK: Public Instance Interface 18 | 19 | public var number: Int { 20 | let originalString = String(describing: self) 21 | let regex = NSRegularExpression(#".+= (\d+)"#) 22 | 23 | guard 24 | let match = regex.firstMatch(in: originalString), 25 | let range = Range(match.range(at: 1), in: originalString), 26 | let integer = Int(originalString[range]) 27 | else { 28 | return -1 29 | } 30 | 31 | return integer 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/TimeInterval+Calendar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+Calendar.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 2/26/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension TimeInterval { 11 | // MARK: Public Static Interface 12 | 13 | /// - Warning: This is unmoored from reality. Consider using higher abstractions like `Calendar`. 14 | public static func day() -> TimeInterval { 15 | days(1) 16 | } 17 | 18 | /// - Warning: This is unmoored from reality. Consider using higher abstractions like `Calendar`. 19 | public static func days(_ days: Double) -> TimeInterval { 20 | hours(24) * days 21 | } 22 | 23 | /// - Warning: This is unmoored from reality. Consider using higher abstractions like `Calendar`. 24 | public static func hour() -> TimeInterval { 25 | hours(1) 26 | } 27 | 28 | /// - Warning: This is unmoored from reality. Consider using higher abstractions like `Calendar`. 29 | public static func hours(_ hours: Double) -> TimeInterval { 30 | minutes(60) * hours 31 | } 32 | 33 | public static func millisecond() -> TimeInterval { 34 | milliseconds(1) 35 | } 36 | 37 | public static func milliseconds(_ milliseconds: Double) -> TimeInterval { 38 | milliseconds / 1000 39 | } 40 | 41 | /// - Warning: This is unmoored from reality. Consider using higher abstractions like `Calendar`. 42 | public static func minutes() -> TimeInterval { 43 | minutes(1) 44 | } 45 | 46 | /// - Warning: This is unmoored from reality. Consider using higher abstractions like `Calendar`. 47 | public static func minutes(_ minutes: Double) -> TimeInterval { 48 | seconds(60) * minutes 49 | } 50 | 51 | public static func second() -> TimeInterval { 52 | seconds(1) 53 | } 54 | 55 | public static func seconds(_ seconds: Double) -> TimeInterval { 56 | seconds 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/TimeInterval+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+Constants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/24/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension TimeInterval { 11 | // MARK: Presentation Values 12 | 13 | public static let confirmationPause: TimeInterval = 0.75 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Foundation/UserDefaults+Migrate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Migrate.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 5/9/22. 6 | // 7 | 8 | import Foundation 9 | import os.log 10 | 11 | private let logger = Logger(subsystem: "es.kylehugh.code-monkey-apple", category: "user-defaults") 12 | 13 | extension UserDefaults { 14 | // MARK: Public Instance Interface 15 | 16 | public func migrate(to other: UserDefaults) { 17 | logger.info("Attempting to migrate to other store…") 18 | 19 | guard not(other.get(.wasMigratedTo)) else { 20 | logger.info("Skipping migration, other store was already migrated to") 21 | 22 | return 23 | } 24 | 25 | logger.info("Commencing one-time migration…") 26 | 27 | for (key, value) in dictionaryRepresentation() { 28 | other.set(value, forKey: key) 29 | } 30 | 31 | other.set(.wasMigratedTo, to: true) 32 | 33 | logger.info("Finished migrating to other store") 34 | } 35 | } 36 | 37 | // MARK: - Boolean Storage Keys 38 | 39 | extension StorageKeyProtocol where Self == StorageKey { 40 | // MARK: Public Static Interface 41 | 42 | public static var wasMigratedTo: Self { 43 | StorageKey( 44 | id: "WasMigratedTo", 45 | defaultValue: false 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/HealthKit/HKHealthStore+HeadKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HKHealthStore+Super Headache.swift 3 | // Super Headache 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | #if canImport(HealthKit) 9 | 10 | import Combine 11 | import HealthKit 12 | 13 | @available(macOS 13.0, *) 14 | extension HKHealthStore { 15 | // MARK: Public Instance Interface 16 | 17 | public func requestAuthorization( 18 | toReadObjectTypes typesToRead: Set 19 | ) async -> Result { 20 | await requestAuthorization(toShareSampleTypes: [], andReadObjectTypes: typesToRead) 21 | } 22 | 23 | public func requestAuthorization( 24 | toShareSample sample: HKSample 25 | ) async -> Result { 26 | await requestAuthorization(toShareSampleTypes: [sample.sampleType], andReadObjectTypes: []) 27 | } 28 | 29 | public func requestAuthorization( 30 | toShareSampleTypes typesToShare: Set 31 | ) async -> Result { 32 | await requestAuthorization(toShareSampleTypes: typesToShare, andReadObjectTypes: []) 33 | } 34 | 35 | public func requestAuthorization( 36 | toShareSampleTypes typesToShare: Set, 37 | andReadObjectTypes typesToRead: Set 38 | ) async -> Result { 39 | do { 40 | try await requestAuthorization(toShare: typesToShare, read: typesToRead) 41 | 42 | return .success 43 | } catch { 44 | guard let frameworkError = error as? HKError else { 45 | return .failure(HealthKitError.Authorization.unknownError(error)) 46 | } 47 | 48 | let appError: HealthKitError.Authorization = { 49 | switch frameworkError.code { 50 | case .errorAuthorizationNotDetermined: 51 | return .userIgnored 52 | default: 53 | return .frameworkError(frameworkError) 54 | } 55 | }() 56 | 57 | return .failure(appError) 58 | } 59 | } 60 | 61 | public func saveWithResult(_ object: HKObject) async -> Result { 62 | do { 63 | try await save(object) as Void 64 | 65 | return .success 66 | } catch { 67 | guard let frameworkError = error as? HKError else { 68 | return .failure(.unknownError(error)) 69 | } 70 | 71 | let appError: HealthKitError.Persistence = { 72 | switch frameworkError.code { 73 | case .errorAuthorizationNotDetermined: 74 | return .authorizationNotDetermined 75 | case .errorAuthorizationDenied: 76 | return .authorizationDenied 77 | case .errorInvalidArgument: 78 | return .invalidArgument 79 | default: 80 | return .unknownFrameworkError(frameworkError) 81 | } 82 | }() 83 | 84 | return .failure(appError) 85 | } 86 | } 87 | } 88 | 89 | // MARK: - Previews 90 | 91 | #if DEBUG 92 | @available(macOS 13.0, *) 93 | extension HKHealthStore { 94 | public static let preview = HKHealthStore() 95 | } 96 | #endif 97 | 98 | #endif 99 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Standard Library/Bool+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | extension Bool { 9 | // MARK: Public Instance Interface 10 | 11 | public var not: Bool { 12 | !self 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Standard Library/RawRepresentable+Comparable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentable+Comparable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/3/21. 6 | // 7 | 8 | extension RawRepresentable where RawValue: Comparable { 9 | // MARK: Public Static Interface 10 | 11 | public static func < (lhs: Self, rhs: Self) -> Bool { 12 | lhs.rawValue < rhs.rawValue 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Standard Library/Result+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | extension Result where Success == Void { 9 | // MARK: Public Static Interface 10 | 11 | public static var success: Self { 12 | .success(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Standard Library/String+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Constants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/24/21. 6 | // 7 | 8 | extension String { 9 | // MARK: Zero-Width Characters 10 | 11 | public static let zeroWidthJoiner = "‍" 12 | public static let zeroWidthNonJoiner = "‌" 13 | public static let zeroWidthSpace = "​" 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Standard Library/String+SnakeCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+SnakeCase.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/1/22. 6 | // 7 | 8 | extension String { 9 | // MARK: Public Instance Interface 10 | 11 | public func logCased() -> String { 12 | snakeCased().uppercased() 13 | } 14 | 15 | public func snakeCased() -> String { 16 | guard !isEmpty else { 17 | return self 18 | } 19 | 20 | var words: [Range] = [] 21 | var wordStart = startIndex 22 | var searchRange = index(after: wordStart) ..< endIndex 23 | 24 | while let upperCaseRange = rangeOfCharacter(from: .uppercaseLetters, options: [], range: searchRange) { 25 | let untilUpperCase = wordStart ..< upperCaseRange.lowerBound 26 | words.append(untilUpperCase) 27 | searchRange = upperCaseRange.lowerBound ..< searchRange.upperBound 28 | 29 | guard 30 | let lowerCaseRange = rangeOfCharacter( 31 | from: .lowercaseLetters, 32 | options: [], 33 | range: searchRange 34 | ) 35 | else { 36 | wordStart = searchRange.lowerBound 37 | break 38 | } 39 | 40 | let nextCharacterAfterCapital = index(after: upperCaseRange.lowerBound) 41 | 42 | if lowerCaseRange.lowerBound == nextCharacterAfterCapital { 43 | wordStart = upperCaseRange.lowerBound 44 | } else { 45 | let beforeLowerIndex = index(before: lowerCaseRange.lowerBound) 46 | words.append(upperCaseRange.lowerBound ..< beforeLowerIndex) 47 | wordStart = beforeLowerIndex 48 | } 49 | 50 | searchRange = lowerCaseRange.upperBound ..< searchRange.upperBound 51 | } 52 | 53 | words.append(wordStart ..< searchRange.upperBound) 54 | 55 | return words 56 | .map { self[$0].lowercased() } 57 | .joined(separator: "_") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/Standard Library/Task+Sleep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/24/21. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) 11 | extension Task where Success == Never, Failure == Never { 12 | // MARK: Public Static Interface 13 | 14 | public static func sleep(milliseconds: Double) async throws { 15 | try await sleep(nanoseconds: UInt64(milliseconds * 1_000_000)) 16 | } 17 | 18 | public static func sleep(seconds: TimeInterval) async throws { 19 | try await sleep(milliseconds: seconds * 1_000) 20 | } 21 | 22 | public static func sleepUnlessCancelled(nanoseconds: UInt64) async throws { 23 | guard not(Task.isCancelled) else { 24 | return 25 | } 26 | 27 | try await sleep(nanoseconds: nanoseconds) 28 | } 29 | 30 | public static func sleepUnlessCancelled(milliseconds: TimeInterval) async throws { 31 | try await sleepUnlessCancelled(nanoseconds: UInt64(milliseconds) * 1_000_000) 32 | } 33 | 34 | public static func sleepUnlessCancelled(seconds: TimeInterval) async throws { 35 | try await sleepUnlessCancelled(milliseconds: seconds * 1_000) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Binding+Change.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding+Change.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 4/19/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Binding { 11 | // MARK: Public Instance Interface 12 | 13 | public func onChange(_ handler: @escaping (Value) async -> Void) -> Binding { 14 | onChange { value in 15 | Task { 16 | await handler(value) 17 | } 18 | } 19 | } 20 | 21 | public func onChange(_ handler: @escaping (Value) -> Void) -> Binding { 22 | Binding( 23 | get: { 24 | wrappedValue 25 | }, 26 | set: { 27 | wrappedValue = $0 28 | handler($0) 29 | } 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Binding+Compose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding+Compose.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/19/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Binding { 11 | // MARK: Public Instance Interface 12 | 13 | public func composedToBool( 14 | whenEqualTo value: Value 15 | ) -> Binding where Value == Unwrapped?, Unwrapped: Equatable { 16 | Binding { 17 | wrappedValue == value 18 | } set: { 19 | wrappedValue = $0 ? value : nil 20 | } 21 | } 22 | 23 | public func composed(through keyPath: WritableKeyPath) -> Binding { 24 | Binding { 25 | wrappedValue[keyPath: keyPath] 26 | } set: { 27 | wrappedValue[keyPath: keyPath] = $0 28 | } 29 | } 30 | 31 | public func composed(to: @escaping (Value) -> Other, from: @escaping (Other) -> Value) -> Binding { 32 | Binding { 33 | to(wrappedValue) 34 | } set: { 35 | wrappedValue = from($0) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Binding+Unidirectional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding+Unidirectional.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 8/2/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Binding { 11 | // MARK: Unidirectional Initialization 12 | 13 | public init(_ constant: Value, set: @escaping (Value) -> Void) { 14 | self.init(get: { constant }, set: set) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Button+Concurrency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button+Concurrency.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 9/11/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, *) 11 | extension Button { 12 | // MARK: Creating a Button 13 | 14 | public init(asyncAction: @escaping () async -> Void, label: () -> Label) { 15 | self.init( 16 | action: { 17 | Task { 18 | await asyncAction() 19 | } 20 | }, 21 | label: label 22 | ) 23 | } 24 | 25 | public init(_ titleKey: LocalizedStringKey, asyncAction: @escaping () async -> Void) where Label == Text { 26 | self.init( 27 | titleKey, 28 | action: { 29 | Task { 30 | await asyncAction() 31 | } 32 | } 33 | ) 34 | } 35 | 36 | public init(_ title: S, asyncAction: @escaping () async -> Void) where Label == Text, S: StringProtocol { 37 | self.init( 38 | title, 39 | action: { 40 | Task { 41 | await asyncAction() 42 | } 43 | } 44 | ) 45 | } 46 | 47 | // MARK: Creating a Button with a Role 48 | 49 | public init(role: ButtonRole?, asyncAction: @escaping () async -> Void, label: () -> Label) { 50 | self.init( 51 | role: role, 52 | action: { 53 | Task { 54 | await asyncAction() 55 | } 56 | }, 57 | label: label 58 | ) 59 | } 60 | 61 | public init( 62 | _ titleKey: LocalizedStringKey, 63 | role: ButtonRole?, 64 | asyncAction: @escaping () async -> Void 65 | ) where Label == Text { 66 | self.init( 67 | titleKey, 68 | role: role, 69 | action: { 70 | Task { 71 | await asyncAction() 72 | } 73 | } 74 | ) 75 | } 76 | 77 | public init( 78 | _ title: S, 79 | role: ButtonRole?, 80 | asyncAction: @escaping () async -> Void 81 | ) where Text == Label, S: StringProtocol { 82 | self.init( 83 | title, 84 | role: role, 85 | action: { 86 | Task { 87 | await asyncAction() 88 | } 89 | } 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Color+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+RankThings.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/4/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(iOS) 11 | import UIKit 12 | #elseif os(watchOS) 13 | import WatchKit 14 | #elseif os(macOS) 15 | import AppKit 16 | #endif 17 | 18 | // MARK: - Decodable Extension 19 | 20 | extension Color: Decodable { 21 | // MARK: Public Instance Interface 22 | 23 | public init(from decoder: Decoder) throws { 24 | let container = try decoder.container(keyedBy: CodingKeys.self) 25 | let r = try container.decode(Double.self, forKey: .red) 26 | let g = try container.decode(Double.self, forKey: .green) 27 | let b = try container.decode(Double.self, forKey: .blue) 28 | 29 | self.init(red: r, green: g, blue: b) 30 | } 31 | } 32 | 33 | // MARK: - Encodable Extension 34 | 35 | extension Color: Encodable { 36 | // MARK: Public Instance Interface 37 | 38 | public func encode(to encoder: Encoder) throws { 39 | guard let colorComponents = colorComponents else { 40 | return 41 | } 42 | 43 | var container = encoder.container(keyedBy: CodingKeys.self) 44 | try container.encode(colorComponents.red, forKey: .red) 45 | try container.encode(colorComponents.green, forKey: .green) 46 | try container.encode(colorComponents.blue, forKey: .blue) 47 | } 48 | 49 | // MARK: Private Instance Interface 50 | 51 | private var colorComponents: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)? { 52 | var r: CGFloat = 0 53 | var g: CGFloat = 0 54 | var b: CGFloat = 0 55 | var a: CGFloat = 0 56 | 57 | #if os(macOS) 58 | NSColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) 59 | // Note that non RGB color will raise an exception, that I don't now how to catch because it is an Objc 60 | // exception. 61 | #else 62 | guard UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) else { 63 | // Pay attention that the color should be convertible into RGB format 64 | // Colors using hue, saturation and brightness won't work 65 | return nil 66 | } 67 | #endif 68 | 69 | return (r, g, b, a) 70 | } 71 | } 72 | 73 | // MARK: - Color.CodingKeys Definition 74 | 75 | extension Color { 76 | public enum CodingKeys: String, CodingKey { 77 | case red 78 | case green 79 | case blue 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Color+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Constants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/11/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) && !os(watchOS) 11 | 12 | import UIKit 13 | 14 | extension Color { 15 | // MARK: Grays 16 | 17 | public static let lightGray = Color(.lightGray) 18 | 19 | // MARK: System Grouped Backgrounds 20 | 21 | public static let secondarySystemGroupedBackground = Color(.secondarySystemGroupedBackground) 22 | public static let systemGroupedBackground = Color(.systemGroupedBackground) 23 | public static let tertiarySystemGroupedBackground = Color(.tertiarySystemGroupedBackground) 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Color+Equality.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Equality.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/11/22. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import SwiftUI 10 | import UIKit 11 | 12 | extension Color { 13 | // MARK: Public Instance Interface 14 | 15 | public func rgbaEqualsRGBA(from rhs: Color) -> Bool { 16 | UIColor(self).rgbaEqualsRGBA(from: UIColor(rhs)) 17 | } 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Color+Storable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Storable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color: Storable { 11 | // NO-OP 12 | } 13 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/EdgeInsets+Zero.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsets+Zero.swift 3 | // code-monkey-apple 4 | // 5 | // Created by Kyle Hughes on 1/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension EdgeInsets { 11 | // MARK: Public Static Interface 12 | 13 | public static let zero = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/Image+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+Constants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/18/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Image { 11 | // MARK: Public Static Interface 12 | 13 | public static var boldCheckmark: some View { 14 | Image(systemName: "checkmark") 15 | .font(.body.bold()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/NavigationLink+Empty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationLink+Empty.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/12/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension NavigationLink { 11 | // MARK: Public Initialization 12 | 13 | public init(destination: Destination, isActive: Binding) where Label == EmptyView { 14 | self.init(destination: destination, isActive: isActive) { 15 | EmptyView() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+AppLifecycle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+AppLifecycle.swift 3 | // SuperHeadaache 4 | // 5 | // Created by Kyle Hughes on 8/22/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import SwiftUI 11 | import UIKit 12 | 13 | extension View { 14 | // MARK: Public Instance Interface 15 | 16 | public func onAppEnterForeground(perform: @escaping () -> Void) -> some View { 17 | onReceive( 18 | NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) 19 | ) { _ in 20 | perform() 21 | } 22 | } 23 | 24 | public func onSignificantTimeChange(perform: @escaping () -> Void) -> some View { 25 | onReceive( 26 | NotificationCenter.default.publisher(for: UIApplication.significantTimeChangeNotification) 27 | ) { _ in 28 | perform() 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+AppStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+AppStore.swift 3 | // Music Triage 4 | // 5 | // Created by Kyle Hughes on 10/2/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import StoreKit 11 | import SwiftUI 12 | 13 | extension View { 14 | // MARK: Public Instance Interface 15 | 16 | public func appStoreProductPage( 17 | for appID: String, 18 | when shouldPresent: Binding, 19 | tintColor: Color = .accentColor 20 | ) -> some View { 21 | viewControllerSpringboard(isPresented: shouldPresent) { 22 | Coordinator { 23 | shouldPresent.wrappedValue = false 24 | } 25 | } action: { viewController, coordinator, _ in 26 | let productViewController = SKStoreProductViewController() 27 | productViewController.delegate = coordinator 28 | 29 | productViewController.loadProduct( 30 | withParameters: [ 31 | SKStoreProductParameterITunesItemIdentifier: appID 32 | ] 33 | ) 34 | productViewController.view.tintColor = UIColor(tintColor) 35 | 36 | viewController.present(productViewController, animated: true) 37 | } 38 | } 39 | 40 | public func appStoreProductPage( 41 | for appID: Binding, 42 | tintColor: Color = .accentColor 43 | ) -> some View { 44 | viewControllerSpringboard(with: appID) { 45 | Coordinator { 46 | appID.wrappedValue = nil 47 | } 48 | } action: { viewController, coordinator, appID in 49 | let productViewController = SKStoreProductViewController() 50 | productViewController.delegate = coordinator 51 | 52 | productViewController.loadProduct( 53 | withParameters: [ 54 | SKStoreProductParameterITunesItemIdentifier: appID 55 | ] 56 | ) 57 | productViewController.view.tintColor = UIColor(tintColor) 58 | 59 | viewController.present(productViewController, animated: true) 60 | } 61 | } 62 | } 63 | 64 | // MARK: - Coordinator Definition 65 | 66 | private final class Coordinator: NSObject { 67 | private let onDismiss: () -> Void 68 | 69 | // MARK: Fileprivate Inialization 70 | 71 | fileprivate init(onDismiss: @escaping () -> Void) { 72 | self.onDismiss = onDismiss 73 | } 74 | } 75 | 76 | // MARK: - SKStoreProductViewControllerDelegate Extension 77 | 78 | extension Coordinator: SKStoreProductViewControllerDelegate { 79 | // MARK: Responding to a Dismiss Action 80 | 81 | fileprivate func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { 82 | onDismiss() 83 | } 84 | } 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+Concurrency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Concurrency.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 3/28/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | // MARK: Public Instance Interface 12 | 13 | public func onChange( 14 | of value: Value, 15 | perform action: @escaping (Value) async -> Void 16 | ) -> some View where Value : Equatable { 17 | onChange(of: value) { newValue in 18 | Task { 19 | await action(newValue) 20 | } 21 | } 22 | } 23 | 24 | public func onTapGesture(count: Int = 1, perform action: @escaping () async -> Void) -> some View { 25 | onTapGesture(count: count) { 26 | Task { 27 | await action() 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+Conditional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Conditional.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/13/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | // MARK: Public Instance Interface 12 | 13 | /// - Warning: This should only be used to conditionally apply modifiers that require protocols, e.g. 14 | /// `ButtonStyle` or `ShapeStyle`. Otherwise, ternary operators at the callsite are preferred. If the condition 15 | /// changes for the same view then the transition animation may be incorrect. 16 | @ViewBuilder 17 | public func `if`( 18 | _ condition: @autoclosure () -> Bool, 19 | @ViewBuilder then: (Self) -> some View, 20 | @ViewBuilder else: (Self) -> some View 21 | ) -> some View { 22 | if condition() { 23 | then(self) 24 | } else { 25 | `else`(self) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+Preview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Preview.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/12/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | // MARK: Public Instance Interface 12 | 13 | public func defaultPreviewDevice() -> some View { 14 | previewDevice("iPhone 13") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+Share.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Share.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/3/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import Foundation 11 | import SwiftUI 12 | 13 | public typealias ActivityItemFactory = (ActivityItem) -> Any where ActivityItem: Equatable 14 | 15 | extension View { 16 | // MARK: Public Instance Interface 17 | 18 | @available(*, deprecated, message: "Presentation gets stuck in infinite loop.") 19 | public func share( 20 | activityItem: Binding 21 | ) -> some View where ActivityItem: CodeMonkeyApple.ActivityItem { 22 | share(activityItem: activityItem) { 23 | $0.platformActivityItem 24 | } 25 | } 26 | 27 | @available(*, deprecated, message: "Presentation gets stuck in infinite loop.") 28 | public func share( 29 | activityItem: Binding, 30 | factory: @escaping ActivityItemFactory 31 | ) -> some View where ActivityItem: Equatable { 32 | viewControllerSpringboard(with: activityItem) { viewController, _, item in 33 | let platformActivityItem = factory(item) 34 | let activityViewController = UIActivityViewController(activityItem: platformActivityItem) 35 | 36 | activityViewController.allowsProminentActivity = true 37 | 38 | activityViewController.completionWithItemsHandler = { _, _, _, _ in 39 | activityItem.wrappedValue = nil 40 | } 41 | 42 | viewController.present(activityViewController, animated: true, completion: nil) 43 | } 44 | } 45 | } 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+Snapshot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Snapshot.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/18/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) && !os(watchOS) 11 | 12 | import UIKit 13 | 14 | extension View { 15 | // MARK: Public Instance Interface 16 | 17 | public func snapshot() -> UIImage { 18 | UIView.setAnimationsEnabled(false) 19 | 20 | defer { 21 | UIView.setAnimationsEnabled(true) 22 | } 23 | 24 | let controller = UIHostingController(rootView: self) 25 | let view = controller.view 26 | 27 | let targetSize = controller.view.intrinsicContentSize 28 | view?.bounds = CGRect(origin: .zero, size: targetSize) 29 | view?.backgroundColor = .clear 30 | 31 | let renderer = UIGraphicsImageRenderer(size: targetSize) 32 | 33 | return renderer.image { _ in 34 | view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true) 35 | } 36 | } 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/SwiftUI/View+SpecificModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+SpecificModifiers.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/12/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | // MARK: Public Instance Interface 12 | 13 | /// Value comes from "What's new in SwiftUI" from WWDC 2021. 14 | public func maxWidthForBorderedButton() -> some View { 15 | frame(maxWidth: 300) 16 | } 17 | 18 | /// Value comes from testing a TextEditor with one character. 19 | func minimumHeightForTextEditorInForm() -> some View { 20 | frame(minHeight: 38) 21 | } 22 | 23 | public func paddingForMultilineTextContentInInsetGroupedList() -> some View { 24 | padding(.vertical, 8) 25 | } 26 | 27 | func sectionHeaderAndFooterHidden() -> some View { 28 | labelsHidden() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/UIKit/UIActivityViewController+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIActivityViewController+CodeMonkeyApple.swift 3 | // Rank Things 4 | // 5 | // Created by Kyle Hughes on 7/1/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | extension UIActivityViewController { 13 | // MARK: Public Initialization 14 | 15 | public convenience init(activityItem: Any) { 16 | self.init(activityItems: [activityItem]) 17 | } 18 | 19 | public convenience init(activityItems: [Any]) { 20 | self.init(activityItems: activityItems, applicationActivities: nil) 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/UIKit/UIColor+Equality.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Equality.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/11/22. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | extension UIColor { 12 | // MARK: Public Instance Interface 13 | 14 | public func rgbaEqualsRGBA(from rhs: UIColor) -> Bool { 15 | var lhsR: CGFloat = 0 16 | var lhsG: CGFloat = 0 17 | var lhsB: CGFloat = 0 18 | var lhsA: CGFloat = 0 19 | getRed(&lhsR, green: &lhsG, blue: &lhsB, alpha: &lhsA) 20 | 21 | var rhsR: CGFloat = 0 22 | var rhsG: CGFloat = 0 23 | var rhsB: CGFloat = 0 24 | var rhsA: CGFloat = 0 25 | rhs.getRed(&rhsR, green: &rhsG, blue: &rhsB, alpha: &rhsA) 26 | 27 | return 28 | lhsR == rhsR && 29 | lhsG == rhsG && 30 | lhsB == rhsB && 31 | lhsA == rhsA 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/UIKit/UIContentSizeCategory+Relative.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIContentSizeCategory+Relative.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | // MARK: - Extension for UIContentSizeCategory 13 | 14 | extension UIContentSizeCategory { 15 | // MARK: Internal Instance Interface 16 | 17 | public var next: UIContentSizeCategory { 18 | switch self { 19 | case .extraSmall: 20 | return .small 21 | case .small: 22 | return .medium 23 | case .medium: 24 | return .large 25 | case .large: 26 | return .extraLarge 27 | case .extraLarge: 28 | return .extraExtraLarge 29 | case .extraExtraLarge: 30 | return .extraExtraExtraLarge 31 | case .extraExtraExtraLarge: 32 | return .accessibilityMedium 33 | case .accessibilityMedium: 34 | return .accessibilityLarge 35 | case .accessibilityLarge: 36 | return .accessibilityExtraLarge 37 | case .accessibilityExtraLarge: 38 | return .accessibilityExtraExtraLarge 39 | case .accessibilityExtraExtraLarge: 40 | return .accessibilityExtraExtraExtraLarge 41 | case .accessibilityExtraExtraExtraLarge: 42 | return self 43 | case .unspecified: 44 | return self 45 | default: 46 | return self 47 | } 48 | } 49 | 50 | public var previous: UIContentSizeCategory { 51 | switch self { 52 | case .extraSmall: 53 | return self 54 | case .small: 55 | return .extraSmall 56 | case .medium: 57 | return .small 58 | case .large: 59 | return .medium 60 | case .extraLarge: 61 | return .large 62 | case .extraExtraLarge: 63 | return .extraLarge 64 | case .extraExtraExtraLarge: 65 | return .extraExtraLarge 66 | case .accessibilityMedium: 67 | return .extraExtraExtraLarge 68 | case .accessibilityLarge: 69 | return .accessibilityMedium 70 | case .accessibilityExtraLarge: 71 | return .accessibilityLarge 72 | case .accessibilityExtraExtraLarge: 73 | return .accessibilityExtraLarge 74 | case .accessibilityExtraExtraExtraLarge: 75 | return .accessibilityExtraExtraLarge 76 | case .unspecified: 77 | return self 78 | default: 79 | return self 80 | } 81 | } 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/UIKit/UIImage+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+CodeMonkeyApple.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | #if canImport(UIKit) 9 | 10 | import UIKit 11 | 12 | extension UIImage { 13 | // MARK: Public Instance Interface 14 | 15 | public var hasAlphaChannel: Bool { 16 | guard let cgImage = cgImage else { 17 | return false 18 | } 19 | 20 | return cgImage.hasAlphaChannel 21 | } 22 | 23 | public func dithering() throws -> UIImage { 24 | try ImageFilters.Dither.apply(to: self) 25 | } 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/UIKit/UIScreen+iPodTouch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScreen+iPodTouch.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/12/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | extension UIScreen { 13 | // MARK: Internal Instance Interface 14 | 15 | public var hasiPodTouchScreenSize: Bool { 16 | bounds == CGRect(x: 0, y: 0, width: 320, height: 568) 17 | } 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Extensions/UIKit/UIViewController+CodeMonkeyApple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+CodeMonkeyApple.swift 3 | // Rank Things 4 | // 5 | // Created by Kyle Hughes on 6/12/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | extension UIViewController { 13 | // MARK: Public Instance Interface 14 | 15 | public var viewControllerToPresentOn: UIViewController { 16 | presentedViewController?.viewControllerToPresentOn ?? self 17 | } 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Frame Rate/FrameRate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameRate.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 5/22/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import SwiftUI 11 | 12 | /// - SeeAlso: https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro 13 | public enum FrameRate { 14 | // MARK: Public Static Interface 15 | 16 | public static func set(to preference: Preference) { 17 | DisplayLinkHolder.shared.setFrameRateRange(to: preference.systemValue) 18 | } 19 | } 20 | 21 | // MARK: - FrameRate.Preference Definition 22 | 23 | extension FrameRate { 24 | public enum Preference { 25 | case `default` 26 | case fast 27 | case slow 28 | 29 | // MARK: Public Instance Interface 30 | 31 | var systemValue: CAFrameRateRange { 32 | switch self { 33 | case .default: 34 | return .default 35 | case .fast: 36 | return CAFrameRateRange(minimum: 80, maximum: 120, preferred: 120) 37 | case .slow: 38 | return CAFrameRateRange(minimum: 8, maximum: 15, preferred: 0) 39 | } 40 | } 41 | } 42 | } 43 | 44 | // MARK: - DisplayLinkHolder Definition 45 | 46 | private final class DisplayLinkHolder { 47 | fileprivate static let shared = DisplayLinkHolder() 48 | 49 | private var displayLink: CADisplayLink! 50 | 51 | // MARK: Private Initialization 52 | 53 | private init() { 54 | displayLink = CADisplayLink(target: self, selector: #selector(handler)) 55 | displayLink.add(to: .current, forMode: .default) 56 | } 57 | 58 | // MARK: Fileprivate Instance Interface 59 | 60 | fileprivate func setFrameRateRange(to newValue: CAFrameRateRange) { 61 | displayLink.preferredFrameRateRange = newValue 62 | } 63 | 64 | // MARK: Private Instance Interface 65 | 66 | @objc 67 | private func handler(link: CADisplayLink) { 68 | // NO-OP 69 | } 70 | } 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Global/Global+Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global+Animation.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/17/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @MainActor 11 | public func withAnimationOnMainActor( 12 | _ animation: Animation? = .default, 13 | _ body: () throws -> Result 14 | ) rethrows -> Result { 15 | try withAnimation(animation, body) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Global/Global+Conditional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global+Conditional.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | @inlinable 9 | public func not(_ bool: Bool) -> Bool { 10 | !bool 11 | } 12 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Global/Global+DoOnce.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global+DoOnce.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/15/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @inlinable 11 | public func doOnce(_ boolean: inout Bool, action: () -> Void) { 12 | guard !boolean else { 13 | return 14 | } 15 | 16 | boolean = true 17 | 18 | action() 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Global/Global+Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global+Logging.swift 3 | // CodeMonkeySwift 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | public func describe(_ thing: Any) -> String { 9 | String(describing: thing) 10 | } 11 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Haptic Feedback Generator/HapticFeedbackGenerator+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticFeedbackGenerator+Constants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/14/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | extension HapticFeedbackGenerator.SemanticFeedback { 11 | public static let dismissSheet = Self(.impact(.light)) 12 | public static let presentSheet = Self(.impact(.medium)) 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Haptic Feedback Generator/HapticFeedbackGenerator+StorageKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticFeedbackGenerator+StorageKey.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/3/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | // MARK: - Extension for HapticFeedback 13 | 14 | extension HapticFeedback { 15 | // MARK: Public Instance Interface 16 | 17 | @inlinable 18 | public static func setIsDisabled( 19 | basedOn isDisabledKey: Key, 20 | storage: Storage 21 | ) where Key: StorageKeyProtocol, Key.Value == Bool { 22 | HapticFeedbackGenerator.shared.setIsDisabled(basedOn: isDisabledKey, storage: storage) 23 | } 24 | 25 | @inlinable 26 | public static func setIsEnabled( 27 | basedOn isEnabledKey: Key, 28 | storage: Storage 29 | ) where Key: StorageKeyProtocol, Key.Value == Bool { 30 | HapticFeedbackGenerator.shared.setIsDisabled(basedOn: isEnabledKey, storage: storage) 31 | } 32 | } 33 | 34 | // MARK: - Extension for HapticFeedbackGenerator 35 | 36 | extension HapticFeedbackGenerator { 37 | // MARK: Public Initialization 38 | 39 | public convenience init( 40 | isDisabledKey: Key, 41 | storage: Storage 42 | ) where Key: StorageKeyProtocol, Key.Value == Bool { 43 | self.init( 44 | isDisabledProvider: { 45 | storage.get(isDisabledKey) 46 | } 47 | ) 48 | } 49 | 50 | public convenience init( 51 | isEnabledKey: Key, 52 | storage: Storage 53 | ) where Key: StorageKeyProtocol, Key.Value == Bool { 54 | self.init( 55 | isEnabledProvider: { 56 | storage.get(isEnabledKey) 57 | } 58 | ) 59 | } 60 | 61 | // MARK: Public Instance Interface 62 | 63 | public func setIsDisabled( 64 | basedOn isDisabledKey: Key, 65 | storage: Storage 66 | ) where Key: StorageKeyProtocol, Key.Value == Bool { 67 | setIsDisabled { 68 | storage.get(isDisabledKey) 69 | } 70 | } 71 | 72 | public func setIsEnabled( 73 | basedOn isEnabledKey: Key, 74 | storage: Storage 75 | ) where Key: StorageKeyProtocol, Key.Value == Bool { 76 | setIsEnabled { 77 | storage.get(isEnabledKey) 78 | } 79 | } 80 | } 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Hash/DerivedHash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DerivedHash.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class DerivedHash: Hash { 11 | private var _value: Int { 12 | didSet { 13 | if oldValue != _value { 14 | notifyDependentsOfDependencyUpdate() 15 | } 16 | } 17 | } 18 | 19 | // MARK: Public Initialization 20 | 21 | public init(_ label: String) { 22 | let labelHashValue = label.hashValue 23 | 24 | _value = Self.calculate(for: labelHashValue, from: []) 25 | 26 | super.init(label: label, labelHashValue: labelHashValue) 27 | } 28 | 29 | // MARK: Private Static Interface 30 | 31 | private static func calculate(for labelHashValue: Int, from dependencies: [Hash]) -> Int { 32 | var hasher = Hasher() 33 | 34 | hasher.combine(labelHashValue) 35 | 36 | for dependency in dependencies { 37 | hasher.combine(dependency.value) 38 | } 39 | 40 | return hasher.finalize() 41 | } 42 | 43 | // MARK: Hash Implementation 44 | 45 | public override var value: Int { 46 | _value 47 | } 48 | 49 | public override func dependencyDidUpdate() { 50 | update() 51 | } 52 | 53 | public override func update() { 54 | let oldValue = value 55 | let newValue = Self.calculate(for: labelHashValue, from: dependencies) 56 | 57 | guard oldValue != newValue else { 58 | return 59 | } 60 | 61 | _value = newValue 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Hash/Hash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hash.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | import Foundation 9 | 10 | open class Hash { 11 | public let label: String 12 | 13 | public private(set) var dependencies: [Hash] 14 | public private(set) var dependents: [Hash] 15 | 16 | let labelHashValue: Int 17 | 18 | // MARK: Public Initialization 19 | 20 | public convenience init(_ label: String) { 21 | self.init(label: label, labelHashValue: label.hashValue) 22 | } 23 | 24 | public init(label: String, labelHashValue: Int) { 25 | self.label = label 26 | self.labelHashValue = labelHashValue 27 | 28 | dependencies = [] 29 | dependents = [] 30 | } 31 | 32 | // MARK: Abstract Public Instance Interface 33 | 34 | open var value: Int { 35 | fatalError("Must be implemented in subclass.") 36 | } 37 | 38 | open func dependencyDidUpdate() { 39 | fatalError("Must be implemented in subclass.") 40 | } 41 | 42 | open func update() { 43 | fatalError("Must be implemented in subclass.") 44 | } 45 | 46 | // MARK: Public Instance Interface 47 | 48 | public func addDependency(on dependency: Hash) { 49 | dependencies.append(dependency) 50 | dependency.dependents.append(self) 51 | 52 | dependencyDidUpdate() 53 | } 54 | 55 | public func addingDependency(on dependency: Hash) -> Self { 56 | addDependency(on: dependency) 57 | 58 | return self 59 | } 60 | 61 | // MARK: Internal Instance Interface 62 | 63 | func notifyDependentsOfDependencyUpdate() { 64 | for dependent in dependents { 65 | dependent.dependencyDidUpdate() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Hash/PushedOriginalHash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushedOriginalHash.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | public final class PushedOriginalHash: Hash where Value: Hashable { 9 | private let get: () -> Value 10 | 11 | // MARK: Public Initialization 12 | 13 | public init(_ label: String, get: @escaping () -> Value) { 14 | self.get = get 15 | 16 | super.init(label: label, labelHashValue: label.hashValue) 17 | } 18 | 19 | // MARK: Hash Implementation 20 | 21 | public override var value: Int { 22 | `get`().hashValue 23 | } 24 | 25 | public override func addDependency(on dependency: Hash) { 26 | fatalError("Passthrough hash cannot support dependencies.") 27 | } 28 | 29 | public override func addingDependency(on dependency: Hash) -> Self { 30 | fatalError("Passthrough hash cannot support dependencies.") 31 | } 32 | 33 | public override func dependencyDidUpdate() { 34 | // NO-OP 35 | } 36 | 37 | public override func update() { 38 | notifyDependentsOfDependencyUpdate() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/HealthKit Error/HealthKitError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveToHealthError.swift 3 | // Models 4 | // 5 | // Created by Kyle Hughes on 3/24/21. 6 | // 7 | 8 | #if canImport(HealthKit) 9 | 10 | import HealthKit 11 | 12 | @available(macOS 13.0, *) 13 | public enum HealthKitError { 14 | // NO-OP 15 | } 16 | 17 | // MARK: - HealthKitError.Authorization Definition 18 | 19 | @available(macOS 13.0, *) 20 | extension HealthKitError { 21 | public enum Authorization: Error { 22 | case frameworkError(HKError) 23 | case frameworkFailedInternally 24 | case unknownError(Error) 25 | case userIgnored 26 | } 27 | } 28 | 29 | // MARK: - HealthKitError.Authorization Definition 30 | 31 | @available(macOS 13.0, *) 32 | extension HealthKitError { 33 | public enum AuthorizationAndPersistence: Error { 34 | case authorization(Authorization) 35 | case persistence(Persistence) 36 | } 37 | } 38 | 39 | // MARK: - HealthKitError.SaveObject Definition 40 | 41 | @available(macOS 13.0, *) 42 | extension HealthKitError { 43 | public enum Persistence: Error { 44 | case authorizationNotDetermined 45 | case authorizationDenied 46 | case frameworkFailedInternally 47 | case invalidArgument 48 | case unknownError(Error) 49 | case unknownFrameworkError(HKError) 50 | } 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Identified/Identified.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Identified.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/12/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Identified: Identifiable { 11 | public let id: UUID 12 | 13 | public var value: Value 14 | 15 | // MARK: Public Initialization 16 | 17 | public init(_ value: Value) { 18 | self.value = value 19 | 20 | id = UUID() 21 | } 22 | 23 | public init(id: UUID, value: Value) { 24 | self.id = id 25 | self.value = value 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Image Processing/Image Filters/Dither/ImageFilters+Dither+AppKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFilters+Dither+AppKit.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 9 | 10 | import Accelerate 11 | import AppKit 12 | 13 | extension ImageFilters.Dither { 14 | // MARK: Public Static Interface 15 | 16 | public static func apply(to image: NSImage, targetWidth: CGFloat? = nil) throws -> NSImage { 17 | NSImage(cgImage: try apply(to: try makeCGImage(from: image), targetWidth: targetWidth), size: NSZeroSize) 18 | } 19 | 20 | // MARK: Private Static Interface 21 | 22 | private static func makeCGImage(from image: NSImage) throws -> CGImage { 23 | guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 24 | throw Error.cannotCreateInputCGImage 25 | } 26 | 27 | return cgImage 28 | } 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Image Processing/Image Filters/Dither/ImageFilters+Dither+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DitherImageFilter+UIKit.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | #if canImport(UIKit) 9 | 10 | import Accelerate 11 | import UIKit 12 | 13 | extension ImageFilters.Dither { 14 | // MARK: Public Static Interface 15 | 16 | public static func apply(to image: UIImage, targetWidth: CGFloat? = nil) throws -> UIImage { 17 | UIImage(cgImage: try apply(to: try makeCGImage(from: image), targetWidth: targetWidth)) 18 | } 19 | 20 | // MARK: Private Static Interface 21 | 22 | private static func makeCGImage(from image: UIImage) throws -> CGImage { 23 | guard let cgImage = image.cgImage else { 24 | throw Error.cannotCreateInputCGImage 25 | } 26 | 27 | return cgImage 28 | } 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Image Processing/Image Filters/ImageFilters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFilters.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | public enum ImageFilters { 9 | // NO-OP 10 | } 11 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Image Processing/VectorImageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VectorImageError.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/6/21. 6 | // 7 | 8 | import Accelerate 9 | 10 | public enum VectorImageError: Error { 11 | case frameworkError(vImage_Error) 12 | } 13 | 14 | // MARK: - Global Functions 15 | 16 | public func throwableVectorImageAction(action: () -> vImage_Error) throws { 17 | let error = action() 18 | 19 | guard error == kvImageNoError else { 20 | throw VectorImageError.frameworkError(error) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Info.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Info.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 1/24/21. 6 | // 7 | 8 | public enum Info { 9 | // MARK: Public Static Properties 10 | 11 | public static let id = ReverseDomainName.es_kylehugh_codeMonkey_apple 12 | } 13 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Loadable/Loadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loadable.swift 3 | // Save My Streak 4 | // 5 | // Created by Kyle Hughes on 4/23/23. 6 | // 7 | 8 | @dynamicMemberLookup 9 | public enum Loadable { 10 | case loaded(Value) 11 | case loading 12 | 13 | // MARK: Public Subscript Interface 14 | 15 | @inlinable 16 | public subscript(dynamicMember keyPath: KeyPath) -> Loadable { 17 | switch self { 18 | case let .loaded(value): 19 | return .loaded(value[keyPath: keyPath]) 20 | case .loading: 21 | return .loading 22 | } 23 | } 24 | 25 | // MARK: Public Instance Interface 26 | 27 | @inlinable 28 | public var isLoaded: Bool { 29 | switch self { 30 | case .loaded: 31 | return true 32 | case .loading: 33 | return false 34 | } 35 | } 36 | 37 | @inlinable 38 | public var isLoading: Bool { 39 | switch self { 40 | case .loaded: 41 | return false 42 | case .loading: 43 | return true 44 | } 45 | } 46 | 47 | @inlinable 48 | public func map(_ transform: (Value) throws -> Other) rethrows -> Loadable { 49 | switch self { 50 | case let .loaded(value): 51 | return try .loaded(transform(value)) 52 | case .loading: 53 | return .loading 54 | } 55 | } 56 | 57 | @inlinable 58 | public func resolve(replacingLoadingWith replacement: @autoclosure () -> Value) -> Value { 59 | switch self { 60 | case let .loaded(value): 61 | return value 62 | case .loading: 63 | return replacement() 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Equatable Extension 69 | 70 | extension Loadable: Equatable where Value: Equatable { 71 | // NO-OP 72 | } 73 | 74 | // MARK: - Extension where Value == Void 75 | 76 | extension Loadable where Value == Void { 77 | // MARK: Public Static Interface 78 | 79 | @inlinable 80 | public static var loaded: Self { 81 | .loaded(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Notification Center Delegate/NotificationCenterDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenterDelegate.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 4/19/22. 6 | // 7 | 8 | import Combine 9 | import UserNotifications 10 | 11 | public final class NotificationCenterDelegate: NSObject { 12 | private let goToNotificationSettingsSubject: CurrentValueSubject 13 | 14 | // MARK: Public Initialization 15 | 16 | public init(userNotificationCenter: UNUserNotificationCenter = .current()) { 17 | goToNotificationSettingsSubject = CurrentValueSubject(false) 18 | 19 | super.init() 20 | 21 | userNotificationCenter.delegate = self 22 | } 23 | 24 | // MARK: Public Instance Interface 25 | 26 | public func didGoToNotificationSettings() { 27 | goToNotificationSettingsSubject.value = false 28 | } 29 | 30 | public var goToNotificationSettings: AnyPublisher { 31 | goToNotificationSettingsSubject 32 | .removeDuplicates() 33 | .eraseToAnyPublisher() 34 | } 35 | } 36 | 37 | // MARK: - UNUserNotificationCenterDelegate Extension 38 | 39 | extension NotificationCenterDelegate: UNUserNotificationCenterDelegate { 40 | #if !os(watchOS) 41 | 42 | // MARK: Displaying Notification Settings 43 | 44 | public func userNotificationCenter( 45 | _ center: UNUserNotificationCenter, 46 | openSettingsFor notification: UNNotification? 47 | ) { 48 | goToNotificationSettingsSubject.value = true 49 | } 50 | 51 | #endif 52 | } 53 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Orderable/Orderable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Orderable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 5/30/21. 6 | // 7 | 8 | public protocol Orderable { 9 | associatedtype Ordinal: SignedInteger 10 | 11 | // MARK: Instance Interface 12 | 13 | var ordinal: Ordinal { get } 14 | } 15 | 16 | // MARK: - Extension for Collection 17 | 18 | extension Collection where Element: Orderable { 19 | // MARK: Public Instance Interface 20 | 21 | public var largestOrdinal: Element.Ordinal? { 22 | ordered().map(\.ordinal).last 23 | } 24 | 25 | public var nextOrdinal: Element.Ordinal { 26 | guard let largestOrdinal = largestOrdinal else { 27 | return 1 28 | } 29 | 30 | return largestOrdinal + 1 31 | } 32 | 33 | public func ordered() -> [Element] { 34 | sorted { 35 | $0.ordinal < $1.ordinal 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Orderable/OrdinalMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrdinalMap.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 5/31/21. 6 | // 7 | 8 | public struct OrdinalMap where Key: Hashable, OrderableType: Orderable { 9 | public let largestOrdinal: OrderableType.Ordinal? 10 | public let nextOrdinal: OrderableType.Ordinal 11 | 12 | private var storage: [Key: OrderableType.Ordinal] 13 | 14 | // MARK: Public Initialization 15 | 16 | public init( 17 | orderedElements: [Element] 18 | ) 19 | where 20 | Element: Identifiable, 21 | OrderableType: Identifiable, 22 | OrderableType.ID == Element.ID, 23 | OrderableType.ID == Key 24 | { 25 | largestOrdinal = 0 < orderedElements.count ? OrderableType.Ordinal(orderedElements.count) : nil 26 | nextOrdinal = (largestOrdinal ?? 0) + 1 27 | storage = [:] 28 | 29 | for (index, element) in orderedElements.enumerated() { 30 | storage[element.id] = OrderableType.Ordinal(index + 1) 31 | } 32 | } 33 | 34 | // MARK: Public Subscripts 35 | 36 | public subscript(key: Key) -> OrderableType.Ordinal? { 37 | storage[key] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Property Wrappers/Build-Dependent Value/BuildDependentValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildDependentValue.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/20/21. 6 | // 7 | 8 | @propertyWrapper 9 | public struct BuildDependentValue { 10 | public let wrappedValue: Value 11 | 12 | // MARK: Public Initialization 13 | 14 | public init( 15 | debug: Value, 16 | release: Value 17 | ) { 18 | #if DEBUG 19 | wrappedValue = debug 20 | #else 21 | wrappedValue = release 22 | #endif 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Property Wrappers/Screen-Scale-Dependent Value/ScreenScaleDependentValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenScaleDependentValue.swift 3 | // Super Headache 4 | // 5 | // Created by Kyle Hughes on 4/3/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | @propertyWrapper 13 | public struct ScreenScaleDependentValue { 14 | public let wrappedValue: Value 15 | 16 | // MARK: Public Initialization 17 | 18 | public init( 19 | pixels: @autoclosure () -> Value 20 | ) where Value: BinaryFloatingPoint { 21 | wrappedValue = pixels() / Value(UIScreen.main.scale) 22 | } 23 | 24 | public init( 25 | x: @autoclosure () -> Value, 26 | xx: @autoclosure () -> Value, 27 | xxx: @autoclosure () -> Value 28 | ) { 29 | switch UIScreen.main.scale { 30 | case 1: 31 | wrappedValue = x() 32 | case 2: 33 | wrappedValue = xx() 34 | case 3: 35 | wrappedValue = xxx() 36 | default: 37 | wrappedValue = x() 38 | } 39 | } 40 | } 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Relative Direction/ZRelativeDirection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZRelativeDirection.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/18/22. 6 | // 7 | 8 | public enum ZRelativeDirection: SynthesizedIdentifiable { 9 | case down 10 | case up 11 | 12 | // MARK: Public Instance Interface 13 | 14 | public var opposite: Self { 15 | switch self { 16 | case .down: 17 | return .up 18 | case .up: 19 | return .down 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Resources/String+Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Resources.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/3/22. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Common Strings 11 | 12 | extension String { 13 | public static let `default` = NSLocalizedString( 14 | "DEFAULT", 15 | bundle: .module, 16 | value: "Default", 17 | comment: "Label for a value that is the default selection." 18 | ) 19 | } 20 | 21 | // MARK: - Specific Strings 22 | 23 | extension String { 24 | // MARK: Runtime Accent Color 25 | 26 | public static let blackWhiteColor = NSLocalizedString( 27 | "COLOR_BLACK_WHITE", 28 | bundle: .module, 29 | value: "Black & White", 30 | comment: "Label for a color that can be either black or white." 31 | ) 32 | 33 | public static let blueColor = NSLocalizedString( 34 | "COLOR_BLUE", 35 | bundle: .module, 36 | value: "Blue", 37 | comment: "Name of the color blue." 38 | ) 39 | 40 | public static let brownColor = NSLocalizedString( 41 | "COLOR_BROWN", 42 | bundle: .module, 43 | value: "Brown", 44 | comment: "Name of the color brown." 45 | ) 46 | 47 | public static let coralColor = NSLocalizedString( 48 | "COLOR_CORAL", 49 | bundle: .module, 50 | value: "Coral", 51 | comment: "Name of the color coral." 52 | ) 53 | 54 | public static let cyanColor = NSLocalizedString( 55 | "COLOR_CYAN", 56 | bundle: .module, 57 | value: "Cyan", 58 | comment: "Name of the color cyan." 59 | ) 60 | 61 | public static let greenColor = NSLocalizedString( 62 | "COLOR_GREEN", 63 | bundle: .module, 64 | value: "Green", 65 | comment: "Name of the color green." 66 | ) 67 | 68 | public static let indigoColor = NSLocalizedString( 69 | "COLOR_INDIGO", 70 | bundle: .module, 71 | value: "Indigo", 72 | comment: "Name of the color indigo." 73 | ) 74 | 75 | public static let mintColor = NSLocalizedString( 76 | "COLOR_MINT", 77 | bundle: .module, 78 | value: "Mint", 79 | comment: "Name of the color mint." 80 | ) 81 | 82 | public static let orangeColor = NSLocalizedString( 83 | "COLOR_ORANGE", 84 | bundle: .module, 85 | value: "Orange", 86 | comment: "Name of the color orange." 87 | ) 88 | 89 | public static let pinkColor = NSLocalizedString( 90 | "COLOR_PINK", 91 | bundle: .module, 92 | value: "Pink", 93 | comment: "Name of the color pink." 94 | ) 95 | 96 | public static let purpleColor = NSLocalizedString( 97 | "COLOR_PURPLE", 98 | bundle: .module, 99 | value: "Purple", 100 | comment: "Name of the color purple." 101 | ) 102 | 103 | public static let redColor = NSLocalizedString( 104 | "COLOR_RED", 105 | bundle: .module, 106 | value: "Red", 107 | comment: "Name of the color red." 108 | ) 109 | 110 | public static let tealColor = NSLocalizedString( 111 | "COLOR_TEAL", 112 | bundle: .module, 113 | value: "Teal", 114 | comment: "Name of the color teal." 115 | ) 116 | 117 | public static let yellowColor = NSLocalizedString( 118 | "COLOR_YELLOW", 119 | bundle: .module, 120 | value: "Yellow", 121 | comment: "Name of the color yellow." 122 | ) 123 | 124 | // MARK: Runtime Color Scheme 125 | 126 | public static let darkColorScheme = NSLocalizedString( 127 | "COLOR_SCHEME_DARK", 128 | bundle: .module, 129 | value: "Dark", 130 | comment: "Title for a dark color scheme; most colors are dark with a black background." 131 | ) 132 | 133 | public static let lightColorScheme = NSLocalizedString( 134 | "COLOR_SCHEME_LIGHT", 135 | bundle: .module, 136 | value: "Light", 137 | comment: "Title for a dark color scheme; most colors are light with a white background." 138 | ) 139 | 140 | public static let systemColorScheme = NSLocalizedString( 141 | "COLOR_SCHEME_SYSTEM", 142 | bundle: .module, 143 | value: "System", 144 | comment: "Title for the color scheme that is set at the operating-system level and applies to all apps." 145 | ) 146 | } 147 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Resources/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylehughes/code-monkey-apple/e07111729273c920880514cdea6ebd2df5c19431/Sources/CodeMonkeyApple/Resources/de.lproj/Localizable.strings -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylehughes/code-monkey-apple/e07111729273c920880514cdea6ebd2df5c19431/Sources/CodeMonkeyApple/Resources/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Runtime Accent Color/RuntimeAccentColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuntimeAccentColor.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/10/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public enum RuntimeAccentColor: String, CaseIterable, Storable, SynthesizedIdentifiable { 11 | case blue 12 | case brown 13 | case coral 14 | case cyan 15 | case `default` 16 | case green 17 | case indigo 18 | case mint 19 | case orange 20 | case pink 21 | case primary 22 | case purple 23 | case red 24 | case teal 25 | case yellow 26 | 27 | // MARK: Public Static Interface 28 | 29 | public static let allCasesInDisplayOrder: [RuntimeAccentColor] = allCases.sorted { $0.title < $1.title } 30 | 31 | public static let defaultEquivalent: RuntimeAccentColor = { 32 | #if canImport(UIKit) 33 | for color in allCases { 34 | guard 35 | color != .default, 36 | color.platformValue.rgbaEqualsRGBA(from: RuntimeAccentColor.default.platformValue) 37 | else { 38 | continue 39 | } 40 | 41 | return color 42 | } 43 | #endif 44 | 45 | return .default 46 | }() 47 | 48 | // MARK: Public Instance Interface 49 | 50 | public var nonDefaultEquivalent: RuntimeAccentColor { 51 | guard self == .default else { 52 | return self 53 | } 54 | 55 | return .defaultEquivalent 56 | } 57 | 58 | public var platformValue: Color { 59 | switch self { 60 | case .blue: 61 | return .blue 62 | case .brown: 63 | return .brown 64 | case .coral: 65 | return Color(.displayP3, red: 255/255, green: 130/255, blue: 119/255, opacity: 1.0) 66 | case .cyan: 67 | return .cyan 68 | case .default: 69 | return Color("AccentColor") 70 | case .green: 71 | return .green 72 | case .indigo: 73 | return .indigo 74 | case .mint: 75 | return .mint 76 | case .orange: 77 | return .orange 78 | case .pink: 79 | return .pink 80 | case .primary: 81 | return .primary 82 | case .purple: 83 | return .purple 84 | case .red: 85 | return .red 86 | case .teal: 87 | return .teal 88 | case .yellow: 89 | return .yellow 90 | } 91 | } 92 | 93 | public var title: String { 94 | switch self { 95 | case .blue: 96 | return .blueColor 97 | case .brown: 98 | return .brownColor 99 | case .coral: 100 | return .coralColor 101 | case .cyan: 102 | return .cyanColor 103 | case .default: 104 | return .default 105 | case .green: 106 | return .greenColor 107 | case .indigo: 108 | return .indigoColor 109 | case .mint: 110 | return .mintColor 111 | case .orange: 112 | return .orangeColor 113 | case .pink: 114 | return .pinkColor 115 | case .primary: 116 | return .blackWhiteColor 117 | case .purple: 118 | return .purpleColor 119 | case .red: 120 | return .redColor 121 | case .teal: 122 | return .tealColor 123 | case .yellow: 124 | return .yellowColor 125 | } 126 | } 127 | } 128 | 129 | // MARK: - Extension for View 130 | 131 | extension View { 132 | // MARK: Public Instance Interface 133 | 134 | public func accentColor(_ runtimeAccentColor: RuntimeAccentColor) -> some View { 135 | accentColor(runtimeAccentColor.platformValue) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Runtime Color Scheme/RuntimeColorScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuntimeColorScheme.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/4/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public enum RuntimeColorScheme: String, Codable, Storable, SynthesizedIdentifiable { 11 | case dark = "dark" 12 | case light = "light" 13 | case system = "system" 14 | 15 | // MARK: Public Static Interface 16 | 17 | public static let allCasesInDisplayOrder: [RuntimeColorScheme] = [ 18 | .system, 19 | .light, 20 | .dark, 21 | ] 22 | 23 | // MARK: Public Instance Interface 24 | 25 | public var platformValue: ColorScheme? { 26 | switch self { 27 | case .dark: 28 | return .dark 29 | case .light: 30 | return .light 31 | case .system: 32 | return nil 33 | } 34 | } 35 | 36 | public var title: String { 37 | switch self { 38 | case .dark: 39 | return .darkColorScheme 40 | case .light: 41 | return .lightColorScheme 42 | case .system: 43 | return .systemColorScheme 44 | } 45 | } 46 | } 47 | 48 | // MARK: - Extension for View 49 | 50 | extension View { 51 | // MARK: Public Instance Interface 52 | 53 | public func preferredColorScheme(_ runtimeColorScheme: RuntimeColorScheme) -> some View { 54 | preferredColorScheme(runtimeColorScheme.platformValue) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage Keys/StorageKey+Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageKey+Debug.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/20/22. 6 | // 7 | 8 | // MARK: - Boolean Debug Storage Keys 9 | 10 | extension StorageKeyProtocol where Self == DebugStorageKey { 11 | // MARK: Public Static Interface 12 | 13 | public static var enableAppStoreRating: Self { 14 | Self( 15 | id: "EnableAppStoreRating", 16 | defaultValue: true 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/CompositeStorable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositeStorable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/13/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CompositeStorable: Storable where StorableValue == Components { 11 | // MARK: Associated Types 12 | 13 | associatedtype Components: Storable 14 | 15 | // MARK: Static Interface 16 | 17 | static func compose(from components: Components) -> Self 18 | } 19 | 20 | // MARK: - Storable Implementation 21 | 22 | extension CompositeStorable { 23 | // MARK: Public Static Interface 24 | 25 | public static func decode(from storage: @autoclosure () -> Components?) -> Self? { 26 | guard let components = storage() else { 27 | return nil 28 | } 29 | 30 | return compose(from: components) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/CompositeStorageKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositeStorageKey.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/12/22. 6 | // 7 | 8 | import Foundation 9 | 10 | // TODO: Replace Tuple(2|3) usage with variadic generics when available. 11 | 12 | public struct CompositeStorageKey 13 | where 14 | Value: CompositeStorable, 15 | ComposedKey: StorageKeyProtocol, 16 | Value.Components == ComposedKey.Value 17 | { 18 | @StorageKeyBuilder private let compose: () -> ComposedKey 19 | 20 | // MARK: Public Initialization 21 | 22 | public init(@StorageKeyBuilder compose: @escaping () -> ComposedKey) { 23 | self.compose = compose 24 | } 25 | } 26 | 27 | // MARK: - StorageKeyProtocol Extension 28 | 29 | extension CompositeStorageKey: StorageKeyProtocol { 30 | // MARK: Public Instance Interface 31 | 32 | public var compositeIDs: Set { 33 | compose().compositeIDs 34 | } 35 | 36 | public var defaultValue: Value { 37 | Value.compose(from: compose().defaultValue) 38 | } 39 | 40 | public func get(from userDefaults: UserDefaults) -> Value { 41 | Value.compose(from: compose().get(from: userDefaults)) 42 | } 43 | 44 | public func remove(from userDefaults: UserDefaults) { 45 | compose().remove(from: userDefaults) 46 | } 47 | 48 | public func set(to newValue: Value, in userDefaults: UserDefaults) { 49 | compose().set(to: newValue.encodeForStorage(), in: userDefaults) 50 | } 51 | 52 | #if !os(watchOS) 53 | 54 | public func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value { 55 | Value.compose(from: compose().get(from: ubiquitousStore)) 56 | } 57 | 58 | public func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) { 59 | compose().remove(from: ubiquitousStore) 60 | } 61 | 62 | public func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 63 | compose().set(to: newValue.encodeForStorage(), in: ubiquitousStore) 64 | } 65 | 66 | #endif 67 | } 68 | 69 | // MARK: - Example 70 | 71 | //struct Example: CompositeStorable { 72 | // typealias Components = Tuple3 73 | // 74 | // let string: String 75 | // let number: Int 76 | // let date: Date 77 | // 78 | // static func compose(from components: Components) -> Example { 79 | // Example(string: components.e1, number: components.e2, date: components.e3) 80 | // } 81 | // 82 | // func encodeForStorage() -> Components { 83 | // Components(string, number, date) 84 | // } 85 | //} 86 | // 87 | //extension StorageKeyProtocol 88 | //where 89 | // Self == CompositeStorageKey, StorageKey, StorageKey>> 90 | //{ 91 | // static var example: CompositeStorageKey, StorageKey, StorageKey>> { 92 | // CompositeStorageKey(id: "example") { 93 | // StorageKey.string 94 | // StorageKey.number 95 | // StorageKey.date 96 | // } 97 | // } 98 | //} 99 | // 100 | //extension StorageKey where Value == Date { 101 | // static var date: StorageKey { 102 | // StorageKey(id: "Date", defaultValue: .now) 103 | // } 104 | //} 105 | // 106 | //extension StorageKey where Value == Int { 107 | // static var number: StorageKey { 108 | // StorageKey(id: "Number", defaultValue: 2) 109 | // } 110 | //} 111 | // 112 | //extension StorageKey where Value == String { 113 | // static var string: StorageKey { 114 | // StorageKey(id: "String", defaultValue: "lol") 115 | // } 116 | //} 117 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/DebugStorageKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugStorageKey.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DebugStorageKey: Identifiable, StorageKeyProtocol where Value: Storable { 11 | public let defaultValue: Value 12 | public let id: String 13 | 14 | // MARK: Public Initialization 15 | 16 | public init(id: String, defaultValue: Value) { 17 | self.id = id 18 | self.defaultValue = defaultValue 19 | } 20 | } 21 | 22 | // MARK: - StorageKeyProtocol Extension 23 | 24 | extension DebugStorageKey { 25 | // MARK: Public Instance Interface 26 | 27 | public var compositeIDs: Set { 28 | [id] 29 | } 30 | 31 | public func get(from userDefaults: UserDefaults) -> Value { 32 | #if DEBUG 33 | .decode(for: self, from: Value.extract(self, from: userDefaults)) 34 | #else 35 | defaultValue 36 | #endif 37 | } 38 | 39 | public func set(to newValue: Value, in userDefaults: UserDefaults) { 40 | #if DEBUG 41 | newValue.store(newValue.encodeForStorage(), as: id, in: userDefaults) 42 | #else 43 | // NO-OP 44 | #endif 45 | } 46 | 47 | public func remove(from userDefaults: UserDefaults) { 48 | #if DEBUG 49 | userDefaults.removeObject(forKey: id) 50 | #else 51 | // NO-OP 52 | #endif 53 | } 54 | 55 | #if !os(watchOS) 56 | 57 | public func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value { 58 | #if DEBUG 59 | .decode(for: self, from: Value.extract(self, from: ubiquitousStore)) 60 | #else 61 | defaultValue 62 | #endif 63 | } 64 | 65 | public func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 66 | #if DEBUG 67 | newValue.store(newValue.encodeForStorage(), as: id, in: ubiquitousStore) 68 | #else 69 | // NO-OP 70 | #endif 71 | } 72 | 73 | public func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) { 74 | #if DEBUG 75 | ubiquitousStore.removeObject(forKey: id) 76 | #else 77 | // NO-OP 78 | #endif 79 | } 80 | 81 | #endif 82 | } 83 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/Extensions/UIContentSizeCategory+Storable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIContentSizeCategory+Storable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import Foundation 11 | import UIKit 12 | 13 | extension UIContentSizeCategory: Storable { 14 | // NO-OP 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/Implementations/InMemoryStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryStorage.swift 3 | // Super Headache 4 | // 5 | // Created by Kyle Hughes on 3/23/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class InMemoryStorage { 11 | private var storage: [String: Any] 12 | 13 | // MARK: Public Initialization 14 | 15 | public init() { 16 | storage = [:] 17 | } 18 | 19 | // MARK: Private Instance Interface 20 | 21 | private subscript(key: String) -> Any? { 22 | get { 23 | storage[key] 24 | } 25 | set { 26 | storage[key] = newValue 27 | } 28 | } 29 | } 30 | 31 | // MARK: - Storage Extension 32 | 33 | extension InMemoryStorage: Storage { 34 | // MARK: Getting Values 35 | 36 | public var dictionaryRepresentation: [String : Any] { 37 | storage 38 | } 39 | 40 | public func get(_ key: Key) -> Key.Value where Key: StorageKeyProtocol { 41 | self[key.id] as? Key.Value ?? key.defaultValue 42 | } 43 | 44 | // MARK: Setting Values 45 | 46 | public func set(_ key: Key, to value: Key.Value) where Key: StorageKeyProtocol { 47 | self[key.id] = value 48 | } 49 | 50 | // MARK: Removing Values 51 | 52 | public func remove(_ key: Key) where Key: StorageKeyProtocol { 53 | storage.removeValue(forKey: key.id) 54 | } 55 | 56 | // MARK: Observing Keys 57 | 58 | public func deregister( 59 | observer target: NSObject, 60 | for key: Key, 61 | with context: UnsafeMutableRawPointer? 62 | ) where Key: StorageKeyProtocol { 63 | fatalError("I should implement this.") 64 | } 65 | 66 | public func register( 67 | observer target: NSObject, 68 | for key: Key, 69 | with context: UnsafeMutableRawPointer?, 70 | valueWillChange: () -> Void 71 | ) where Key: StorageKeyProtocol { 72 | fatalError("I should implement this.") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/Implementations/UbiquitousStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UbquitousStorage.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | #if !os(watchOS) 11 | 12 | extension NSUbiquitousKeyValueStore: Storage { 13 | public static let didChangeInternallyNotification = NSNotification.Name( 14 | "NSUbiquitousKeyValueStore.DidChangeInternally" 15 | ) 16 | 17 | // MARK: Getting Values 18 | 19 | @inlinable 20 | public func get(_ key: Key) -> Key.Value where Key : StorageKeyProtocol { 21 | key.get(from: self) 22 | } 23 | 24 | // MARK: Setting Values 25 | 26 | @inlinable 27 | public func set(_ key: Key, to value: Key.Value) where Key : StorageKeyProtocol { 28 | key.set(to: value, in: self) 29 | 30 | postInternalChangeNotification(for: key) 31 | } 32 | 33 | // MARK: Removing Values 34 | 35 | @inlinable 36 | public func remove(_ key: Key) where Key: StorageKeyProtocol { 37 | key.remove(from: self) 38 | 39 | postInternalChangeNotification(for: key) 40 | } 41 | 42 | // MARK: Observing Keys 43 | 44 | @inlinable 45 | public func deregister( 46 | observer target: NSObject, 47 | for key: Key, 48 | with context: UnsafeMutableRawPointer? 49 | ) where Key: StorageKeyProtocol { 50 | NotificationCenter.default.removeObserver(self, name: Self.didChangeExternallyNotification, object: self) 51 | NotificationCenter.default.removeObserver(self, name: Self.didChangeInternallyNotification, object: self) 52 | } 53 | 54 | @inlinable 55 | public func register( 56 | observer target: NSObject, 57 | for key: Key, 58 | with context: UnsafeMutableRawPointer?, 59 | valueWillChange: @escaping () -> Void 60 | ) where Key: StorageKeyProtocol { 61 | NotificationCenter.default.addObserver( 62 | forName: Self.didChangeExternallyNotification, 63 | object: self, 64 | queue: nil 65 | ) { 66 | self.didObserveChange(for: key, via: $0, valueWillChange: valueWillChange) 67 | } 68 | 69 | NotificationCenter.default.addObserver( 70 | forName: Self.didChangeInternallyNotification, 71 | object: self, 72 | queue: nil 73 | ) { 74 | self.didObserveChange(for: key, via: $0, valueWillChange: valueWillChange) 75 | } 76 | } 77 | 78 | // MARK: Internal Instance Interface 79 | 80 | internal static let changedKeysKey = AnyHashable(NSUbiquitousKeyValueStoreChangedKeysKey) 81 | 82 | @usableFromInline 83 | internal func didObserveChange( 84 | for key: Key, 85 | via notification: Notification, 86 | valueWillChange: @escaping () -> Void 87 | ) where Key: StorageKeyProtocol { 88 | guard let changedKeyIDs = notification.userInfo?[Self.changedKeysKey] as? [String] else { 89 | return 90 | } 91 | 92 | let changedKeyIDsSet = Set(changedKeyIDs) 93 | 94 | guard not(changedKeyIDsSet.union(key.compositeIDs).isEmpty) else { 95 | return 96 | } 97 | 98 | valueWillChange() 99 | } 100 | 101 | @usableFromInline 102 | internal func postInternalChangeNotification(for key: Key) where Key: StorageKeyProtocol { 103 | NotificationCenter.default.post( 104 | name: Self.didChangeInternallyNotification, 105 | object: self, 106 | userInfo: [ 107 | Self.changedKeysKey: Array(key.compositeIDs) 108 | ] 109 | ) 110 | } 111 | } 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/Implementations/UserDefaultsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsStorage.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension UserDefaults: Storage { 11 | // MARK: Getting Values 12 | 13 | public var dictionaryRepresentation: [String : Any] { 14 | dictionaryRepresentation() 15 | } 16 | 17 | @inlinable 18 | public func get(_ key: Key) -> Key.Value where Key: StorageKeyProtocol { 19 | key.get(from: self) 20 | } 21 | 22 | // MARK: Setting Values 23 | 24 | @inlinable 25 | public func set(_ key: Key, to value: Key.Value) where Key: StorageKeyProtocol { 26 | key.set(to: value, in: self) 27 | } 28 | 29 | // MARK: Removing Values 30 | 31 | @inlinable 32 | public func remove(_ key: Key) where Key: StorageKeyProtocol { 33 | key.remove(from: self) 34 | } 35 | 36 | // MARK: Observing Keys 37 | 38 | public func deregister( 39 | observer target: NSObject, 40 | for key: Key, 41 | with context: UnsafeMutableRawPointer? 42 | ) where Key: StorageKeyProtocol { 43 | for keyID in key.compositeIDs { 44 | removeObserver(target, forKeyPath: keyID, context: context) 45 | } 46 | } 47 | 48 | public func register( 49 | observer target: NSObject, 50 | for key: Key, 51 | with context: UnsafeMutableRawPointer?, 52 | valueWillChange: () -> Void 53 | ) where Key: StorageKeyProtocol { 54 | for keyID in key.compositeIDs { 55 | addObserver(target, forKeyPath: keyID, context: context) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/StorableCodableWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorableCodableWrapper.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/16/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Prevents the need to manually implement Codable for all of the types that we want to store in `@AppStorage` as 11 | /// `RawRepresentable` `String`s. 12 | @dynamicMemberLookup 13 | public struct StorableCodableWrapper where Value: Codable { 14 | public var storedValue: Value 15 | 16 | // MARK: Public Initialization 17 | 18 | public init(_ storedValue: Value) { 19 | self.storedValue = storedValue 20 | } 21 | 22 | // MARK: Public Subscripts 23 | 24 | public subscript(dynamicMember keyPath: KeyPath) -> PropertyValue { 25 | storedValue[keyPath: keyPath] 26 | } 27 | } 28 | 29 | // MARK: - Decodable Extension 30 | 31 | extension StorableCodableWrapper: Decodable { 32 | // MARK: Public Initialization 33 | 34 | public init(from decoder: Decoder) throws { 35 | storedValue = try Value(from: decoder) 36 | } 37 | } 38 | 39 | // MARK: - Encodable Extension 40 | 41 | extension StorableCodableWrapper: Encodable { 42 | // MARK: Public Instance Interface 43 | 44 | public func encode(to encoder: Encoder) throws { 45 | try storedValue.encode(to: encoder) 46 | } 47 | } 48 | 49 | // MARK: - RawRepresentable Extension 50 | 51 | extension StorableCodableWrapper: RawRepresentable { 52 | // MARK: Public Initialization 53 | 54 | public init?(rawValue: String) { 55 | guard 56 | let data = rawValue.data(using: .utf8), 57 | let color = try? JSONDecoder.default.decode(Self.self, from: data) 58 | else { 59 | return nil 60 | } 61 | 62 | self = color 63 | } 64 | 65 | // MARK: Public Instance Interface 66 | 67 | public var rawValue: String { 68 | try! String(data: JSONEncoder.default.encode(self), encoding: .utf8)! 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storage.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Storage { 11 | // MARK: Getting Values 12 | 13 | var dictionaryRepresentation: [String: Any] { get } 14 | 15 | func get(_ key: Key) -> Key.Value where Key: StorageKeyProtocol 16 | 17 | // MARK: Setting Values 18 | 19 | func set(_ key: Key, to value: Key.Value) where Key: StorageKeyProtocol 20 | 21 | // MARK: Removing Values 22 | 23 | func remove(_ key: Key) where Key: StorageKeyProtocol 24 | 25 | // MARK: Observing Keys 26 | 27 | func deregister( 28 | observer target: NSObject, 29 | for key: Key, 30 | with context: UnsafeMutableRawPointer? 31 | ) where Key: StorageKeyProtocol 32 | 33 | func register( 34 | observer target: NSObject, 35 | for key: Key, 36 | with context: UnsafeMutableRawPointer?, 37 | valueWillChange: @escaping () -> Void 38 | ) where Key: StorageKeyProtocol 39 | } 40 | 41 | #if DEBUG 42 | // MARK: - Preview 43 | 44 | extension Storage where Self == InMemoryStorage { 45 | // MARK: Public Static Interface 46 | 47 | public static var preview: Self { 48 | InMemoryStorage() 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/StorageKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageKey.swift 3 | // Storage 4 | // 5 | // Created by Kyle Hughes on 3/28/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct StorageKey: Identifiable where Value: Storable { 11 | public let defaultValue: Value 12 | public let id: String 13 | 14 | // MARK: Public Initialization 15 | 16 | public init(id: String, defaultValue: Value) { 17 | self.id = id 18 | self.defaultValue = defaultValue 19 | } 20 | } 21 | 22 | // MARK: - Conditional Codable Extension 23 | 24 | extension StorageKey: Codable where Value: Codable { 25 | // NO-OP 26 | } 27 | 28 | // MARK: - Conditional Equatable Extension 29 | 30 | extension StorageKey: Equatable where Value: Equatable { 31 | // NO-OP 32 | } 33 | 34 | // MARK: - Conditional Hashable Extension 35 | 36 | extension StorageKey: Hashable where Value: Hashable { 37 | // NO-OP 38 | } 39 | 40 | // MARK: - StorageKeyProtocol Extension 41 | 42 | extension StorageKey: StorageKeyProtocol { 43 | // MARK: Public Instance Interface 44 | 45 | public var compositeIDs: Set { 46 | [id] 47 | } 48 | 49 | public func get(from userDefaults: UserDefaults) -> Value { 50 | .decode(for: self, from: Value.extract(self, from: userDefaults)) 51 | } 52 | 53 | public func remove(from userDefaults: UserDefaults) { 54 | userDefaults.removeObject(forKey: id) 55 | } 56 | 57 | public func set(to newValue: Value, in userDefaults: UserDefaults) { 58 | newValue.store(newValue.encodeForStorage(), as: id, in: userDefaults) 59 | } 60 | 61 | #if !os(watchOS) 62 | 63 | public func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value { 64 | .decode(for: self, from: Value.extract(self, from: ubiquitousStore)) 65 | } 66 | 67 | public func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) { 68 | ubiquitousStore.removeObject(forKey: id) 69 | } 70 | 71 | public func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 72 | newValue.store(newValue.encodeForStorage(), as: id, in: ubiquitousStore) 73 | } 74 | 75 | #endif 76 | } 77 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/StorageKeyBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageKeyBuilder.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/13/22. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct StorageKeyBuilder { 12 | // NO-OP 13 | } 14 | 15 | // MARK: - Statement Blocks 16 | 17 | extension StorageKeyBuilder { 18 | // MARK: Public Static Interface 19 | 20 | public static func buildBlock() -> StorageKey { 21 | StorageKey(id: "TODO", defaultValue: nil) 22 | } 23 | 24 | public static func buildBlock(_ k1: K1) -> K1 where K1: StorageKeyProtocol { 25 | k1 26 | } 27 | 28 | public static func buildBlock( 29 | _ k1: K1, 30 | _ k2: K2 31 | ) -> Tuple2 32 | where 33 | K1: StorageKeyProtocol, 34 | K2: StorageKeyProtocol 35 | { 36 | Tuple2(k1, k2) 37 | } 38 | 39 | public static func buildBlock( 40 | _ k1: K1, 41 | _ k2: K2, 42 | _ k3: K3 43 | ) -> Tuple3 44 | where 45 | K1: StorageKeyProtocol, 46 | K2: StorageKeyProtocol, 47 | K3: StorageKeyProtocol 48 | { 49 | Tuple3(k1, k2, k3) 50 | } 51 | } 52 | 53 | // MARK: - Optionals 54 | 55 | extension StorageKeyBuilder { 56 | // MARK: Public Static Interface 57 | 58 | public static func buildIf(_ key: Key?) -> Key? { 59 | key 60 | } 61 | } 62 | 63 | // MARK: - Conditionals 64 | 65 | extension StorageKeyBuilder { 66 | // MARK: Public Static Interface 67 | 68 | public static func buildEither( 69 | first: TrueKey 70 | ) -> Conditional where TrueKey: StorageKeyProtocol, FalseKey: StorageKeyProtocol { 71 | .true(first) 72 | } 73 | 74 | public static func buildEither( 75 | second: FalseKey 76 | ) -> Conditional where TrueKey: StorageKeyProtocol, FalseKey: StorageKeyProtocol { 77 | .false(second) 78 | } 79 | } 80 | 81 | // MARK: - Expressions 82 | 83 | extension StorageKeyBuilder { 84 | // NO-OP, can be implemented by consumer if desired 85 | } 86 | 87 | // MARK: - Final Results 88 | 89 | extension StorageKeyBuilder { 90 | // NO-OP, can be implemented by consumer if desired 91 | } 92 | 93 | // MARK: - Limited Availability 94 | 95 | extension StorageKeyBuilder { 96 | // MARK: Public Static Interface 97 | 98 | public static func buildLimitedAvailability(_ key: Key) -> Key where Key: StorageKeyProtocol { 99 | key 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/StorageKeyObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageKeyObserver.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/13/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This class, and the `Storage` functions it relies upon, implement the two known ways to observe `UserDefaults` and 11 | /// `NSUbiquitousKeyValueStore`. It is likely that other `Storage` implementations can be written under the same 12 | /// interface but that is not what it is optimized for. This lets us use `StoredValue` with any type of `Storage` and 13 | /// simplifies many callsites that previously had to be duplicated between those two known `Storage` implementations. 14 | public class StorageKeyObserver: NSObject, ObservableObject where Key: StorageKeyProtocol { 15 | public let key: Key 16 | public let storage: Storage 17 | 18 | private var context: Int 19 | 20 | // MARK: Public Initialization 21 | 22 | public init(storage: Storage, key: Key) { 23 | self.storage = storage 24 | self.key = key 25 | 26 | context = 0 27 | 28 | super.init() 29 | 30 | storage.register(observer: self, for: key, with: &context) { [weak self] in 31 | self?.objectWillChange() 32 | } 33 | } 34 | 35 | deinit { 36 | storage.deregister(observer: self, for: key, with: &context) 37 | } 38 | 39 | // MARK: Public Instance Interface 40 | 41 | public var value: Key.Value { 42 | get { 43 | storage.get(key) 44 | } 45 | set { 46 | objectWillChange.send() 47 | 48 | storage.set(key, to: newValue) 49 | } 50 | } 51 | 52 | // MARK: NSObject Implementation 53 | 54 | public override func observeValue( 55 | forKeyPath keyPath: String?, 56 | of object: Any?, 57 | change: [NSKeyValueChangeKey : Any]?, 58 | context: UnsafeMutableRawPointer? 59 | ) { 60 | objectWillChange() 61 | } 62 | 63 | // MARK: Private Instance Interface 64 | 65 | private func objectWillChange() { 66 | Task { 67 | await MainActor.run { 68 | objectWillChange.send() 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/StorageKeyProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageKeyProtocol.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/17/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol StorageKeyProtocol: Identifiable where ID == String { 11 | // MARK: Associated Types 12 | 13 | associatedtype Value: Storable 14 | 15 | // MARK: Instance Interface 16 | 17 | var compositeIDs: Set { get } 18 | var defaultValue: Value { get } 19 | var id: String { get } 20 | 21 | func get(from userDefaults: UserDefaults) -> Value 22 | func remove(from userDefaults: UserDefaults) 23 | func set(to newValue: Value, in userDefaults: UserDefaults) 24 | 25 | #if !os(watchOS) 26 | func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value 27 | func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) 28 | func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) 29 | #endif 30 | } 31 | 32 | // MARK: - Default Implementation 33 | 34 | extension StorageKeyProtocol { 35 | // MARK: Public Instance Interface 36 | 37 | public var id: String { 38 | compositeIDs 39 | .joined(separator: ",") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/StoredValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoredValue.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftUI 11 | 12 | @propertyWrapper 13 | public struct StoredValue: DynamicProperty where Key: StorageKeyProtocol { 14 | private let key: Key 15 | 16 | @StateObject private var observer: StorageKeyObserver 17 | 18 | // MARK: Public Initialization 19 | 20 | public init(_ key: Key, storage: Storage) { 21 | self.key = key 22 | 23 | _observer = StateObject(wrappedValue: StorageKeyObserver(storage: storage, key: key)) 24 | } 25 | 26 | // MARK: Property Wrapper Implementation 27 | 28 | public var projectedValue: Binding { 29 | Binding { 30 | wrappedValue 31 | } set: { 32 | wrappedValue = $0 33 | } 34 | } 35 | 36 | public var wrappedValue: Key.Value { 37 | get { 38 | observer.value 39 | } 40 | nonmutating set { 41 | observer.value = newValue 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Storage/UserDefaultsRegistrationBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsRegistrationBuilder.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/29/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct UserDefaultsRegistrationBuilder { 11 | private var registrations: [String: Any] 12 | 13 | // MARK: Public Initialization 14 | 15 | public init() { 16 | registrations = [:] 17 | } 18 | 19 | // MARK: Public Instance Interface 20 | 21 | public func adding(_ key: StorageKey) -> UserDefaultsRegistrationBuilder { 22 | var copy = self 23 | 24 | copy.registrations[key.id] = key.defaultValue.encodeForStorage() 25 | 26 | return copy 27 | } 28 | 29 | public func adding(_ key: StorageKey) -> UserDefaultsRegistrationBuilder { 30 | var copy = self 31 | 32 | if let encodedValue = key.defaultValue.encodeForStorage() { 33 | copy.registrations[key.id] = encodedValue 34 | } 35 | 36 | return copy 37 | } 38 | 39 | public func adding(_ key: DebugStorageKey) -> UserDefaultsRegistrationBuilder { 40 | #if DEBUG 41 | var copy = self 42 | 43 | copy.registrations[key.id] = key.defaultValue.encodeForStorage() 44 | 45 | return copy 46 | #else 47 | self 48 | #endif 49 | } 50 | 51 | public func adding(_ key: DebugStorageKey) -> UserDefaultsRegistrationBuilder { 52 | #if DEBUG 53 | var copy = self 54 | 55 | if let encodedValue = key.defaultValue.encodeForStorage() { 56 | copy.registrations[key.id] = encodedValue 57 | } 58 | 59 | return copy 60 | #else 61 | self 62 | #endif 63 | } 64 | 65 | public func build() -> [String: Any] { 66 | registrations 67 | } 68 | } 69 | 70 | // MARK: - Extension for UserDefaults 71 | 72 | extension UserDefaults { 73 | // MARK: Public Instance Interface 74 | 75 | @inlinable 76 | public func register(builder: (UserDefaultsRegistrationBuilder) -> UserDefaultsRegistrationBuilder) { 77 | register(defaults: builder(.init()).build()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Button Styles/Circle Bordered Button Style/CircleBorderedButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleBorderedButtonStyle.swift 3 | // SuperHeadache 4 | // 5 | // Created by Kyle Hughes on 3/22/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct CircleBorderedButtonStyle { 11 | // NO-OP 12 | } 13 | 14 | // MARK: - ButtonStyle Extension 15 | 16 | extension CircleBorderedButtonStyle: ButtonStyle { 17 | // MARK: Public Instance Interface 18 | 19 | public func makeBody(configuration: Configuration) -> some View { 20 | configuration.label 21 | .foregroundStyle(.tint) 22 | .padding(8) 23 | .background( 24 | Circle() 25 | .fill(.tint) 26 | .opacity(UIConstants.borderedStyleBackgroundOpacity) 27 | ) 28 | .opacity(configuration.isPressed ? UIConstants.borderedStylePressedOpacity : 1.0) 29 | } 30 | } 31 | 32 | // MARK: - Extension for ButtonStyle 33 | 34 | extension ButtonStyle where Self == CircleBorderedButtonStyle { 35 | // MARK: Public Static Interface 36 | 37 | public static var circleBordered: CircleBorderedButtonStyle { 38 | CircleBorderedButtonStyle() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Environment Values/HasHomeIndicatorEnvironmentValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HasHomeIndicatorEnvironmentValue.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/14/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct HasHomeIndicatorKey { 11 | // NO-OP 12 | } 13 | 14 | // MARK: - EnvironmentKey Extension 15 | 16 | #if canImport(UIKit) && !os(watchOS) 17 | 18 | import UIKit 19 | 20 | extension HasHomeIndicatorKey: EnvironmentKey { 21 | // MARK: Getting the Default Value 22 | 23 | public static var defaultValue: Bool { 24 | guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { 25 | return false 26 | } 27 | 28 | let bottomSafeAreaInset = windowScene.windows.first?.safeAreaInsets.bottom ?? 0 29 | 30 | return bottomSafeAreaInset > 0 31 | } 32 | } 33 | 34 | #else 35 | 36 | extension HasHomeIndicatorKey: EnvironmentKey { 37 | // MARK: Getting the Default Value 38 | 39 | public static let defaultValue = false 40 | } 41 | 42 | #endif 43 | 44 | // MARK: - Extension for EnvironmentValues 45 | 46 | extension EnvironmentValues { 47 | // MARK: Public Instance Interface 48 | 49 | public var hasHomeIndicator: Bool { 50 | get { 51 | self[HasHomeIndicatorKey.self] 52 | } 53 | set { 54 | self[HasHomeIndicatorKey.self] = newValue 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/For Each With Index/ForEachWithIndex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForEachWithIndex.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/4/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct ForEachWithIndex 11 | where 12 | Content: View, 13 | Data: RandomAccessCollection, 14 | Data.Index: Hashable 15 | { 16 | private let content: (Data.Index, Data.Element) -> Content 17 | private let data: Data 18 | 19 | // MARK: Public Initialization 20 | 21 | public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) { 22 | self.init(data) { _, element in 23 | content(element) 24 | } 25 | } 26 | 27 | public init(_ data: Data, @ViewBuilder content: @escaping (Data.Index, Data.Element) -> Content) { 28 | self.data = data 29 | self.content = content 30 | } 31 | } 32 | 33 | // MARK: - View Extension 34 | 35 | extension ForEachWithIndex: View { 36 | // MARK: View Bodwy 37 | 38 | public var body: ForEach<[(Data.Index, Data.Element)], Data.Index, Content> { 39 | ForEach(Array(zip(data.indices, data)), id: \.0) { index, element in 40 | content(index, element) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Geometry Preference Reader/AppendValuePreferenceKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppendValuePreferenceKey.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/29/21. 6 | // 7 | // https://finestructure.co/blog/2020/1/20/swiftui-equal-widths-view-constraints 8 | 9 | import SwiftUI 10 | 11 | public struct AppendValuePreferenceKey { 12 | // NO-OP 13 | } 14 | 15 | // MARK: - PreferenceKey Extension 16 | 17 | extension AppendValuePreferenceKey: PreferenceKey { 18 | public typealias Value = [CGFloat] 19 | 20 | // MARK: Public Static Interface 21 | 22 | public static var defaultValue: [CGFloat] { 23 | [] 24 | } 25 | 26 | public static func reduce(value: inout Value, nextValue: () -> Value) { 27 | value.append(contentsOf: nextValue()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Geometry Preference Reader/GeometryPreferenceReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeometryPreferenceReader.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/29/21. 6 | // 7 | // https://finestructure.co/blog/2020/1/20/swiftui-equal-widths-view-constraints 8 | 9 | import SwiftUI 10 | 11 | public struct GeometryPreferenceReader where Key.Value == Value { 12 | public let key: Key.Type 13 | public let value: (GeometryProxy) -> Value 14 | 15 | // MARK: Public Initialization 16 | 17 | public init(value: @escaping (GeometryProxy) -> Value) { 18 | self.value = value 19 | 20 | key = Key.self 21 | } 22 | 23 | public init(key: Key.Type, value: @escaping (GeometryProxy) -> Value) { 24 | self.key = key 25 | self.value = value 26 | } 27 | } 28 | 29 | // MARK: - ViewModifier Extension 30 | 31 | extension GeometryPreferenceReader: ViewModifier { 32 | // MARK: Public Instance Interface 33 | 34 | public func body(content: Content) -> some View { 35 | content 36 | .background( 37 | GeometryReader { 38 | Color.clear.preference(key: self.key, value: self.value($0)) 39 | } 40 | ) 41 | } 42 | } 43 | 44 | // MARK: - Extension for View 45 | 46 | extension View { 47 | // MARK: Public Instance Interface 48 | 49 | public func assignMaxPreference( 50 | for key: K.Type, 51 | to binding: Binding 52 | ) -> some View where K.Value == [CGFloat] { 53 | onPreferenceChange(key.self) { preferences in 54 | let maxPreference = preferences.reduce(0, max) 55 | 56 | // We only set value if > 0 to avoid pinning sizes to zero. 57 | guard 0 < maxPreference else { 58 | return 59 | } 60 | 61 | binding.wrappedValue = maxPreference 62 | } 63 | } 64 | 65 | public func read( 66 | key: Key.Type, 67 | value: @escaping (GeometryProxy) -> Value 68 | ) -> some View where Key: PreferenceKey, Key.Value == Value { 69 | read(GeometryPreferenceReader(key: key, value: value)) 70 | } 71 | 72 | public func read( 73 | _ preference: GeometryPreferenceReader 74 | ) -> some View where Key: PreferenceKey { 75 | modifier(preference) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Geometry Preference Reader/GeometrySizePreferenceKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeometrySizePreferenceKey.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct GeometrySizePreferenceKey { 11 | // Private Initialization 12 | 13 | private init() { 14 | // NO-OP 15 | } 16 | } 17 | 18 | // MARK: - PreferenceKey Extension 19 | 20 | extension GeometrySizePreferenceKey: PreferenceKey { 21 | // MARK: Public Instance Interface 22 | 23 | public static var defaultValue: CGSize { 24 | .zero 25 | } 26 | 27 | // MARK: Combining Preferences 28 | 29 | public static func reduce(value: inout Value, nextValue: () -> Value) { 30 | value = CGSize( 31 | width: value.width + nextValue().width, 32 | height: value.height + nextValue().height 33 | ) 34 | } 35 | } 36 | 37 | // MARK: - Extension for View 38 | 39 | extension View { 40 | // MARK: Public Instance Interface 41 | 42 | public func onSizeChange(perform handler: @escaping (CGSize) -> Void) -> some View { 43 | read(key: GeometrySizePreferenceKey.self) { 44 | $0.frame(in: .local).size 45 | } 46 | .onPreferenceChange(GeometrySizePreferenceKey.self, perform: handler) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Geometry Preference Reader/Preference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preference.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/29/21. 6 | // 7 | // https://finestructure.co/blog/2020/1/20/swiftui-equal-widths-view-constraints 8 | 9 | public protocol Preference { 10 | // NO-OP 11 | } 12 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Grid/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/19/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct Grid { 11 | public static let defaultColor: Color = .black 12 | public static let defaultLineWidth: CGFloat = 1 13 | 14 | private let color: Color 15 | private let lineWidth: CGFloat 16 | private let numberOfColumns: Int 17 | private let numberOfRows: Int 18 | 19 | // MARK: Public Initialization 20 | 21 | public init( 22 | _ numberOfCellsOnEachAxis: Int, 23 | color: Color = defaultColor, 24 | lineWidth: CGFloat = defaultLineWidth 25 | ) { 26 | self.init(numberOfCellsOnEachAxis, numberOfCellsOnEachAxis, color: color, lineWidth: lineWidth) 27 | } 28 | 29 | public init( 30 | _ numberOfColumns: Int, 31 | _ numberOfRows: Int, 32 | color: Color = defaultColor, 33 | lineWidth: CGFloat = defaultLineWidth 34 | ) { 35 | self.numberOfColumns = numberOfColumns 36 | self.numberOfRows = numberOfRows 37 | self.color = color 38 | self.lineWidth = lineWidth 39 | } 40 | } 41 | 42 | // MARK: - View Extension 43 | 44 | extension Grid: View { 45 | // MARK: View Body 46 | 47 | public var body: some View { 48 | GeometryReader { geometry in 49 | Path { path in 50 | let columnWidth = geometry.size.width / CGFloat(numberOfColumns) 51 | let rowHeight = geometry.size.height / CGFloat(numberOfRows) 52 | 53 | for column in 0 ... numberOfColumns { 54 | let offset = CGFloat(column) * columnWidth 55 | path.move(to: CGPoint(x: offset, y: 0)) 56 | path.addLine(to: CGPoint(x: offset, y: geometry.size.height)) 57 | } 58 | 59 | for row in 0 ... numberOfRows { 60 | let offset = CGFloat(row) * rowHeight 61 | path.move(to: CGPoint(x: 0, y: offset)) 62 | path.addLine(to: CGPoint(x: geometry.size.width, y: offset)) 63 | } 64 | } 65 | .stroke(color, lineWidth: lineWidth) 66 | } 67 | } 68 | } 69 | 70 | // MARK: - Extension for View 71 | 72 | extension View { 73 | // MARK: Public Instance Interace 74 | 75 | public func overlayWithGrid( 76 | _ numberOfCellsOnEachAxis: Int, 77 | color: Color = Grid.defaultColor, 78 | lineWidth: CGFloat = Grid.defaultLineWidth 79 | ) -> some View { 80 | overlay(Grid(numberOfCellsOnEachAxis, color: color, lineWidth: lineWidth)) 81 | } 82 | 83 | public func overlayWithGrid( 84 | _ numberOfColumns: Int, 85 | _ numberOfRows: Int, 86 | color: Color = Grid.defaultColor, 87 | lineWidth: CGFloat = Grid.defaultLineWidth 88 | ) -> some View { 89 | overlay(Grid(numberOfColumns, numberOfRows, color: color, lineWidth: lineWidth)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Particle Emitter/ParticleEmitterCellBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticleEmitterCellBuilder.swift 3 | // public 4 | // 5 | // Created by Kyle Hughes on 6/27/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import UIKit 11 | 12 | @resultBuilder 13 | public struct ParticleEmitterCellBuilder { 14 | // MARK: Public Static Interface 15 | 16 | public static func buildBlock(_ cells: CAEmitterCell...) -> [CAEmitterCell] { 17 | Array(cells) 18 | } 19 | } 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Preference Keys/MaxValuePreferenceKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaxValuePreferenceKey.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/14/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct MaxValuePreferenceKey where WrappedValue: Comparable { 11 | // NO-OP 12 | } 13 | 14 | // MARK: - PreferenceKey Extension 15 | 16 | extension MaxValuePreferenceKey: PreferenceKey { 17 | // MARK: Getting the Default Value 18 | 19 | public typealias Value = Optional 20 | 21 | // MARK: Combining Preferences 22 | 23 | public static func reduce(value: inout Value, nextValue: () -> Value) { 24 | switch (value, nextValue()) { 25 | case let (.some(original), .some(next)): 26 | value = max(original, next) 27 | case (.some, .none): 28 | break 29 | case let (.none, .some(next)): 30 | value = next 31 | case (.none, .none): 32 | break 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Selectively-Rounded Rectangle/CornerRadii.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRadii.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 4/12/22. 6 | // 7 | 8 | import Accelerate 9 | import CoreGraphics 10 | import SwiftUI 11 | 12 | public struct CornerRadii: Equatable { 13 | public private(set) var bottomLeft: CGFloat 14 | public private(set) var bottomRight: CGFloat 15 | public private(set) var topLeft: CGFloat 16 | public private(set) var topRight: CGFloat 17 | 18 | // MARK: Internal Intialization 19 | 20 | public init(_ all: CGFloat) { 21 | self.init(topLeft: all, topRight: all, bottomLeft: all, bottomRight: all) 22 | } 23 | 24 | public init( 25 | top: CGFloat, 26 | bottom: CGFloat 27 | ) { 28 | self.init(topLeft: top, topRight: top, bottomLeft: bottom, bottomRight: bottom) 29 | } 30 | 31 | public init( 32 | left: CGFloat, 33 | right: CGFloat 34 | ) { 35 | self.init(topLeft: left, topRight: right, bottomLeft: left, bottomRight: right) 36 | } 37 | 38 | public init( 39 | topLeft: CGFloat, 40 | topRight: CGFloat, 41 | bottomLeft: CGFloat, 42 | bottomRight: CGFloat 43 | ) { 44 | self.topLeft = topLeft 45 | self.topRight = topRight 46 | self.bottomLeft = bottomLeft 47 | self.bottomRight = bottomRight 48 | } 49 | } 50 | 51 | // MARK: - AdditiveArithmetic Extension 52 | 53 | extension CornerRadii { 54 | // MARK: Convenience 55 | 56 | public static var zero: Self { 57 | Self(0) 58 | } 59 | 60 | // MARK: Operator Functions 61 | 62 | public static func + (lhs: Self, rhs: Self) -> Self { 63 | CornerRadii( 64 | topLeft: lhs.topLeft + rhs.topLeft, 65 | topRight: lhs.topRight + rhs.topRight, 66 | bottomLeft: lhs.bottomLeft + rhs.bottomLeft, 67 | bottomRight: lhs.bottomRight + rhs.bottomRight 68 | ) 69 | } 70 | 71 | public static func += (lhs: inout Self, rhs: Self) { 72 | lhs.bottomLeft += rhs.bottomLeft 73 | lhs.bottomRight += rhs.bottomRight 74 | lhs.topLeft += rhs.topLeft 75 | lhs.topRight += rhs.topRight 76 | } 77 | 78 | public static func - (lhs: Self, rhs: Self) -> Self { 79 | CornerRadii( 80 | topLeft: lhs.topLeft - rhs.topLeft, 81 | topRight: lhs.topRight - rhs.topRight, 82 | bottomLeft: lhs.bottomLeft - rhs.bottomLeft, 83 | bottomRight: lhs.bottomRight - rhs.bottomRight 84 | ) 85 | } 86 | 87 | public static func -= (lhs: inout Self, rhs: Self) { 88 | lhs.bottomLeft -= rhs.bottomLeft 89 | lhs.bottomRight -= rhs.bottomRight 90 | lhs.topLeft -= rhs.topLeft 91 | lhs.topRight -= rhs.topRight 92 | } 93 | } 94 | 95 | // MARK: - VectorArithmetic Extension 96 | 97 | extension CornerRadii: VectorArithmetic { 98 | // MARK: Manipulating Values 99 | 100 | public mutating func scale(by rhs: Double) { 101 | bottomLeft.scale(by: rhs) 102 | bottomRight.scale(by: rhs) 103 | topLeft.scale(by: rhs) 104 | topRight.scale(by: rhs) 105 | } 106 | 107 | public var magnitudeSquared: Double { 108 | bottomLeft * bottomLeft + 109 | bottomRight * bottomRight + 110 | topLeft * topLeft + 111 | topRight * topRight 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Selectively-Rounded Rectangle/SelectivelyRoundedRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedRectangle+Super Headache.swift 3 | // Super Headache 4 | // 5 | // Created by Kyle Hughes on 3/27/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct SelectivelyRoundedRectangle { 11 | public var animatableData: CornerRadii 12 | 13 | // MARK: Internal Intialization 14 | 15 | public init(radii: CornerRadii) { 16 | animatableData = radii 17 | } 18 | } 19 | 20 | // MARK: - Shape Extension 21 | 22 | extension SelectivelyRoundedRectangle: Shape { 23 | // MARK: Internal Instance Interface 24 | 25 | public func path(in rect: CGRect) -> Path { 26 | .continuouslyRoundedRectangle(in: rect, with: animatableData) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/Time Interval Picker/TimeIntervalPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalPicker.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/18/22. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import SwiftUI 11 | 12 | public struct TimeIntervalPicker { 13 | @Binding public var duration: TimeInterval 14 | 15 | // MARK: Public Initialization 16 | 17 | public init(duration: Binding) { 18 | _duration = duration 19 | } 20 | } 21 | 22 | // MARK: - UIViewRepresentable Extension 23 | 24 | extension TimeIntervalPicker: UIViewRepresentable { 25 | // MARK: Public Typealiases 26 | 27 | public typealias UIViewType = UIDatePicker 28 | 29 | // MARK: Creating and Updating the View 30 | 31 | public func makeCoordinator() -> TimeIntervalPicker.Coordinator { 32 | Coordinator(duration: $duration) 33 | } 34 | 35 | public func makeUIView(context: Context) -> UIDatePicker { 36 | let timeDurationPicker = UIDatePicker() 37 | timeDurationPicker.datePickerMode = .countDownTimer 38 | timeDurationPicker.addTarget(context.coordinator, action: #selector(Coordinator.changed(_:)), for: .valueChanged) 39 | return timeDurationPicker 40 | } 41 | 42 | public func updateUIView(_ uiView: UIDatePicker, context: Context) { 43 | uiView.countDownDuration = duration 44 | } 45 | } 46 | 47 | #if DEBUG 48 | // MARK: - Previews 49 | 50 | struct TimeIntervalPicker_Previews: PreviewProvider { 51 | // MARK: Public Static Interface 52 | 53 | public static var previews: some View { 54 | TimeIntervalPicker(duration: .constant(.hours(2))) 55 | } 56 | } 57 | #endif 58 | 59 | // MARK: TimeIntervalPicker.Coordinator Extension 60 | 61 | extension TimeIntervalPicker { 62 | public class Coordinator: NSObject { 63 | private var duration: Binding 64 | 65 | // MARK: Public Initialization 66 | 67 | public init(duration: Binding) { 68 | self.duration = duration 69 | } 70 | 71 | // MARK: Public Instance Interface 72 | 73 | @objc public func changed(_ sender: UIDatePicker) { 74 | self.duration.wrappedValue = sender.countDownDuration 75 | } 76 | } 77 | } 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/View Modifiers/Device Did Shake/DeviceDidShakeViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceDidShakeViewModifier.swift 3 | // SuperHeadache 4 | // 5 | // Created by Kyle Hughes on 8/21/21. 6 | // 7 | 8 | #if canImport(UIKit) && !os(watchOS) 9 | 10 | import SwiftUI 11 | import UIKit 12 | 13 | public struct DeviceShakeViewModifier: ViewModifier { 14 | internal let action: () async -> Void 15 | 16 | // MARK: Public Instance Interface 17 | 18 | public func body(content: Content) -> some View { 19 | content 20 | .onAppear() 21 | .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in 22 | Task { 23 | await action() 24 | } 25 | } 26 | } 27 | } 28 | 29 | // MARK: - Extension for View 30 | 31 | extension View { 32 | // MARK: Public Instance Interface 33 | 34 | public func onShake(perform action: @escaping () async -> Void) -> some View { 35 | modifier(DeviceShakeViewModifier(action: action)) 36 | } 37 | } 38 | 39 | // MARK: - Extension for UIDevice 40 | 41 | extension UIDevice { 42 | public static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification") 43 | } 44 | 45 | // MARK: - Extension for UIWindow 46 | 47 | extension UIWindow { 48 | // MARK: Internal Instance Interface 49 | 50 | open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { 51 | guard motion == .motionShake else { 52 | return 53 | } 54 | 55 | NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil) 56 | } 57 | } 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/SwiftUI/View Modifiers/Measure and Set To Max Height/MeasureAndSetToMaxHeightViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeasureAndSetToMaxHeightViewModifier.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/14/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct MeasureAndSetToMaxHeightViewModifier { 11 | @Binding private var value: CGFloat? 12 | 13 | // MARK: Public Initialization 14 | 15 | public init(value: Binding) { 16 | _value = value 17 | } 18 | } 19 | 20 | // MARK: - ViewModifier Extension 21 | 22 | extension MeasureAndSetToMaxHeightViewModifier: ViewModifier { 23 | // MARK: Getting the Body 24 | 25 | public func body(content: Content) -> some View { 26 | content 27 | .read(key: MaxValuePreferenceKey.self) { 28 | $0.size.height 29 | } 30 | .onPreferenceChange(MaxValuePreferenceKey.self) { 31 | value = $0 32 | } 33 | .frame(height: value) 34 | } 35 | } 36 | 37 | // MARK: - Extension for View 38 | 39 | extension View { 40 | // MARK: Public Instance Interface 41 | 42 | @inlinable 43 | public func measureAndSetToMaxHeight(storedIn storage: Binding) -> some View { 44 | modifier(MeasureAndSetToMaxHeightViewModifier(value: storage)) 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Synthesized Identifiable/SynthesizedIdentifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynthesizedIdentifiable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 7/24/21. 6 | // 7 | 8 | public protocol SynthesizedIdentifiable: Hashable, Identifiable { 9 | // NO-OP 10 | } 11 | 12 | // MARK: - Implementation 13 | 14 | extension SynthesizedIdentifiable { 15 | // MARK: Public Instance Interface 16 | 17 | public var id: Self { 18 | self 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Task Executor/TaskExecutor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskExecutor.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 9/26/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Actor that can constrain the number of concurrent `Task`s being executed, like `OperationQueue` with 11 | /// `maxConcurrentOperationCount`. 12 | public actor TaskExecutor { 13 | private let maxConcurrentTaskCount: Int 14 | 15 | private var numberOfRunningTasks: Int 16 | private var pendingContinuations: [UnsafeContinuation] 17 | 18 | // MARK: Public Initialization 19 | 20 | public init(for urlSession: URLSession) { 21 | self.init(maxConcurrentTaskCount: urlSession.configuration.httpMaximumConnectionsPerHost) 22 | } 23 | 24 | public init(maxConcurrentTaskCount: Int) { 25 | self.maxConcurrentTaskCount = maxConcurrentTaskCount 26 | 27 | numberOfRunningTasks = 0 28 | pendingContinuations = [] 29 | } 30 | 31 | // MARK: Public Instance Interface 32 | 33 | public nonisolated func submit( 34 | priority: TaskPriority? = nil, 35 | _ task: @escaping () async -> Success 36 | ) -> Task { 37 | Task(priority: priority) { 38 | await submit(task) 39 | } 40 | } 41 | 42 | public nonisolated func submit( 43 | priority: TaskPriority? = nil, 44 | _ task: @escaping () async throws -> Success 45 | ) -> Task { 46 | Task(priority: priority) { 47 | try await submit(task) 48 | } 49 | } 50 | 51 | public func submit(_ task: @escaping () async throws -> Success) async rethrows -> Success { 52 | if maxConcurrentTaskCount <= numberOfRunningTasks { 53 | await withUnsafeContinuation { 54 | pendingContinuations.append($0) 55 | } 56 | } 57 | 58 | numberOfRunningTasks += 1 59 | 60 | defer { 61 | numberOfRunningTasks -= 1 62 | 63 | if not(pendingContinuations.isEmpty) { 64 | pendingContinuations.removeFirst().resume() 65 | } 66 | } 67 | 68 | return try await task() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Time Interval Selection/TimeIntervalSelection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalSelection.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 4/16/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum TimeIntervalSelection: Codable, Equatable, Hashable, Storable { 11 | case builtIn(TimeInterval) 12 | case custom(TimeInterval) 13 | 14 | // MARK: Public Instance Interface 15 | 16 | public var isBuiltIn: Bool { 17 | switch self { 18 | case .builtIn: 19 | return true 20 | case .custom: 21 | return false 22 | } 23 | } 24 | 25 | public var isCustom: Bool { 26 | switch self { 27 | case .builtIn: 28 | return false 29 | case .custom: 30 | return true 31 | } 32 | } 33 | 34 | public var value: TimeInterval { 35 | switch self { 36 | case let .builtIn(value): 37 | return value 38 | case let .custom(value): 39 | return value 40 | } 41 | } 42 | } 43 | 44 | // MARK: - Versionable Extension 45 | 46 | extension TimeIntervalSelection: Versionable { 47 | // MARK: Public Typealiases 48 | 49 | public typealias Previous = Self 50 | public typealias Wrapper = TimeIntervalSelectionVersionableWrapper 51 | 52 | // MARK: Public Static Interface 53 | 54 | public static var version: TimeIntervalSelectionVersion { 55 | .v1 56 | } 57 | 58 | public static func extract(from wrapper: TimeIntervalSelectionVersionableWrapper) -> Self? { 59 | switch wrapper { 60 | case let .v1(value): 61 | return value 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Time Interval Selection/TimeIntervalSelectionVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalSelectionVersion.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | public enum TimeIntervalSelectionVersion: UInt, Version { 9 | case v1 = 1 10 | } 11 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Time Interval Selection/TimeIntervalSelectionVersionableWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalSelectionVersionableWrapper.swift 3 | // Common 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | public enum TimeIntervalSelectionVersionableWrapper { 9 | case v1(TimeIntervalSelection) 10 | } 11 | 12 | // MARK: - VersionableWrapper Extension 13 | 14 | extension TimeIntervalSelectionVersionableWrapper: CodableVersionableWrapper { 15 | // MARK: Public Typealiases 16 | 17 | public typealias Version = TimeIntervalSelectionVersion 18 | 19 | // MARK: Public Static Interface 20 | 21 | public static func deserialize( 22 | version: Version, 23 | from container: KeyedDecodingContainer, 24 | at key: Key 25 | ) throws -> Self where Key: CodingKey { 26 | switch version { 27 | case .v1: 28 | return try .v1(container.decode(TimeIntervalSelection.self, forKey: key)) 29 | } 30 | } 31 | 32 | // MARK: Public Instance Interface 33 | 34 | public var version: TimeIntervalSelectionVersion { 35 | switch self { 36 | case .v1: 37 | return .v1 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Tuple Values/Tuple2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tuple2.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/13/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Tuple2 { 11 | public let e1: E1 12 | public let e2: E2 13 | 14 | // MARK: Public Initialization 15 | 16 | public init( 17 | _ e1: E1, 18 | _ e2: E2 19 | ) { 20 | self.e1 = e1 21 | self.e2 = e2 22 | } 23 | } 24 | 25 | // MARK: - Equatable Extension 26 | 27 | extension Tuple2: Equatable 28 | where 29 | E1: Equatable, 30 | E2: Equatable 31 | { 32 | // NO-OP 33 | } 34 | 35 | // MARK: - Hashable Extension 36 | 37 | extension Tuple2: Hashable 38 | where 39 | E1: Hashable, 40 | E2: Hashable 41 | { 42 | // NO-OP 43 | } 44 | 45 | // MARK: - Storable Extension 46 | 47 | extension Tuple2: Storable 48 | where 49 | E1: Storable, 50 | E2: Storable 51 | { 52 | // MARK: Public Typealiases 53 | 54 | public typealias StorableValue = ( 55 | E1.StorableValue, 56 | E2.StorableValue 57 | ) 58 | 59 | // MARK: Public Static Interface 60 | 61 | public static func decode(from storage: @autoclosure () -> StorableValue?) -> Self? { 62 | guard 63 | let ( 64 | storedE1, 65 | storedE2 66 | ) = storage(), 67 | let e1 = E1.decode(from: storedE1), 68 | let e2 = E2.decode(from: storedE2) 69 | else { 70 | return nil 71 | } 72 | 73 | return Self( 74 | e1, 75 | e2 76 | ) 77 | } 78 | 79 | // MARK: Public Instance Interface 80 | 81 | public func encodeForStorage() -> StorableValue { 82 | ( 83 | e1.encodeForStorage(), 84 | e2.encodeForStorage() 85 | ) 86 | } 87 | } 88 | 89 | // MARK: - StorageKeyProtocol Extension 90 | 91 | extension Tuple2: Identifiable, StorageKeyProtocol 92 | where 93 | E1: StorageKeyProtocol, 94 | E2: StorageKeyProtocol 95 | { 96 | // MARK: Public Typealiases 97 | 98 | public typealias Value = Tuple2< 99 | E1.Value, 100 | E2.Value 101 | > 102 | 103 | // MARK: Public Instance Interface 104 | 105 | public var compositeIDs: Set { 106 | [ 107 | e1.id, 108 | e2.id, 109 | ] 110 | } 111 | 112 | public var defaultValue: Value { 113 | Value( 114 | e1.defaultValue, 115 | e2.defaultValue 116 | ) 117 | } 118 | 119 | public func get(from userDefaults: UserDefaults) -> Value { 120 | Value( 121 | e1.get(from: userDefaults), 122 | e2.get(from: userDefaults) 123 | ) 124 | } 125 | 126 | public func remove(from userDefaults: UserDefaults) { 127 | e1.remove(from: userDefaults) 128 | e2.remove(from: userDefaults) 129 | } 130 | 131 | public func set(to newValue: Value, in userDefaults: UserDefaults) { 132 | e1.set(to: newValue.e1, in: userDefaults) 133 | e2.set(to: newValue.e2, in: userDefaults) 134 | } 135 | 136 | #if !os(watchOS) 137 | 138 | public func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value { 139 | Value( 140 | e1.get(from: ubiquitousStore), 141 | e2.get(from: ubiquitousStore) 142 | ) 143 | } 144 | 145 | public func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) { 146 | e1.remove(from: ubiquitousStore) 147 | e2.remove(from: ubiquitousStore) 148 | } 149 | 150 | public func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 151 | e1.set(to: newValue.e1, in: ubiquitousStore) 152 | e2.set(to: newValue.e2, in: ubiquitousStore) 153 | } 154 | 155 | #endif 156 | } 157 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/UIConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIConstants.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 6/20/22. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | public enum UIConstants { 11 | public static let borderedStyleBackgroundOpacity: CGFloat = 0.175 12 | public static let borderedStylePressedOpacity: CGFloat = 0.75 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/User Activity/EmptyUserActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyUserActivity.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 3/23/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EmptyUserActivity { 11 | public static let identifier = ReverseDomainName.es_kylehugh_codeMonkey_apple 12 | .adding(subdomain: "user-activity") 13 | .adding(subdomain: "empty") 14 | 15 | public let id: ReverseDomainName 16 | public let title: String 17 | 18 | // MARK: Public Initialization 19 | 20 | public init() { 21 | id = Self.identifier 22 | title = String() 23 | } 24 | } 25 | 26 | // MARK: - UserActivity Extension 27 | 28 | extension EmptyUserActivity: UserActivity { 29 | // MARK: Public Initialization 30 | 31 | public init?(from platformActivity: NSUserActivity) { 32 | return nil 33 | } 34 | 35 | // MARK: Public Instance Interface 36 | 37 | public func makePlatformActivity() -> NSUserActivity { 38 | fatalError("Should not be called, this type symbolizes no activity") 39 | } 40 | 41 | public func update(platformActivity: NSUserActivity) { 42 | // NO-OP 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/User Activity/Extensions/View+UserActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+UserActivity.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/22/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | // MARK: Public Instance Interface 12 | 13 | public func userActivity( 14 | _ userActivity: EmptyUserActivity, 15 | isActive: Bool = true 16 | ) -> some View { 17 | self.userActivity(userActivity.id.rawValue, isActive: false, userActivity.update) 18 | } 19 | 20 | public func userActivity( 21 | _ userActivity: UserActivity, 22 | isActive: Bool = true 23 | ) -> some View where UserActivity: CodeMonkeyApple.UserActivity { 24 | self.userActivity(userActivity.id.rawValue, isActive: isActive, userActivity.update) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/User Activity/UserActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserActivity.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 8/22/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol UserActivity: Identifiable { 11 | // MARK: Initialization 12 | 13 | init?(from platformActivity: NSUserActivity) 14 | 15 | // MARK: Instance Inteface 16 | 17 | var id: ReverseDomainName { get } 18 | var title: String { get } 19 | 20 | func makePlatformActivity() -> NSUserActivity 21 | func update(platformActivity: NSUserActivity) 22 | } 23 | 24 | // MARK: - Implementation 25 | 26 | extension UserActivity { 27 | // MARK: Public Instance Interface 28 | 29 | public func becomeCurrent() { 30 | makePlatformActivity().becomeCurrent() 31 | } 32 | 33 | public func resignCurrent() { 34 | makePlatformActivity().becomeCurrent() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/User Notifications/UserNotificationUserInfoKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNotificationUserInfoKeys.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/12/22. 6 | // 7 | 8 | extension AnyHashable { 9 | public static let dateRequestedUserNotificationUserInfoKey = "dateRequested_codeMonkeyApple" 10 | } 11 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/CodableVersionableEnvelope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableVersionableEnvelope.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/2/21. 6 | // 7 | 8 | /// A type that automatically encodes and decodes a `Versionable` body. It is the one-stop-shop for persisting `Codable` 9 | /// models to disk. 10 | /// 11 | /// Migration to the model version happens automatically if the type being decoded is of an earlier version. 12 | /// 13 | /// The recommended practice is to keep all of the model versions that have ever shipped with the software. Always 14 | /// encode and decode the latest model version. 15 | public struct CodableVersionableEnvelope: 16 | Encodable 17 | where 18 | Model: Codable & Versionable, 19 | Model.Wrapper: CodableVersionableWrapper 20 | { 21 | public let model: Model 22 | public let version: Model.Wrapper.Version 23 | 24 | // MARK: Public Initialization 25 | 26 | public init(_ body: Model) { 27 | self.model = body 28 | 29 | version = Model.version 30 | } 31 | } 32 | 33 | // MARK: - Decodable Extension 34 | 35 | extension CodableVersionableEnvelope: Decodable { 36 | // MARK: Public Initialization 37 | 38 | public init(from decoder: Decoder) throws { 39 | let container = try decoder.container(keyedBy: CodingKeys.self) 40 | let decodedVersionRawValue = try container.decode(Model.Wrapper.Version.RawValue.self, forKey: .version) 41 | 42 | guard decodedVersionRawValue <= Model.version.rawValue else { 43 | throw VersionableEnvelopeDecodingError.sourceHasHigherVersionThanDestination( 44 | source: decodedVersionRawValue, 45 | destination: Model.version.rawValue 46 | ) 47 | } 48 | 49 | guard let decodedVersion = Model.Wrapper.Version(rawValue: decodedVersionRawValue) else { 50 | throw VersionableEnvelopeDecodingError.versionUnknown(decodedVersionRawValue) 51 | } 52 | 53 | model = try Model.migrate( 54 | from: Model.Wrapper.deserialize(version: decodedVersion, from: container, at: .model) 55 | ) 56 | version = Model.version 57 | } 58 | } 59 | 60 | // MARK: - CodingKeys Definition 61 | 62 | extension CodableVersionableEnvelope { 63 | public enum CodingKeys: String, CodingKey { 64 | case model = "model" 65 | case version = "version" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/CodableVersionableWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableVersionWrapper.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/7/21. 6 | // 7 | 8 | public protocol CodableVersionableWrapper: VersionableWrapper { 9 | // MARK: Static Interface 10 | 11 | static func deserialize( 12 | version: Version, 13 | from container: KeyedDecodingContainer, 14 | at key: Key 15 | ) throws -> Self 16 | } 17 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/Errors/VersionMigrationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionMigrationError.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/7/21. 6 | // 7 | 8 | public enum VersionMigrationError: 9 | Error, 10 | Equatable 11 | where 12 | Wrapper: CodeMonkeyApple.VersionableWrapper 13 | { 14 | case ineligibleForMigration(source: Wrapper.Version, destination: Wrapper.Version) 15 | case sourceHasHigherVersionThanDestination(source: Wrapper.Version, destination: Wrapper.Version) 16 | 17 | // MARK: Public Static Interface 18 | 19 | public static func ineligibleForMigration( 20 | source: Wrapper, 21 | destination: Wrapper.Version 22 | ) -> Self { 23 | .ineligibleForMigration(source: source.version, destination: destination) 24 | } 25 | 26 | public static func ineligibleForMigration( 27 | source: Wrapper.Version, 28 | destination: Wrapper 29 | ) -> Self { 30 | .ineligibleForMigration(source: source, destination: destination.version) 31 | } 32 | 33 | public static func ineligibleForMigration( 34 | source: Wrapper, 35 | destination: Wrapper 36 | ) -> Self { 37 | .ineligibleForMigration(source: source.version, destination: destination.version) 38 | } 39 | 40 | public static func sourceHasHigherVersionThanDestination( 41 | source: Wrapper, 42 | destination: Wrapper.Version 43 | ) -> Self { 44 | .sourceHasHigherVersionThanDestination(source: source.version, destination: destination) 45 | } 46 | 47 | public static func sourceHasHigherVersionThanDestination( 48 | source: Wrapper.Version, 49 | destination: Wrapper 50 | ) -> Self { 51 | .sourceHasHigherVersionThanDestination(source: source, destination: destination.version) 52 | } 53 | 54 | public static func sourceHasHigherVersionThanDestination( 55 | source: Wrapper, 56 | destination: Wrapper 57 | ) -> Self { 58 | .sourceHasHigherVersionThanDestination(source: source.version, destination: destination.version) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/Errors/VersionableEnvelopeDecodingError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionableEnvelopeDecodingError.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/7/21. 6 | // 7 | 8 | public enum VersionableEnvelopeDecodingError: Error, Equatable { 9 | case sourceHasHigherVersionThanDestination(source: UInt, destination: UInt) 10 | case versionUnknown(UInt) 11 | 12 | // MARK: Public Static Interface 13 | 14 | public static func sourceHasHigherVersionThanDestination( 15 | source: Version, 16 | destination: Version 17 | ) -> Self where Version: CodeMonkeyApple.Version { 18 | .sourceHasHigherVersionThanDestination(source: source.rawValue, destination: destination.rawValue) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/31/21. 6 | // 7 | 8 | public protocol Version: 9 | CaseIterable, 10 | Comparable, 11 | Codable, 12 | RawRepresentable, 13 | SynthesizedIdentifiable 14 | where 15 | RawValue == UInt 16 | { 17 | // NO-OP 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/Versionable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Versionable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 10/31/21. 6 | // 7 | 8 | public protocol Versionable { 9 | // MARK: Associated Types 10 | 11 | associatedtype Previous: Versionable where Previous.Wrapper == Wrapper 12 | associatedtype Wrapper: VersionableWrapper 13 | 14 | // MARK: Initialization 15 | 16 | init(from previous: Previous) throws 17 | 18 | // MARK: Static Interface 19 | 20 | /// The version of the model. 21 | static var version: Wrapper.Version { get } 22 | 23 | static func extract(from wrapper: Wrapper) -> Self? 24 | } 25 | 26 | // MARK: - Bespoke Implementation 27 | 28 | extension Versionable { 29 | // MARK: Public Static Interface 30 | 31 | public static func migrate(from wrapper: Wrapper) throws -> Self { 32 | guard wrapper.version <= version else { 33 | throw VersionMigrationError.sourceHasHigherVersionThanDestination(source: wrapper, destination: version) 34 | } 35 | 36 | guard let model = extract(from: wrapper) else { 37 | return try Self(from: Previous.migrate(from: wrapper)) 38 | } 39 | 40 | return model 41 | } 42 | 43 | // MARK: Public Instance Interface 44 | 45 | public var modelVersion: Wrapper.Version { 46 | Self.version 47 | } 48 | } 49 | 50 | // MARK: - Convenience Implementation for Earliest Model 51 | 52 | extension Versionable where Previous == Self { 53 | // MARK: Public Initialization 54 | 55 | public init(from previous: Previous) { 56 | self = previous 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/CodeMonkeyApple/Versionable/VersionableWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionableWrapper.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 11/2/21. 6 | // 7 | 8 | public protocol VersionableWrapper { 9 | // MARK: Associated Types 10 | 11 | associatedtype Version: CodeMonkeyApple.Version 12 | 13 | // MARK: Instance Interface 14 | 15 | var version: Version { get } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Domain Name/DomainNameTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainNameTests.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 3/15/21. 6 | // 7 | 8 | import CodeMonkeyApple 9 | import XCTest 10 | 11 | final class DomainNameTests: XCTestCase { 12 | // MARK: - XCTestCase Implementation 13 | 14 | override func setUp() { 15 | // NO-OP 16 | } 17 | 18 | override func tearDown() { 19 | // NO-OP 20 | } 21 | } 22 | 23 | // MARK: - Tests 24 | 25 | extension DomainNameTests { 26 | // MARK: Initialization Tests 27 | 28 | func test_init_rawValue_empty() { 29 | // Given… 30 | 31 | let rawValue = "" 32 | let subdomains: [Subdomain] = [ 33 | ] 34 | 35 | // When… 36 | 37 | let domainName = DomainName(rawValue: rawValue) 38 | 39 | // Then… 40 | 41 | XCTAssertEqual(domainName.rawValue, rawValue) 42 | XCTAssertEqual(domainName.subdomains, subdomains) 43 | } 44 | 45 | func test_init_rawValue_multiple() { 46 | // Given… 47 | 48 | let rawValue = "es.kylehugh.subdomain" 49 | let subdomains: [Subdomain] = [ 50 | "es", 51 | "kylehugh", 52 | "subdomain" 53 | ] 54 | 55 | // When… 56 | 57 | let domainName = DomainName(rawValue: rawValue) 58 | 59 | // Then… 60 | 61 | XCTAssertEqual(domainName.rawValue, rawValue) 62 | XCTAssertEqual(domainName.subdomains, subdomains) 63 | } 64 | 65 | func test_init_rawValue_single() { 66 | // Given… 67 | 68 | let rawValue = "es" 69 | let subdomains: [Subdomain] = [ 70 | Subdomain(rawValue: rawValue) 71 | ] 72 | 73 | // When… 74 | 75 | let domainName = DomainName(rawValue: rawValue) 76 | 77 | // Then… 78 | 79 | XCTAssertEqual(domainName.rawValue, rawValue) 80 | XCTAssertEqual(domainName.subdomains, subdomains) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Hash/DerivedHashTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentHashTests.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | import XCTest 10 | 11 | final class DerivedHashTests: XCTestCase { 12 | // MARK: XCTestCase Implementation 13 | 14 | override func setUp() { 15 | // NO-OP 16 | } 17 | 18 | override func tearDown() { 19 | // NO-OP 20 | } 21 | } 22 | 23 | // MARK: - Hash Tests 24 | 25 | extension DerivedHashTests { 26 | // MARK: Tests 27 | 28 | func test_initialization_equivalent() { 29 | XCTAssertEqual(DerivedHash("Label").value, DerivedHash("Label").value) 30 | } 31 | 32 | func test_initialization_unique() { 33 | XCTAssertNotEqual(DerivedHash("First").value, DerivedHash("Second").value) 34 | } 35 | 36 | func test_dependencyDidUpdate_whenDependencyDidNotUpdate() { 37 | expectDependentHashesAreEqual { dependency, first, second in 38 | first.dependencyDidUpdate() 39 | } 40 | } 41 | 42 | func test_dependencyDidUpdate_whenDependencyNotUpdate() { 43 | expectDependentHashesAreDifferent { dependency, first, second in 44 | dependency._value += 1 45 | first.dependencyDidUpdate() 46 | } 47 | } 48 | 49 | func test_update_whenDependencyDidNotUpdate() { 50 | expectDependentHashesAreEqual { dependency, first, second in 51 | first.update() 52 | } 53 | } 54 | 55 | func test_update_whenDependencyDidUpdate() { 56 | expectDependentHashesAreDifferent { dependency, first, second in 57 | dependency._value += 1 58 | first.update() 59 | } 60 | } 61 | 62 | // MARK: Private Instance Interface 63 | 64 | private func expectDependentHashesAreEqual(after operation: (MockHash, DerivedHash, DerivedHash) -> Void) { 65 | let dependency = MockHash("Dependency", value: 1) 66 | 67 | let first = DerivedHash("First") 68 | .addingDependency(on: dependency) 69 | 70 | let second = DerivedHash("Second") 71 | .addingDependency(on: first) 72 | 73 | let firstValueBeforeUpdate = first.value 74 | let secondValueBeforeUpdate = second.value 75 | 76 | operation(dependency, first, second) 77 | 78 | XCTAssertEqual(firstValueBeforeUpdate, first.value) 79 | XCTAssertEqual(secondValueBeforeUpdate, second.value) 80 | } 81 | 82 | private func expectDependentHashesAreDifferent(after operation: (MockHash, DerivedHash, DerivedHash) -> Void) { 83 | let dependency = MockHash("Dependency", value: 1) 84 | 85 | let first = DerivedHash("First") 86 | .addingDependency(on: dependency) 87 | 88 | let second = DerivedHash("Second") 89 | .addingDependency(on: first) 90 | 91 | let firstValueBeforeUpdate = first.value 92 | let secondValueBeforeUpdate = second.value 93 | 94 | operation(dependency, first, second) 95 | 96 | XCTAssertNotEqual(firstValueBeforeUpdate, first.value) 97 | XCTAssertNotEqual(secondValueBeforeUpdate, second.value) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Hash/MockHash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockHash.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 11/30/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | 10 | final class MockHash: Hash { 11 | var _value: Int 12 | 13 | // MARK: Public Initialization 14 | 15 | public init(_ label: String, value: Int) { 16 | _value = value 17 | 18 | super.init(label: label, labelHashValue: label.hashValue) 19 | } 20 | 21 | // MARK: Hash Implementation 22 | 23 | public override var value: Int { 24 | _value 25 | } 26 | 27 | public override func dependencyDidUpdate() { 28 | // NO-OP 29 | } 30 | 31 | public override func update() { 32 | // NO-OP 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/ModelV1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelV1.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | import XCTest 10 | 11 | public struct ModelV1: Codable, Equatable, Storable { 12 | public let id: UUID 13 | public let name: String 14 | 15 | public static var random: Self { 16 | ModelV1(id: UUID(), name: UUID().uuidString) 17 | } 18 | } 19 | 20 | // MARK: - Versionable Extension 21 | 22 | extension ModelV1: Versionable { 23 | // TODO: does this make sense? Yikes! 24 | public typealias Previous = Self 25 | public typealias Wrapper = ModelWrapper 26 | 27 | // MARK: Internal Initialization 28 | 29 | public init(from previous: Previous) { 30 | self = previous 31 | } 32 | 33 | // MARK: Internal Static Interface 34 | 35 | public static var version: ModelVersion { 36 | .v1 37 | } 38 | 39 | public static func extract(from wrapper: Wrapper) -> Self? { 40 | switch wrapper { 41 | case let .v1(model): 42 | return model 43 | default: 44 | return nil 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/ModelV2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelV2.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | import Foundation 10 | 11 | public struct ModelV2: Codable, Equatable, Storable { 12 | public let identifier: String 13 | public let name: String 14 | public let email: String? 15 | 16 | public static var random: Self { 17 | ModelV2(identifier: UUID().uuidString, name: UUID().uuidString, email: UUID().uuidString) 18 | } 19 | } 20 | 21 | // MARK: - Versionable Extension 22 | 23 | extension ModelV2: Versionable { 24 | public typealias Previous = ModelV1 25 | public typealias Wrapper = ModelWrapper 26 | 27 | // MARK: Public Initialization 28 | 29 | public init(from previous: Previous) throws { 30 | email = nil 31 | identifier = previous.id.uuidString 32 | name = previous.name 33 | } 34 | 35 | // MARK: Public Static Interface 36 | 37 | public static var version: ModelVersion { 38 | .v2 39 | } 40 | 41 | public static func extract(from wrapper: Wrapper) -> Self? { 42 | switch wrapper { 43 | case let .v2(model): 44 | return model 45 | default: 46 | return nil 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/ModelV3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelV3.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | import Foundation 10 | 11 | public struct ModelV3: Codable, Equatable, Storable { 12 | public let id: String 13 | public let fullName: String 14 | public let emailAddress: String? 15 | 16 | public static var random: Self { 17 | ModelV3(id: UUID().uuidString, fullName: UUID().uuidString, emailAddress: UUID().uuidString) 18 | } 19 | } 20 | 21 | // MARK: - Versionable Extension 22 | 23 | extension ModelV3: Versionable { 24 | public typealias Previous = ModelV2 25 | public typealias Wrapper = ModelWrapper 26 | 27 | // MARK: Public Initialization 28 | 29 | public init(from previous: Previous) throws { 30 | id = previous.identifier 31 | fullName = previous.name 32 | emailAddress = previous.email 33 | } 34 | 35 | // MARK: Public Static Interface 36 | 37 | public static var version: ModelVersion { 38 | .v3 39 | } 40 | 41 | public static func extract(from wrapper: Wrapper) -> Self? { 42 | switch wrapper { 43 | case let .v3(model): 44 | return model 45 | default: 46 | return nil 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/ModelVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelVersion.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | 10 | public enum ModelVersion: UInt, Version { 11 | case v1 = 1 12 | case v2 = 2 13 | case v3 = 3 14 | } 15 | 16 | // MARK: - Comparable Extension 17 | 18 | extension Comparable where Self: RawRepresentable, RawValue == Int { 19 | // MARK: Public Static Interface 20 | 21 | public static func < (lhs: Self, rhs: Self) -> Bool { 22 | lhs.rawValue < rhs.rawValue 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/ModelWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelWrapper.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import CodeMonkeyApple 9 | 10 | public enum ModelWrapper: CodableVersionableWrapper { 11 | public typealias Version = ModelVersion 12 | 13 | case v1(ModelV1) 14 | case v2(ModelV2) 15 | case v3(ModelV3) 16 | 17 | // MARK: Public Static Inteface 18 | 19 | public static func deserialize( 20 | version: Version, 21 | from container: KeyedDecodingContainer, 22 | at key: Key 23 | ) throws -> Self { 24 | switch version { 25 | case .v1: 26 | return try .v1(container.decode(ModelV1.self, forKey: key)) 27 | case .v2: 28 | return try .v2(container.decode(ModelV2.self, forKey: key)) 29 | case .v3: 30 | return try .v3(container.decode(ModelV3.self, forKey: key)) 31 | } 32 | } 33 | 34 | // MARK: Public Instance Interface 35 | 36 | public var version: Version { 37 | switch self { 38 | case .v1: 39 | return .v1 40 | case .v2: 41 | return .v2 42 | case .v3: 43 | return .v3 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/StorableTestHarness.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorableTestHarness.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/18/22. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | @testable import CodeMonkeyApple 12 | 13 | struct StorableTestHarness where Target: Equatable & Storable { 14 | private let firstValue: Target 15 | private let secondValue: Target 16 | private let storage: UserDefaults 17 | 18 | // MARK: Internal Initialization 19 | 20 | init(firstValue: Target, secondValue: Target, storage: UserDefaults) { 21 | self.firstValue = firstValue 22 | self.secondValue = secondValue 23 | self.storage = storage 24 | } 25 | 26 | // MARK: Getting Value Tests 27 | 28 | func test() { 29 | test_default_nonoptional() 30 | test_default_optional() 31 | test_set_nonoptional() 32 | test_set_optional_from() 33 | test_set_optional_to() 34 | } 35 | 36 | func test_default_nonoptional() { 37 | typealias Value = Target 38 | let defaultValue: Value = firstValue 39 | let expectedValue: Value = defaultValue 40 | 41 | let key = StorageKey(id: UUID().uuidString, defaultValue: defaultValue) 42 | 43 | XCTAssertEqual(storage.get(key), expectedValue) 44 | } 45 | 46 | func test_default_optional() { 47 | typealias Value = Target? 48 | let defaultValue: Value = nil 49 | let expectedValue: Value = defaultValue 50 | 51 | let key = StorageKey(id: UUID().uuidString, defaultValue: defaultValue) 52 | 53 | XCTAssertEqual(storage.get(key), expectedValue) 54 | } 55 | 56 | func test_set_nonoptional() { 57 | typealias Value = Target 58 | let defaultValue: Value = firstValue 59 | let expectedValue: Value = secondValue 60 | 61 | let key = StorageKey(id: UUID().uuidString, defaultValue: defaultValue) 62 | storage.set(key, to: expectedValue) 63 | 64 | XCTAssertEqual(storage.get(key), expectedValue) 65 | } 66 | 67 | func test_set_optional_from() { 68 | typealias Value = Target? 69 | let defaultValue: Value = nil 70 | let expectedValue: Value = secondValue 71 | 72 | let key = StorageKey(id: UUID().uuidString, defaultValue: defaultValue) 73 | storage.set(key, to: expectedValue) 74 | 75 | let newValue: Value = storage.get(key) 76 | 77 | XCTAssertEqual(newValue, expectedValue) 78 | } 79 | 80 | func test_set_optional_to() { 81 | typealias Value = Target? 82 | let defaultValue: Value = firstValue 83 | let expectedValue: Value = nil 84 | 85 | let key = StorageKey(id: UUID().uuidString, defaultValue: defaultValue) 86 | storage.set(key, to: expectedValue) 87 | 88 | XCTAssertNil(storage.object(forKey: key.id)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/TestCodableStorable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCodableStorable.swift 3 | // CodeMonkeyApple 4 | // 5 | // Created by Kyle Hughes on 4/17/22. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import CodeMonkeyApple 11 | 12 | struct TestCodableStorable: Codable, Equatable, Storable { 13 | static let random = TestCodableStorable(int: .random(in: 0 ... .max), string: UUID().uuidString) 14 | 15 | let int: Int 16 | let string: String 17 | } 18 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/TestRawRepresentableStorable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestRawRepresentableStorable.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 4/17/22. 6 | // 7 | 8 | @testable import CodeMonkeyApple 9 | 10 | enum TestRawRepresentableStorable: String, Equatable, Storable { 11 | case caseOne 12 | case caseTwo = "CASE_TWO" 13 | } 14 | -------------------------------------------------------------------------------- /Tests/CodeMonkeyAppleTests/Utilities/XCTAssertAsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTAssertAsync.swift 3 | // CodeMonkeyAppleTests 4 | // 5 | // Created by Kyle Hughes on 9/19/21. 6 | // 7 | 8 | import XCTest 9 | 10 | // MARK: - Public Global Interface 11 | 12 | public func XCTAsyncAssertEqual( 13 | _ expression1: @autoclosure () async throws -> T, 14 | _ expression2: @autoclosure () async throws -> T, 15 | _ message: @autoclosure () -> String = "", 16 | file: StaticString = #filePath, 17 | line: UInt = #line 18 | ) async rethrows where T: Equatable { 19 | let value1 = try await expression1() 20 | let value2 = try await expression2() 21 | XCTAssertEqual(value1, value2, message(), file: file, line: line) 22 | } 23 | 24 | public func XCTAsyncAssertNil( 25 | _ expression: @autoclosure () async throws -> Any?, 26 | _ message: @autoclosure () -> String = "", 27 | file: StaticString = #filePath, 28 | line: UInt = #line 29 | ) async rethrows { 30 | let value = try await expression() 31 | XCTAssertNil(value, message(), file: file, line: line) 32 | } 33 | 34 | public func XCTAsyncAssertNotNil( 35 | _ expression: @autoclosure () async throws -> Any?, 36 | _ message: @autoclosure () -> String = "", 37 | file: StaticString = #filePath, 38 | line: UInt = #line 39 | ) async rethrows { 40 | let value = try await expression() 41 | XCTAssertNotNil(value, message(), file: file, line: line) 42 | } 43 | 44 | public func XCTAsyncUnwrap( 45 | _ expression: @autoclosure () async throws -> T?, 46 | _ message: @autoclosure () -> String = "", 47 | file: StaticString = #filePath, 48 | line: UInt = #line 49 | ) async throws -> T { 50 | let value = try await expression() 51 | return try XCTUnwrap(value, message(), file: file, line: line) 52 | } 53 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CodeMonkeyAppleTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CodeMonkeyAppleTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------