Adds ExperimentalFeaturesView to browse and configure experimental features
This commit is contained in:
parent
5b4f9ea593
commit
bd0c72e847
@ -176,6 +176,7 @@
|
||||
D53415A7298C78BC00FD67B1 /* Contributors.plist in Resources */ = {isa = PBXBuildFile; fileRef = D53415A6298C78BC00FD67B1 /* Contributors.plist */; };
|
||||
D539103229E88B6C0006B350 /* DeltaPreviews.h in Headers */ = {isa = PBXBuildFile; fileRef = D539103129E88B6C0006B350 /* DeltaPreviews.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
D539104029E88BC40006B350 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D539103F29E88BC40006B350 /* ContentView.swift */; };
|
||||
D54A4BB329E4D27E004C7D57 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; };
|
||||
D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54F710129E89DCB009C069A /* SettingsUserInfoKey.swift */; };
|
||||
D54F710429E89DFC009C069A /* NotificationName+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54F710329E89DFC009C069A /* NotificationName+Settings.swift */; };
|
||||
D55C468F29E761C000EA6DE9 /* AnyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C468E29E761C000EA6DE9 /* AnyFeature.swift */; };
|
||||
@ -183,7 +184,9 @@
|
||||
D5864970297734280081477E /* CheatMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586496F297734280081477E /* CheatMetadata.swift */; };
|
||||
D586497229774ABD0081477E /* CheatBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586497129774ABD0081477E /* CheatBase.swift */; };
|
||||
D5864978297756CE0081477E /* CheatBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5864977297756CE0081477E /* CheatBaseView.swift */; };
|
||||
D5A817B329DF6C6C00904AFE /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817B229DF6C6C00904AFE /* ExperimentalFeatures.swift */; };
|
||||
D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */; };
|
||||
D5A9C00329DDED6D00A8D610 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */; };
|
||||
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 */; };
|
||||
@ -421,6 +424,7 @@
|
||||
D539102F29E88B6B0006B350 /* DeltaPreviews.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DeltaPreviews.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D539103129E88B6C0006B350 /* DeltaPreviews.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeltaPreviews.h; sourceTree = "<group>"; };
|
||||
D539103F29E88BC40006B350 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureDetailView.swift; sourceTree = "<group>"; };
|
||||
D54F710129E89DCB009C069A /* SettingsUserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUserInfoKey.swift; sourceTree = "<group>"; };
|
||||
D54F710329E89DFC009C069A /* NotificationName+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Settings.swift"; sourceTree = "<group>"; };
|
||||
D55C468E29E761C000EA6DE9 /* AnyFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyFeature.swift; sourceTree = "<group>"; };
|
||||
@ -431,8 +435,10 @@
|
||||
D58F39C529E0A473008B4100 /* Option.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Option.swift; sourceTree = "<group>"; };
|
||||
D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+OptionValues.swift"; sourceTree = "<group>"; };
|
||||
D592D6FE29E48FFB008D218A /* OptionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionPickerView.swift; sourceTree = "<group>"; };
|
||||
D5A817B229DF6C6C00904AFE /* ExperimentalFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeatures.swift; sourceTree = "<group>"; };
|
||||
D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D5A9BFFD29DDECF100A8D610 /* Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = "<group>"; };
|
||||
D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.swift; sourceTree = "<group>"; };
|
||||
D5A9C01929DDFBDD00A8D610 /* SettingsName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsName.swift; sourceTree = "<group>"; };
|
||||
D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = "<group>"; };
|
||||
D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = "<group>"; };
|
||||
@ -826,6 +832,7 @@
|
||||
BF11734E1DA32CEC00047DF8 /* Controllers */,
|
||||
BF1DAD5B1D9F574900E752A7 /* Controller Skins */,
|
||||
BF48F74C219A16C100BC2FC1 /* Syncing */,
|
||||
D5A9BFFF29DDECF500A8D610 /* Experimental Features */,
|
||||
D5D797E4298D944C00738869 /* Contributors */,
|
||||
);
|
||||
path = Settings;
|
||||
@ -944,6 +951,7 @@
|
||||
BF59428C1E09BCE50051894B /* Importing */,
|
||||
BF930FFB1EB6D6EC00E8DBA0 /* Systems */,
|
||||
BF525EE61FF5F355004AA849 /* Deep Linking */,
|
||||
D57D795C29F30B1400BB2CF8 /* Experimental Features */,
|
||||
BF5942571E09BB5D0051894B /* Components */,
|
||||
BF696B7E1D9B2AE6009639E0 /* Theming */,
|
||||
BF090CEE1B490C1A00DCAB45 /* Extensions */,
|
||||
@ -1014,6 +1022,14 @@
|
||||
path = DeltaPreviews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D57D795C29F30B1400BB2CF8 /* Experimental Features */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5A817B229DF6C6C00904AFE /* ExperimentalFeatures.swift */,
|
||||
);
|
||||
path = "Experimental Features";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D586496E297734060081477E /* Cheats */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1026,6 +1042,15 @@
|
||||
path = Cheats;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D5A9BFFF29DDECF500A8D610 /* Experimental Features */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */,
|
||||
D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */,
|
||||
);
|
||||
path = "Experimental Features";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D5D797E4298D944C00738869 /* Contributors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1495,6 +1520,7 @@
|
||||
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */,
|
||||
BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */,
|
||||
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
|
||||
D5A9C00329DDED6D00A8D610 /* ExperimentalFeaturesView.swift in Sources */,
|
||||
BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */,
|
||||
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */,
|
||||
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
|
||||
@ -1507,10 +1533,12 @@
|
||||
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */,
|
||||
D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */,
|
||||
BFDCA1E9244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel in Sources */,
|
||||
D5A817B329DF6C6C00904AFE /* ExperimentalFeatures.swift in Sources */,
|
||||
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */,
|
||||
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */,
|
||||
BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */,
|
||||
D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */,
|
||||
D54A4BB329E4D27E004C7D57 /* FeatureDetailView.swift in Sources */,
|
||||
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */,
|
||||
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */,
|
||||
BF3D6C512202865F0083E05A /* Delta2ToDelta3.xcmappingmodel in Sources */,
|
||||
|
||||
19
Delta/Experimental Features/ExperimentalFeatures.swift
Normal file
19
Delta/Experimental Features/ExperimentalFeatures.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// ExperimentalFeatures.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/6/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
struct ExperimentalFeatures: FeatureContainer
|
||||
{
|
||||
static let shared = ExperimentalFeatures()
|
||||
|
||||
private init()
|
||||
{
|
||||
self.prepareFeatures()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
//
|
||||
// ExperimentalFeaturesView.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/5/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
extension ExperimentalFeaturesView
|
||||
{
|
||||
private class ViewModel: ObservableObject
|
||||
{
|
||||
@Published
|
||||
var sortedFeatures: [any AnyFeature]
|
||||
|
||||
init()
|
||||
{
|
||||
// Sort features alphabetically by name.
|
||||
self.sortedFeatures = ExperimentalFeatures.shared.allFeatures.sorted { (featureA, featureB) in
|
||||
return String(describing: featureA.name) < String(describing: featureB.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExperimentalFeaturesView: View
|
||||
{
|
||||
@StateObject
|
||||
private var viewModel: ViewModel = ViewModel()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(content: {}, footer: {
|
||||
Text("These features have been added by contributors to the open-source Delta project on GitHub and are currently being tested.\n\nYou may encounter bugs when using these features.")
|
||||
.font(.subheadline)
|
||||
})
|
||||
|
||||
ForEach(viewModel.sortedFeatures, id: \.key) { feature in
|
||||
section(for: feature)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
|
||||
// Cannot open existential if return type uses concrete type T in non-covariant position (e.g. Box<T>).
|
||||
// So instead we erase return type to AnyView.
|
||||
private func section<T: AnyFeature>(for feature: T) -> AnyView
|
||||
{
|
||||
let section = FeatureSection(feature: feature)
|
||||
return AnyView(section)
|
||||
}
|
||||
}
|
||||
|
||||
extension ExperimentalFeaturesView
|
||||
{
|
||||
static func makeViewController() -> UIHostingController<some View>
|
||||
{
|
||||
let experimentalFeaturesView = ExperimentalFeaturesView()
|
||||
|
||||
let hostingController = UIHostingController(rootView: experimentalFeaturesView)
|
||||
hostingController.title = NSLocalizedString("Experimental Features", comment: "")
|
||||
return hostingController
|
||||
}
|
||||
}
|
||||
|
||||
private struct FeatureSection<T: AnyFeature>: View
|
||||
{
|
||||
@ObservedObject
|
||||
var feature: T
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
NavigationLink(destination: FeatureDetailView(feature: feature)) {
|
||||
HStack {
|
||||
Text(feature.name)
|
||||
Spacer()
|
||||
|
||||
if feature.isEnabled
|
||||
{
|
||||
Text("On")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
if let description = feature.description
|
||||
{
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
Delta/Settings/Experimental Features/FeatureDetailView.swift
Normal file
117
Delta/Settings/Experimental Features/FeatureDetailView.swift
Normal file
@ -0,0 +1,117 @@
|
||||
//
|
||||
// FeatureDetailView.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/10/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
struct FeatureDetailView<Feature: AnyFeature>: View
|
||||
{
|
||||
@ObservedObject
|
||||
var feature: Feature
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(isOn: $feature.isEnabled.animation()) {
|
||||
Text(feature.name)
|
||||
.bold()
|
||||
}
|
||||
} footer: {
|
||||
if let description = feature.description
|
||||
{
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
|
||||
if feature.isEnabled
|
||||
{
|
||||
ForEach(feature.allOptions, id: \.key) { option in
|
||||
if let optionView = optionView(option)
|
||||
{
|
||||
Section {
|
||||
optionView
|
||||
} footer: {
|
||||
if let description = option.description
|
||||
{
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot open existential if return type uses concrete type T in non-covariant position (e.g. Box<T>).
|
||||
// So instead we erase return type to AnyView.
|
||||
private func optionView<T: AnyOption>(_ option: T) -> AnyView?
|
||||
{
|
||||
guard let view = OptionRow(option: option) else { return nil }
|
||||
return AnyView(view)
|
||||
}
|
||||
}
|
||||
|
||||
private struct OptionRow<Option: AnyOption, DetailView: View>: View where DetailView == Option.DetailView
|
||||
{
|
||||
var name: LocalizedStringKey
|
||||
var value: any LocalizedOptionValue
|
||||
var detailView: DetailView
|
||||
|
||||
@State
|
||||
private var displayInline: Bool = false
|
||||
|
||||
init?(option: Option)
|
||||
{
|
||||
// Only show if option has a name, localizable value, and detailView.
|
||||
guard
|
||||
let name = option.name,
|
||||
let value = option.value as? any LocalizedOptionValue,
|
||||
let detailView = option.detailView()
|
||||
else { return nil }
|
||||
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.detailView = detailView
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if displayInline
|
||||
{
|
||||
// Display entire view inline.
|
||||
detailView
|
||||
}
|
||||
else
|
||||
{
|
||||
let wrappedDetailView = Form {
|
||||
detailView
|
||||
}
|
||||
|
||||
NavigationLink(destination: wrappedDetailView) {
|
||||
HStack {
|
||||
Text(name)
|
||||
Spacer()
|
||||
|
||||
value.localizedDescription
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
// Hack to ensure displayInline preference is in View hierarchy.
|
||||
detailView
|
||||
.hidden()
|
||||
.frame(width: 0, height: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(DisplayInlineKey.self) { displayInline in
|
||||
self.displayInline = displayInline
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user