From 39522fda58220d419fd569b8a2cf9a8b3ec7db79 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 27 Apr 2023 14:58:51 -0500 Subject: [PATCH] [Experimental Feature] Supports AirPlay controller skins Allows users to customize controller skin when AirPlaying games to an external display. --- Cores/DeltaCore | 2 +- Cores/MelonDSDeltaCore | 2 +- Delta.xcodeproj/project.pbxproj | 8 + .../Database/Model/Human/ControllerSkin.swift | 27 ++- .../Model/Misc/ControllerSkinConfigurations.h | 31 +++- Delta/Emulation/GameViewController.swift | 13 +- .../ExperimentalFeatures.swift | 5 + .../Features/AirPlaySkins.swift | 167 ++++++++++++++++++ .../ControllerSkin+Configuring.swift | 23 ++- .../ControllerSkinsViewController.swift | 4 +- .../FeatureDetailView.swift | 14 +- .../EnvironmentValues+FeatureOption.swift | 22 +++ 12 files changed, 284 insertions(+), 34 deletions(-) create mode 100644 Delta/Experimental Features/Features/AirPlaySkins.swift create mode 100644 DeltaFeatures/Extensions/EnvironmentValues+FeatureOption.swift diff --git a/Cores/DeltaCore b/Cores/DeltaCore index 3ce43f3..44a999a 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit 3ce43f3103c637dfdb27f85fc0d0041c8a37292b +Subproject commit 44a999ab2c974bcbc7bbc71753a61b77fa306c07 diff --git a/Cores/MelonDSDeltaCore b/Cores/MelonDSDeltaCore index b0eeb87..581fd35 160000 --- a/Cores/MelonDSDeltaCore +++ b/Cores/MelonDSDeltaCore @@ -1 +1 @@ -Subproject commit b0eeb87c41cf5d78182879a10a51f7c147a60ef7 +Subproject commit 581fd3557c4ffd2cfb7dd049dfba14ed2f14a96c diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index f7fdabf..5bae28e 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; }; D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ADD12129F33FBF00CE0560 /* Features.swift */; }; D5D78AE529F9BC3700E064F0 /* DSAirPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */; }; + D5D78AE729F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.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 */; }; @@ -211,6 +212,7 @@ D5D7C20829E616CF00663793 /* FeatureContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20729E616CF00663793 /* FeatureContainer.swift */; }; D5D7C20A29E61FA600663793 /* OptionToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20929E61FA600663793 /* OptionToggleView.swift */; }; D5D7C20C29E624CB00663793 /* DisplayInlineKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20B29E624CB00663793 /* DisplayInlineKey.swift */; }; + D5DF06E029F326E6009E577C /* AirPlaySkins.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */; }; D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */; }; /* End PBXBuildFile section */ @@ -457,6 +459,7 @@ D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = ""; }; D5ADD12129F33FBF00CE0560 /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.swift; sourceTree = ""; }; D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSAirPlay.swift; sourceTree = ""; }; + D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeatureOption.swift"; sourceTree = ""; }; D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = ""; }; D5D797E7298DC9E200738869 /* cheatbase.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = cheatbase.zip; sourceTree = ""; }; D5D7C1E629E5F90200663793 /* OptionValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionValue.swift; sourceTree = ""; }; @@ -465,6 +468,7 @@ D5D7C20729E616CF00663793 /* FeatureContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureContainer.swift; sourceTree = ""; }; D5D7C20929E61FA600663793 /* OptionToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionToggleView.swift; sourceTree = ""; }; D5D7C20B29E624CB00663793 /* DisplayInlineKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayInlineKey.swift; sourceTree = ""; }; + D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AirPlaySkins.swift; sourceTree = ""; }; D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySearchBar.swift; sourceTree = ""; }; DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -1084,6 +1088,7 @@ isa = PBXGroup; children = ( D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */, + D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */, ); path = Features; sourceTree = ""; @@ -1134,6 +1139,7 @@ D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */, D517F6BD29E7535F000D14D0 /* Collection+Optionals.swift */, D54F710329E89DFC009C069A /* NotificationName+Settings.swift */, + D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift */, ); path = Extensions; sourceTree = ""; @@ -1538,6 +1544,7 @@ BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */, BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */, BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */, + D5DF06E029F326E6009E577C /* AirPlaySkins.swift in Sources */, D5D797E6298D946200738869 /* Contributor.swift in Sources */, D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */, BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */, @@ -1662,6 +1669,7 @@ D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */, D5D7C20629E60F6100663793 /* OptionPickerView.swift in Sources */, D5D7C20329E60F2000663793 /* Option.swift in Sources */, + D5D78AE729F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift in Sources */, D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */, D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */, ); diff --git a/Delta/Database/Model/Human/ControllerSkin.swift b/Delta/Database/Model/Human/ControllerSkin.swift index 6ab08bb..ea09c99 100644 --- a/Delta/Database/Model/Human/ControllerSkin.swift +++ b/Delta/Database/Model/Human/ControllerSkin.swift @@ -13,16 +13,27 @@ import Harmony extension ControllerSkinConfigurations { - init(traits: DeltaCore.ControllerSkin.Traits) + init?(traits: DeltaCore.ControllerSkin.Traits) { - switch (traits.displayType, traits.orientation) + switch (traits.device, traits.displayType, traits.orientation) { - case (.standard, .portrait): self = .standardPortrait - case (.standard, .landscape): self = .standardLandscape - case (.edgeToEdge, .portrait): self = .edgeToEdgePortrait - case (.edgeToEdge, .landscape): self = .edgeToEdgeLandscape - case (.splitView, .portrait): self = .splitViewPortrait - case (.splitView, .landscape): self = .splitViewLandscape + case (.iphone, .standard, .portrait): self = .iphoneStandardPortrait + case (.iphone, .standard, .landscape): self = .iphoneStandardLandscape + case (.iphone, .edgeToEdge, .portrait): self = .iphoneEdgeToEdgePortrait + case (.iphone, .edgeToEdge, .landscape): self = .iphoneEdgeToEdgeLandscape + case (.iphone, .splitView, _): return nil + + case (.ipad, .standard, .portrait): self = .ipadStandardPortrait + case (.ipad, .standard, .landscape): self = .ipadStandardLandscape + case (.ipad, .edgeToEdge, .portrait): self = .ipadEdgeToEdgePortrait + case (.ipad, .edgeToEdge, .landscape): self = .ipadEdgeToEdgeLandscape + case (.ipad, .splitView, .portrait): self = .ipadSplitViewPortrait + case (.ipad, .splitView, .landscape): self = .ipadSplitViewLandscape + + case (.tv, .standard, .portrait): self = .tvStandardPortrait + case (.tv, .standard, .landscape): self = .tvStandardLandscape + case (.tv, .edgeToEdge, _): return nil + case (.tv, .splitView, _): return nil } } } diff --git a/Delta/Database/Model/Misc/ControllerSkinConfigurations.h b/Delta/Database/Model/Misc/ControllerSkinConfigurations.h index 6e44186..df98d99 100644 --- a/Delta/Database/Model/Misc/ControllerSkinConfigurations.h +++ b/Delta/Database/Model/Misc/ControllerSkinConfigurations.h @@ -9,16 +9,35 @@ #ifndef ControllerSkinConfigurations_h #define ControllerSkinConfigurations_h +// Every possible (supported) combination of traits. typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations) { - ControllerSkinConfigurationStandardPortrait = 1 << 0, - ControllerSkinConfigurationStandardLandscape = 1 << 1, + /* iPhone */ + ControllerSkinConfigurationiPhoneStandardPortrait NS_SWIFT_NAME(iphoneStandardPortrait) = 1 << 0, + ControllerSkinConfigurationiPhoneStandardLandscape NS_SWIFT_NAME(iphoneStandardLandscape) = 1 << 1, - ControllerSkinConfigurationSplitViewPortrait = 1 << 2, - ControllerSkinConfigurationSplitViewLandscape = 1 << 3, + // iPhone doesn't support Split View + // ControllerSkinConfigurationiPhoneSplitViewPortrait = 1 << 2, + // ControllerSkinConfigurationiPhoneSplitViewLandscape = 1 << 3, - ControllerSkinConfigurationEdgeToEdgePortrait = 1 << 4, - ControllerSkinConfigurationEdgeToEdgeLandscape = 1 << 5, + ControllerSkinConfigurationiPhoneEdgeToEdgePortrait NS_SWIFT_NAME(iphoneEdgeToEdgePortrait) = 1 << 4, + ControllerSkinConfigurationiPhoneEdgeToEdgeLandscape NS_SWIFT_NAME(iphoneEdgeToEdgeLandscape) = 1 << 5, + + + /* iPad */ + ControllerSkinConfigurationiPadStandardPortrait NS_SWIFT_NAME(ipadStandardPortrait) = 1 << 6, + ControllerSkinConfigurationiPadStandardLandscape NS_SWIFT_NAME(ipadStandardLandscape) = 1 << 7, + + ControllerSkinConfigurationiPadSplitViewPortrait NS_SWIFT_NAME(ipadSplitViewPortrait) = 1 << 2, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewPortrait + ControllerSkinConfigurationiPadSplitViewLandscape NS_SWIFT_NAME(ipadSplitViewLandscape) = 1 << 3, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewLandscape + + ControllerSkinConfigurationiPadEdgeToEdgePortrait NS_SWIFT_NAME(ipadEdgeToEdgePortrait) = 1 << 8, + ControllerSkinConfigurationiPadEdgeToEdgeLandscape NS_SWIFT_NAME(ipadEdgeToEdgeLandscape) = 1 << 9, + + + /* TV */ + ControllerSkinConfigurationTVStandardPortrait = 1 << 10, + ControllerSkinConfigurationTVStandardLandscape = 1 << 11, }; #endif /* ControllerSkinConfigurations_h */ diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index eef3557..d9346e6 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -1144,7 +1144,13 @@ private extension GameViewController if let game = self.game, let traits = scene.gameViewController.controllerView.controllerSkinTraits { - if let standardSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), standardSkin.supports(traits) + if ExperimentalFeatures.shared.airPlaySkins.isEnabled, + let preferredControllerSkin = ExperimentalFeatures.shared.airPlaySkins.preferredAirPlayControllerSkin(for: game.type), preferredControllerSkin.supports(traits) + { + // Use preferredControllerSkin directly. + controllerSkin = preferredControllerSkin + } + else if let standardSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), standardSkin.supports(traits) { if standardSkin.hasTouchScreen(for: traits) { @@ -1359,6 +1365,11 @@ private extension GameViewController case Settings.features.dsAirPlay.$layoutAxis.settingsKey: self.updateExternalDisplay() + case ExperimentalFeatures.shared.airPlaySkins.settingsKey: fallthrough + case _ where settingsName.rawValue.hasPrefix(ExperimentalFeatures.shared.airPlaySkins.settingsKey.rawValue): + // Update whenever any of the AirPlay skins have changed. + self.updateExternalDisplay() + default: break } } diff --git a/Delta/Experimental Features/ExperimentalFeatures.swift b/Delta/Experimental Features/ExperimentalFeatures.swift index 3a6125a..b5edba8 100644 --- a/Delta/Experimental Features/ExperimentalFeatures.swift +++ b/Delta/Experimental Features/ExperimentalFeatures.swift @@ -12,6 +12,11 @@ struct ExperimentalFeatures: FeatureContainer { static let shared = ExperimentalFeatures() + @Feature(name: "AirPlay Skins", + description: "Customize the appearance of games when AirPlaying to your TV.", + options: AirPlaySkinsOptions()) + var airPlaySkins + @Feature(name: "Variable Fast Forward", description: "Change the preferred Fast Foward speed per-system. You can also change it by long-pressing the Fast Forward button from the Pause Menu.", options: VariableFastForwardOptions()) diff --git a/Delta/Experimental Features/Features/AirPlaySkins.swift b/Delta/Experimental Features/Features/AirPlaySkins.swift new file mode 100644 index 0000000..ee8246f --- /dev/null +++ b/Delta/Experimental Features/Features/AirPlaySkins.swift @@ -0,0 +1,167 @@ +// +// AirPlaySkins.swift +// Delta +// +// Created by Riley Testut on 4/20/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI + +import DeltaFeatures +import DeltaCore + +extension Feature where Options == AirPlaySkinsOptions +{ + func preferredAirPlayControllerSkin(for gameType: GameType) -> ControllerSkin? + { + guard let identifier = self[gameType] else { return nil } + + let predicate = NSPredicate(format: "%K == %@", #keyPath(ControllerSkin.identifier), identifier) + let controllerSkin = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self).first + return controllerSkin + } +} + +struct AirPlaySkinsOptions +{ + @Option(name: "Manage Skins", detailView: { _ in SkinManager() }) + private var skinManager: String = "" // Hack until I figure out how to support Void properties... + + @Option(name: LocalizedStringKey(System.nes.localizedName), description: "The controller skin used when AirPlaying NES games.", detailView: { SkinPicker(gameType: .nes, controllerSkinID: $0) }) + var nes: String? + + @Option(name: LocalizedStringKey(System.snes.localizedName), description: "The controller skin used when AirPlaying SNES games.", detailView: { SkinPicker(gameType: .snes, controllerSkinID: $0) }) + var snes: String? + + @Option(name: LocalizedStringKey(System.genesis.localizedName), description: "The controller skin used when AirPlaying Genesis games.", detailView: { SkinPicker(gameType: .genesis, controllerSkinID: $0) }) + var genesis: String? + + @Option(name: LocalizedStringKey(System.n64.localizedName), description: "The controller skin used when AirPlaying N64 games.", detailView: { SkinPicker(gameType: .n64, controllerSkinID: $0) }) + var n64: String? + + @Option(name: LocalizedStringKey(System.gbc.localizedName), description: "The controller skin used when AirPlaying GBC games.", detailView: { SkinPicker(gameType: .gbc, controllerSkinID: $0) }) + var gbc: String? + + @Option(name: LocalizedStringKey(System.gba.localizedName), description: "The controller skin used when AirPlaying GBA games.", detailView: { SkinPicker(gameType: .gba, controllerSkinID: $0) }) + var gba: String? + + @Option(name: LocalizedStringKey(System.ds.localizedName), description: "The controller skin used when AirPlaying DS games.", detailView: { SkinPicker(gameType: .ds, controllerSkinID: $0) }) + var ds: String? + + subscript(gameType: GameType) -> String? { + guard let system = System(gameType: gameType) else { return nil } + switch system + { + case .nes: return self.nes + case .snes: return self.snes + case .genesis: return self.genesis + case .n64: return self.n64 + case .gbc: return self.gbc + case .gba: return self.gba + case .ds: return self.ds + } + } +} + +fileprivate extension AirPlaySkinsOptions +{ + struct SkinPicker: View + { + let gameType: GameType + + @Binding + var controllerSkinID: String? + + @FetchRequest + private var controllerSkins: FetchedResults + + @Environment(\.featureOption) + private var option + + var body: some View { + Picker(option.name ?? "", selection: $controllerSkinID) { + ForEach(controllerSkins, id: \.identifier) { controllerSkin in + Text(controllerSkin.name) + .tag(Optional(controllerSkin.identifier)) // Must be Optional in order for selection to work. + // .tag(controllerSkin.identifier) + } + + Text("None") + .tag(String?.none) + } + .pickerStyle(.menu) + .displayInline() + } + + init(gameType: GameType, controllerSkinID: Binding) + { + self.gameType = gameType + self._controllerSkinID = controllerSkinID + + let configuration = ControllerSkinConfigurations.tvStandardLandscape + + let predicate = NSPredicate(format: "%K == %@ AND (%K & %d) != 0 AND %K == NO", + #keyPath(ControllerSkin.gameType), self.gameType.rawValue, + #keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue, + #keyPath(ControllerSkin.isStandard)) + + self._controllerSkins = FetchRequest(entity: ControllerSkin.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)], predicate: predicate) + } + } + + struct SkinManager: View + { + @FetchRequest(entity: ControllerSkin.entity(), + sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)], + predicate: { + let configuration = ControllerSkinConfigurations.tvStandardLandscape + return NSPredicate(format: "(%K & %d) != 0 AND %K == NO", + #keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue, + #keyPath(ControllerSkin.isStandard)) + }()) + private var controllerSkins: FetchedResults + + var body: some View { + if controllerSkins.isEmpty + { + Text("No AirPlay Skins") + .foregroundColor(.gray) + } + else + { + List { + ForEach(controllerSkins, id: \.identifier) { controllerSkin in + HStack { + Text(controllerSkin.name) + + Spacer() + + if let system = System(gameType: controllerSkin.gameType) + { + Text(system.localizedShortName) + .foregroundColor(.gray) + } + } + } + .onDelete(perform: deleteAirPlaySkins) + } + } + } + + private func deleteAirPlaySkins(at indexes: IndexSet) + { + let objectIDs = indexes.map { controllerSkins[$0].objectID } + + DatabaseManager.shared.performBackgroundTask { context in + let controllerSkins = objectIDs.compactMap { context.object(with: $0) as? ControllerSkin } + for controllerSkin in controllerSkins + { + context.delete(controllerSkin) + } + + context.saveWithErrorLogging() + } + } + } +} diff --git a/Delta/Extensions/ControllerSkin+Configuring.swift b/Delta/Extensions/ControllerSkin+Configuring.swift index 6b3f275..55d95ca 100644 --- a/Delta/Extensions/ControllerSkin+Configuring.swift +++ b/Delta/Extensions/ControllerSkin+Configuring.swift @@ -34,21 +34,20 @@ extension ControllerSkin var configurations = ControllerSkinConfigurations() - let device: DeltaCore.ControllerSkin.Device = (UIDevice.current.userInterfaceIdiom == .pad) ? .ipad : .iphone - - let traitCollections: [(displayType: DeltaCore.ControllerSkin.DisplayType, orientation: DeltaCore.ControllerSkin.Orientation)] = - [(.standard, .portrait), (.standard, .landscape), (.edgeToEdge, .portrait), (.edgeToEdge, .landscape), (.splitView, .portrait), (.splitView, .landscape)] - - for collection in traitCollections - { - let traits = DeltaCore.ControllerSkin.Traits(device: device, displayType: collection.displayType, orientation: collection.orientation) - if skin.supports(traits) - { - let configuration = ControllerSkinConfigurations(traits: traits) - configurations.formUnion(configuration) + let allTraitCombinations = DeltaCore.ControllerSkin.Device.allCases.flatMap { device in + DeltaCore.ControllerSkin.DisplayType.allCases.flatMap { displayType in + DeltaCore.ControllerSkin.Orientation.allCases.map { orientation in + DeltaCore.ControllerSkin.Traits(device: device, displayType: displayType, orientation: orientation) + } } } + for traits in allTraitCombinations + { + guard let configuration = ControllerSkinConfigurations(traits: traits), skin.supports(traits) else { continue } + configurations.formUnion(configuration) + } + self.supportedConfigurations = configurations } } diff --git a/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift b/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift index 1b96e0f..03726ec 100644 --- a/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift +++ b/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift @@ -108,13 +108,13 @@ private extension ControllerSkinsViewController { guard let system = self.system, let traits = self.traits else { return } - let configuration = ControllerSkinConfigurations(traits: traits) + guard let configuration = ControllerSkinConfigurations(traits: traits) else { return } let fetchRequest: NSFetchRequest = ControllerSkin.fetchRequest() if traits.device == .iphone && traits.displayType == .edgeToEdge { - let fallbackConfiguration: ControllerSkinConfigurations = (traits.orientation == .landscape) ? .standardLandscape : .standardPortrait + let fallbackConfiguration: ControllerSkinConfigurations = (traits.orientation == .landscape) ? .iphoneStandardLandscape : .iphoneStandardPortrait // Allow selecting skins that only support standard display types as well. fetchRequest.predicate = NSPredicate(format: "%K == %@ AND ((%K & %d) != 0 OR (%K & %d) != 0)", diff --git a/Delta/Settings/Experimental Features/FeatureDetailView.swift b/Delta/Settings/Experimental Features/FeatureDetailView.swift index e07b5c9..ef0cafb 100644 --- a/Delta/Settings/Experimental Features/FeatureDetailView.swift +++ b/Delta/Settings/Experimental Features/FeatureDetailView.swift @@ -59,9 +59,11 @@ struct FeatureDetailView: View private struct OptionRow: View where DetailView == Option.DetailView { - var name: LocalizedStringKey - var value: any LocalizedOptionValue - var detailView: DetailView + let name: LocalizedStringKey + let value: any LocalizedOptionValue + let detailView: DetailView + + let option: Option @State private var displayInline: Bool = false @@ -78,10 +80,16 @@ private struct OptionRow: View where Detail self.name = name self.value = value self.detailView = detailView + + self.option = option } var body: some View { VStack { + let detailView = detailView + .environment(\.managedObjectContext, DatabaseManager.shared.viewContext) + .environment(\.featureOption, option) + if displayInline { // Display entire view inline. diff --git a/DeltaFeatures/Extensions/EnvironmentValues+FeatureOption.swift b/DeltaFeatures/Extensions/EnvironmentValues+FeatureOption.swift new file mode 100644 index 0000000..82d165c --- /dev/null +++ b/DeltaFeatures/Extensions/EnvironmentValues+FeatureOption.swift @@ -0,0 +1,22 @@ +// +// EnvironmentValues+FeatureOption.swift +// DeltaFeatures +// +// Created by Riley Testut on 4/26/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI + +private struct FeatureOptionKey: EnvironmentKey +{ + static let defaultValue: any AnyOption = Option(wrappedValue: true) +} + +public extension EnvironmentValues +{ + var featureOption: any AnyOption { + get { self[FeatureOptionKey.self] } + set { self[FeatureOptionKey.self] = newValue } + } +}