diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 1d5d9c3..b1e00b2 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -184,7 +184,9 @@ D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; }; D5D797E6298D946200738869 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D797E5298D946200738869 /* Contributor.swift */; }; D5D797E9298DCC7300738869 /* cheatbase.zip in Resources */ = {isa = PBXBuildFile; fileRef = D5D797E7298DC9E200738869 /* cheatbase.zip */; }; + D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */; }; D5D7C1FA29E60EDE00663793 /* libDeltaFeatures.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D5D7C1F129E60DFF00663793 /* libDeltaFeatures.a */; }; + D5D7C1FD29E60EEF00663793 /* OptionalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D532B8BC29E5EDAD009EE27C /* OptionalProtocol.swift */; }; D5D7C20129E60F2000663793 /* LocalizedOptionValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C1E829E5FCDE00663793 /* LocalizedOptionValue.swift */; }; D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C1E629E5F90200663793 /* OptionValue.swift */; }; D5D7C20429E60F2000663793 /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9BFFD29DDECF100A8D610 /* Feature.swift */; }; @@ -405,6 +407,7 @@ D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = ""; }; D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = ""; }; D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = ""; }; + D532B8BC29E5EDAD009EE27C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = ""; }; D53415A4298C782A00FD67B1 /* ContributorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorsView.swift; sourceTree = ""; }; D53415A6298C78BC00FD67B1 /* Contributors.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Contributors.plist; sourceTree = ""; }; D539102F29E88B6B0006B350 /* DeltaPreviews.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DeltaPreviews.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -415,6 +418,7 @@ D586496F297734280081477E /* CheatMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatMetadata.swift; sourceTree = ""; }; D586497129774ABD0081477E /* CheatBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBase.swift; sourceTree = ""; }; D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = ""; }; + D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+OptionValues.swift"; sourceTree = ""; }; D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; D5A9BFFD29DDECF100A8D610 /* Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = ""; }; D5A9C01929DDFBDD00A8D610 /* SettingsName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsName.swift; sourceTree = ""; }; @@ -1030,6 +1034,7 @@ D5D7C1F829E60E8600663793 /* Extensions */ = { isa = PBXGroup; children = ( + D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */, D54F710329E89DFC009C069A /* NotificationName+Settings.swift */, ); path = Extensions; @@ -1039,6 +1044,7 @@ isa = PBXGroup; children = ( D5D7C20729E616CF00663793 /* FeatureContainer.swift */, + D532B8BC29E5EDAD009EE27C /* OptionalProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -1524,10 +1530,12 @@ D5D7C20829E616CF00663793 /* FeatureContainer.swift in Sources */, D517F6BA29E730DA000D14D0 /* SettingsName.swift in Sources */, D5D7C20429E60F2000663793 /* Feature.swift in Sources */, + D5D7C1FD29E60EEF00663793 /* OptionalProtocol.swift in Sources */, D5D7C20129E60F2000663793 /* LocalizedOptionValue.swift in Sources */, D54F710429E89DFC009C069A /* NotificationName+Settings.swift in Sources */, D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */, D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */, + D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DeltaFeatures/Extensions/UserDefaults+OptionValues.swift b/DeltaFeatures/Extensions/UserDefaults+OptionValues.swift new file mode 100644 index 0000000..c909e36 --- /dev/null +++ b/DeltaFeatures/Extensions/UserDefaults+OptionValues.swift @@ -0,0 +1,115 @@ +// +// UserDefaults+OptionValues.swift +// Delta +// +// Created by Riley Testut on 4/7/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +private func wrap(rawValue: RawType, in type: WrapperType.Type) -> WrapperType? +{ + // Ensure rawValue is correct type. + guard let rawValue = rawValue as? WrapperType.RawValue else { return nil } + + let representingValue = WrapperType.init(rawValue: rawValue) + return representingValue +} + +extension UserDefaults +{ + func setOptionValue(_ newValue: Value, forKey key: String) throws + { + switch newValue + { + // case .none/nil does _not_ catch nil values passed in, + // but casting to NSSecureCoding then checking if NSNull does. + // case .none: break + + case let secureCoding as any NSSecureCoding: + if secureCoding is NSNull + { + // Removing value will make us return default value later, + // which isn't what we want if we explicitly set nil. + // Instead, we persist a dictionary with "isNil" key to let + // us know we should return nil later, not the default value. + let nilDictionary = ["isNil": true] as NSDictionary + self.set(nilDictionary, forKey: key) + } + else + { + self.set(secureCoding, forKey: key) + } + + case let rawRepresentable as any RawRepresentable: + self.set(rawRepresentable.rawValue, forKey: key) + + case let codable as any Codable: + let data = try PropertyListEncoder().encode(codable) + self.set(data, forKey: key) + + default: + // Try anyway ¯\_(ツ)_/¯ + self.set(newValue, forKey: key) + } + } + + // Returns Optional. If Value is already an Optional type, this will return a *nested* Optional>. + // If return value == nil, value does _not_ yet exist, so we should use default value. + // If return value == .some(nil) (aka nested nil), the value _does_ exist, and it is explicitly nil. + func optionValue(forKey key: String, type: Value.Type) throws -> Value? + { + guard let rawValue = UserDefaults.standard.object(forKey: key) else { return nil } + + if let nilDictionary = rawValue as? [String: Bool], let isNil = nilDictionary["isNil"], let optionalType = Value.self as? any OptionalProtocol.Type, isNil + { + // Return nil nested inside Optional (aka .some(nil)). + // Caller will treat it as non-nil and thus won't return default value. + let nestedNil = optionalType.none as? Value + return nestedNil + } + + if let value = rawValue as? Value + { + return value + } + else if let optionalType = Value.self as? any OptionalProtocol.Type, let rawRepresentableType = optionalType.wrappedType as? any RawRepresentable.Type + { + // Open `rawRepresentableType` existential as concrete type so we can initialize RawRepresentable. + // Don't cast via as? Value yet because that may result in `.some(nil)` if Value is optional. + guard let rawRepresentable = wrap(rawValue: rawValue, in: rawRepresentableType) else { + // Incorrect raw type, so return nil directly without nesting to use default value. + return nil + } + + // Return (potentially) nested optional. + return rawRepresentable as? Value + } + else if let rawRepresentableType = Value.self as? any RawRepresentable.Type + { + // Open `rawRepresentableType` existential as concrete type so we can initialize RawRepresentable. + // Don't cast via as? Value yet because that may result in `.some(nil)` if Value is optional. + guard let rawRepresentable = wrap(rawValue: rawValue, in: rawRepresentableType) else { + // Incorrect raw type, so return nil directly without nesting to use default value. + return nil + } + + // Return (potentially) nested optional. + return rawRepresentable as? Value + } + else if let codableType = Value.self as? any Codable.Type, let data = rawValue as? Data + { + let decodedValue = try PropertyListDecoder().decode(codableType, from: data) as? Value + return decodedValue + } + else + { + print("[RSTLog] Unsupported option type:", Value.self) + + // Return nil directly, no nesting. + // Caller will treat this as nil and will return the default value instead. + return nil + } + } +} diff --git a/DeltaFeatures/Protocols/OptionalProtocol.swift b/DeltaFeatures/Protocols/OptionalProtocol.swift new file mode 100644 index 0000000..2192b97 --- /dev/null +++ b/DeltaFeatures/Protocols/OptionalProtocol.swift @@ -0,0 +1,24 @@ +// +// OptionalProtocol.swift +// Delta +// +// Created by Riley Testut on 4/11/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +// Public so we can use as generic constraint. +public protocol OptionalProtocol +{ + associatedtype Wrapped + + static var none: Self { get } + + static var wrappedType: Wrapped.Type { get } +} + +extension Optional: OptionalProtocol +{ + public static var wrappedType: Wrapped.Type { return Wrapped.self } +}