Files
SideStore/AltStore/Unstable Features/UnstableFeatures.swift

128 lines
5.4 KiB
Swift
Raw Permalink Normal View History

2023-05-20 09:24:09 -07:00
//
// UnstableFeatures.swift
// SideStore
//
// Created by naturecodevoid on 5/20/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import Foundation
class UnstableFeatures {
#if UNSTABLE
fileprivate struct Metadata {
/// If true, this Unstable Feature will be available in Advanced Settings instead of being exclusive to Developer Mode.
var availableOutsideDevMode = false
/// Run when the feature is enabled.
var onEnable = {}
/// Run when the feature is disabled
var onDisable = {}
}
#endif
2023-05-20 09:24:09 -07:00
enum Feature: String, CaseIterable {
// The value will be the GitHub Issue number. For example, "123" would correspond to https://github.com/SideStore/SideStore/issues/123
//
// Unstable Features must have a GitHub Issue for tracking progress, PRs and feedback/bug reporting/commenting.
//
// Please order the case by the issue number. They will be ordered by issue number (ascending) in the unstable features menu, so please order them the same way here and in `metadata`.
case swiftUI = "386"
case jitUrlScheme = "385"
case onboarding = "389"
/// Dummy variant to ensure there is always at least one variant. DO NOT USE!
case dummy = "dummy"
2023-05-20 09:24:09 -07:00
#if UNSTABLE
fileprivate var metadata: Metadata {
switch self {
// If your unstable feature is stable enough to be used by nightly users who are not alpha testers or developers,
// you may want to have it available in the Unstable Features menu in Advanced Settings (outside of dev mode). To do so, add this:
//case .yourFeature: return Metadata(availableOutsideDevMode: true)
// You can also add custom hooks for when your feature is enabled or disabled. However, we strongly recommend moving these to a new file. Example: https://github.com/SideStore/SideStore/blob/026392dbc7a5454a39b9287f469d32b5e6768bb8/AltStore/Unstable%20Features/UnstableFeatures%2BSwiftUI.swift
// See the `Metadata` struct for more things you can do.
// Please keep the ordering of the cases in this switch statement the same as the ordering of the enum variants!
case .swiftUI: return Metadata(availableOutsideDevMode: true, onEnable: SwiftUI.onEnable, onDisable: SwiftUI.onDisable)
case .jitUrlScheme: return Metadata(availableOutsideDevMode: true)
default: return Metadata()
}
2023-05-20 09:24:09 -07:00
}
#endif
2023-05-20 09:24:09 -07:00
}
2023-05-20 09:24:09 -07:00
#if UNSTABLE
private static var features: [Feature: Bool] = [:]
static func getFeatures(_ inDevMode: Bool) -> [(key: Feature, value: Bool)] {
// Ensure every feature is in the dictionary
for feature in Feature.allCases {
if features[feature] == nil {
features[feature] = false
}
}
return features
.filter { feature, _ in
feature != .dummy &&
(inDevMode || feature.metadata.availableOutsideDevMode)
}.sorted { a, b in a.key.rawValue > b.key.rawValue } // Convert to array of keys and values (and also sort them by issue number)
}
2023-05-20 09:24:09 -07:00
static func load() {
if features.count > 0 { return print("It seems unstable features have already been loaded, skipping") }
2023-05-20 09:24:09 -07:00
if let rawFeatures = UserDefaults.shared.unstableFeatures,
2023-05-20 10:47:55 -07:00
let rawFeatures = try? JSONDecoder().decode([String: Bool].self, from: rawFeatures) {
2023-05-20 09:24:09 -07:00
for rawFeature in rawFeatures {
if let feature = Feature.allCases.first(where: { feature in String(describing: feature) == rawFeature.key }) {
features[feature] = rawFeature.value
2023-05-20 09:24:09 -07:00
} else {
print("Unknown unstable feature: \(rawFeature.key) = \(rawFeature.value)")
}
}
2023-05-20 09:24:09 -07:00
save(load: true)
} else {
print("Setting all unstable features to false since we couldn't load them from UserDefaults (either they were never saved or there was an error decoding JSON)")
for feature in Feature.allCases {
features[feature] = false
2023-05-20 09:24:09 -07:00
}
save()
}
}
private static func save(load: Bool = false) {
var rawFeatures: [String: Bool] = [:]
for feature in features {
2023-05-20 09:24:09 -07:00
rawFeatures[String(describing: feature.key)] = feature.value
}
UserDefaults.shared.unstableFeatures = try! JSONEncoder().encode(rawFeatures)
print("\(load ? "Loaded" : "Saved") unstable features: \(String(describing: rawFeatures))")
}
static func set(_ feature: Feature, enabled: Bool) {
features[feature] = enabled
// Let's save before running the hooks... they might crash the app or something
2023-05-20 09:24:09 -07:00
save()
// Should be no-op for features with the default hooks (they do nothing)
if enabled {
feature.metadata.onEnable()
} else {
feature.metadata.onDisable()
}
2023-05-20 09:24:09 -07:00
}
#endif
2023-05-20 09:24:09 -07:00
@inline(__always) // hopefully this will help the compiler realize that if statements that use this function should be removed on non-unstable builds
static func enabled(_ feature: Feature) -> Bool {
2023-05-20 09:24:09 -07:00
#if UNSTABLE
features[feature] ?? false
2023-05-20 09:24:09 -07:00
#else
false
#endif
}
}