diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index b20b352..9763176 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -167,6 +167,7 @@ BFFDF03F23E3C28A00931B96 /* libGambatte.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03D23E3C0F000931B96 /* libGambatte.a */; }; BFFDF04623E3D3A600931B96 /* libMupen64Plus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */; }; D517F6BA29E730DA000D14D0 /* SettingsName.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C01929DDFBDD00A8D610 /* SettingsName.swift */; }; + D517F6BE29E7535F000D14D0 /* Collection+Optionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = D517F6BD29E7535F000D14D0 /* Collection+Optionals.swift */; }; D524F4A1273DE9A100D500B2 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = D524F4A0273DE9A100D500B2 /* AltKit */; }; D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; }; D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; }; @@ -193,6 +194,7 @@ D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C1E629E5F90200663793 /* OptionValue.swift */; }; D5D7C20329E60F2000663793 /* Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58F39C529E0A473008B4100 /* Option.swift */; }; D5D7C20429E60F2000663793 /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9BFFD29DDECF100A8D610 /* Feature.swift */; }; + D5D7C20629E60F6100663793 /* OptionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D592D6FE29E48FFB008D218A /* OptionPickerView.swift */; }; D5D7C20829E616CF00663793 /* FeatureContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20729E616CF00663793 /* FeatureContainer.swift */; }; D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */; }; /* End PBXBuildFile section */ @@ -407,6 +409,7 @@ BFFDF03D23E3C0F000931B96 /* libGambatte.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libGambatte.a; sourceTree = BUILT_PRODUCTS_DIR; }; BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libMupen64Plus.a; sourceTree = BUILT_PRODUCTS_DIR; }; C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = ""; }; + D517F6BD29E7535F000D14D0 /* Collection+Optionals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Optionals.swift"; sourceTree = ""; }; 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 = ""; }; @@ -425,6 +428,7 @@ D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = ""; }; D58F39C529E0A473008B4100 /* Option.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Option.swift; sourceTree = ""; }; D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+OptionValues.swift"; sourceTree = ""; }; + D592D6FE29E48FFB008D218A /* OptionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionPickerView.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 = ""; }; @@ -1031,6 +1035,7 @@ children = ( D5A9BFFD29DDECF100A8D610 /* Feature.swift */, D58F39C529E0A473008B4100 /* Option.swift */, + D5D7C20529E60F4A00663793 /* Views */, D517F6BB29E737F5000D14D0 /* Types */, D5D7C1FE29E60EF700663793 /* Protocols */, D5D7C1F829E60E8600663793 /* Extensions */, @@ -1042,6 +1047,7 @@ isa = PBXGroup; children = ( D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */, + D517F6BD29E7535F000D14D0 /* Collection+Optionals.swift */, D54F710329E89DFC009C069A /* NotificationName+Settings.swift */, ); path = Extensions; @@ -1058,6 +1064,14 @@ path = Protocols; sourceTree = ""; }; + D5D7C20529E60F4A00663793 /* Views */ = { + isa = PBXGroup; + children = ( + D592D6FE29E48FFB008D218A /* OptionPickerView.swift */, + ); + path = Views; + sourceTree = ""; + }; FD1E8AE87FA2DB8793F7B937 /* Pods */ = { isa = PBXGroup; children = ( @@ -1544,7 +1558,9 @@ D55C468F29E761C000EA6DE9 /* AnyFeature.swift in Sources */, D5D7C20129E60F2000663793 /* LocalizedOptionValue.swift in Sources */, D54F710429E89DFC009C069A /* NotificationName+Settings.swift in Sources */, + D517F6BE29E7535F000D14D0 /* Collection+Optionals.swift in Sources */, D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */, + D5D7C20629E60F6100663793 /* OptionPickerView.swift in Sources */, D5D7C20329E60F2000663793 /* Option.swift in Sources */, D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */, D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */, diff --git a/DeltaFeatures/Extensions/Collection+Optionals.swift b/DeltaFeatures/Extensions/Collection+Optionals.swift new file mode 100644 index 0000000..c451e97 --- /dev/null +++ b/DeltaFeatures/Extensions/Collection+Optionals.swift @@ -0,0 +1,19 @@ +// +// Collection+Optionals.swift +// DeltaFeatures +// +// Created by Riley Testut on 4/12/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +extension Collection +{ + func appendingNil() -> [Element] where Element: OptionalProtocol, Element.Wrapped: LocalizedOptionValue + { + var values = Array(self) + values.append(Element.none) + return values + } +} diff --git a/DeltaFeatures/Option.swift b/DeltaFeatures/Option.swift index 6b4b9d6..8610984 100644 --- a/DeltaFeatures/Option.swift +++ b/DeltaFeatures/Option.swift @@ -16,6 +16,7 @@ public class Option: _AnyOption public let name: LocalizedStringKey? public let description: LocalizedStringKey? + public let values: [Value]? public private(set) var detailView: () -> DetailView? = { nil } // Assigned to property name. @@ -73,17 +74,24 @@ public class Option: _AnyOption } } - private init(defaultValue: Value, name: LocalizedStringKey?, description: LocalizedStringKey?) + private init(defaultValue: Value, name: LocalizedStringKey?, description: LocalizedStringKey?, values: (some Collection)?) { self.defaultValue = defaultValue self.name = name self.description = description + self.values = values.map { Array($0) } + self.detailView = { nil } } + + private convenience init(defaultValue: Value, name: LocalizedStringKey?, description: LocalizedStringKey?) + { + self.init(defaultValue: defaultValue, name: name, description: description, values: [Value]?.none) + } } -// "Hidden" Option (no name or custom SwiftUI view) +// "Hidden" Option (no name, pre-set values, or custom SwiftUI view) public extension Option where DetailView == EmptyView { // Non-Optional @@ -102,6 +110,45 @@ public extension Option where DetailView == EmptyView convenience init(wrappedValue: Value) where Value: OptionalProtocol { self.init(defaultValue: wrappedValue, name: nil, description: nil) + } +} + +// "Picker" Option (User-visible, pre-set options with default picker UI) +public extension Option where Value: LocalizedOptionValue, DetailView == OptionPickerView +{ + // Non-Optional + convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil, values: some Collection) + { + self.init(defaultValue: wrappedValue, name: name, description: description, values: values) + + self.detailView = { [weak self] () -> DetailView? in + guard let self else { return nil } + return OptionPickerView(name: name, options: Array(values), selectedValue: self.valueBinding) + } + } + + // Optional, default = nil + convenience init(name: LocalizedStringKey, description: LocalizedStringKey? = nil, values: some Collection) where Value: OptionalProtocol, Value.Wrapped: LocalizedOptionValue + { + self.init(defaultValue: Value.none, name: name, description: description, values: values) + + self.detailView = { [weak self] () -> DetailView? in + guard let self else { return nil } + return OptionPickerView(name: name, options: values.appendingNil(), selectedValue: self.valueBinding) + } + } + + // Optional, default = non-nil + convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil, values: some Collection) where Value: OptionalProtocol, Value.Wrapped: LocalizedOptionValue + { + self.init(defaultValue: wrappedValue, name: name, description: description, values: values) + + self.detailView = { [weak self] () -> DetailView? in + guard let self else { return nil } + return OptionPickerView(name: name, options: values.appendingNil(), selectedValue: self.valueBinding) + } + } +} // "Custom" Option (User-visible, provides SwiftUI view to configure option) public extension Option where Value: LocalizedOptionValue diff --git a/DeltaFeatures/Protocols/AnyOption.swift b/DeltaFeatures/Protocols/AnyOption.swift index 310dc69..d15cda4 100644 --- a/DeltaFeatures/Protocols/AnyOption.swift +++ b/DeltaFeatures/Protocols/AnyOption.swift @@ -19,6 +19,7 @@ public protocol AnyOption: AnyObject, Identifiable var key: String { get } var settingsKey: SettingsName { get } + var values: [Value]? { get } var detailView: () -> DetailView? { get } var value: Value { get set } diff --git a/DeltaFeatures/Views/OptionPickerView.swift b/DeltaFeatures/Views/OptionPickerView.swift new file mode 100644 index 0000000..3232f88 --- /dev/null +++ b/DeltaFeatures/Views/OptionPickerView.swift @@ -0,0 +1,28 @@ +// +// OptionPickerView.swift +// Delta +// +// Created by Riley Testut on 4/10/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI + +// Type must be public, but not its properties. +public struct OptionPickerView: View +{ + var name: LocalizedStringKey + var options: [Value] + + @Binding + var selectedValue: Value + + public var body: some View { + Picker(name, selection: $selectedValue) { + ForEach(options, id: \.self) { value in + value.localizedDescription + } + } + .pickerStyle(.inline) + } +}