Path: blob/a-new-beginning/Folium-iOS/Controllers/SettingsControllers/GrapeSettingsController.swift
2 views
//
// GrapeSettingsController.swift
// Folium-iOS
//
// Created by Jarrod Norwell on 19/8/2025.
//
import Grape
import Foundation
import SettingsKit
import UIKit
enum GrapeSettingsHeaders : String, CaseIterable {
case core = "Core"
var header: SettingHeader {
switch self {
case .core:
SettingHeader(text: rawValue,
secondaryText: "Core Emulation Settings")
}
}
static var allHeaders: [SettingHeader] { allCases.map { $0.header } }
}
enum GrapeSettingsItems : String, CaseIterable {
// Core
case directBoot = "grape.v1.38.directBoot"
case dsiMode = "grape.v1.38.dsiMode"
var title: String {
switch self {
case .directBoot:
"Direct Boot"
case .dsiMode:
"DSi Mode"
}
}
var details: String? {
switch self {
case .directBoot:
"Boots directly into the game, skipping the DS or DSi start up"
case .dsiMode:
"Enables DSi mode adding DSi camera, game and sound support"
}
}
func setting(_ delegate: SettingDelegate? = nil) -> BaseSetting {
switch self {
case .directBoot,
.dsiMode:
BoolSetting(key: rawValue,
title: title,
details: details,
value: UserDefaults.standard.bool(forKey: rawValue),
delegate: delegate)
}
}
static func settings(_ header: GrapeSettingsHeaders) -> [GrapeSettingsItems] {
switch header {
case .core:
[.directBoot, .dsiMode]
}
}
}
class GrapeSettingsController : UIViewController, UICollectionViewDelegate {
var dataSource: UICollectionViewDiffableDataSource<GrapeSettingsHeaders, AnyHashableSendable>! = nil
var snapshot: NSDiffableDataSourceSnapshot<GrapeSettingsHeaders, AnyHashableSendable>! = nil
override func viewDidLoad() {
super.viewDidLoad()
if let navigationController {
navigationController.navigationBar.prefersLargeTitles = true
}
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), primaryAction: UIAction { _ in
self.dismiss(animated: true)
})
if #available(iOS 26, *) {
navigationItem.title = "Grape"
navigationItem.largeTitle = navigationItem.title
navigationItem.subtitle = "Nintendo DS (melonDS)"
navigationItem.largeSubtitle = navigationItem.subtitle
} else {
title = "Grape"
}
navigationItem.style = .browser
var configuration: UICollectionLayoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.backgroundColor = .clear
configuration.headerMode = .supplementary
configuration.trailingSwipeActionsConfigurationProvider = { indexPath in
guard let dataSource = self.dataSource, let item: BaseSetting = dataSource.itemIdentifier(for: indexPath) as? BaseSetting else {
return UISwipeActionsConfiguration()
}
let informationContextualAction: UIContextualAction = UIContextualAction(style: .normal, title: nil, handler: { action, view, performed in
let alertController: UIAlertController = UIAlertController(title: item.title, message: item.details, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: .default) { _ in
performed(true)
})
// alertController.preferredAction = alertController.actions.first
self.present(alertController, animated: true)
})
informationContextualAction.image = UIImage(systemName: "info")
return UISwipeActionsConfiguration(actions: [
informationContextualAction
])
}
let collectionView: UICollectionView = UICollectionView(frame: .zero,
collectionViewLayout: UICollectionViewCompositionalLayout.list(using: configuration))
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
if #available(iOS 26, *) {
let backgroundView: UIVisualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular))
backgroundView.cornerConfiguration = .corners(radius: .containerConcentric())
collectionView.backgroundColor = .clear
collectionView.backgroundView = backgroundView
view.backgroundColor = .clear
} else {
view.backgroundColor = .systemBackground
}
let selectionCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SelectionSetting> { cell, indexPath, itemIdentifier in
if #available(iOS 26, *) {
var backgroundConfiguration: UIBackgroundConfiguration = .clear()
let effect: UIGlassEffect = UIGlassEffect(style: .regular)
let visualEffectView: UIVisualEffectView = UIVisualEffectView(effect: effect)
visualEffectView.cornerConfiguration = .corners(topLeftRadius: .fixed(cell.effectiveRadius(corner: .topLeft)),
topRightRadius: .fixed(cell.effectiveRadius(corner: .topRight)),
bottomLeftRadius: .fixed(cell.effectiveRadius(corner: .bottomLeft)),
bottomRightRadius: .fixed(cell.effectiveRadius(corner: .bottomRight)))
backgroundConfiguration.customView = visualEffectView
cell.backgroundConfiguration = backgroundConfiguration
}
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = itemIdentifier.title
cell.contentConfiguration = contentConfiguration
let children: [UIMenuElement] = switch itemIdentifier.values {
case let stringInt as [String : Int]:
stringInt.reduce(into: [UIAction](), { partialResult, element in
var state: UIMenuElement.State = .off
if let selectedValue = itemIdentifier.selectedValue as? Int {
state = element.value == selectedValue ? .on : .off
}
partialResult.append(.init(title: element.key, state: state, handler: { _ in
UserDefaults.standard.set(element.value, forKey: itemIdentifier.key)
if let delegate = itemIdentifier.delegate {
delegate.didChangeSetting(at: indexPath)
}
}))
})
case let stringString as [String : String]:
stringString.reduce(into: [UIAction](), { partialResult, element in
var state: UIMenuElement.State = .off
if let selectedValue = itemIdentifier.selectedValue as? String {
state = element.value == selectedValue ? .on : .off
}
partialResult.append(.init(title: element.key, state: state, handler: { _ in
UserDefaults.standard.set(element.value, forKey: itemIdentifier.key)
if let delegate = itemIdentifier.delegate {
delegate.didChangeSetting(at: indexPath)
}
}))
})
default:
[]
}
var title = "Automatic"
if let selectedValue = itemIdentifier.selectedValue {
switch selectedValue {
case let intValue as Int:
if let values = itemIdentifier.values as? [String : Int] {
title = values.first(where: { $0.value == intValue })?.key ?? title
}
case let stringValue as String:
if let values = itemIdentifier.values as? [String : String] {
title = values.first(where: { $0.value == stringValue })?.key ?? title
}
default:
break
}
}
cell.accessories = [
UICellAccessory.label(text: title),
UICellAccessory.popUpMenu(UIMenu(children: children.sorted(by: { $0.title < $1.title })))
]
}
let blankSettingsCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, BlankSetting> { cell, indexPath, itemIdentifier in
cell.contentConfiguration = UIListContentConfiguration.cell()
}
let headerCellRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
var contentConfiguration = UIListContentConfiguration.extraProminentInsetGroupedHeader()
contentConfiguration.text = self.snapshot.sectionIdentifiers[indexPath.section].header.text
contentConfiguration.secondaryText = self.snapshot.sectionIdentifiers[indexPath.section].header.secondaryText
contentConfiguration.secondaryTextProperties.color = .secondaryLabel
supplementaryView.contentConfiguration = contentConfiguration
}
let boolCell: UICollectionView.CellRegistration<UICollectionViewListCell, BoolSetting> = CellManager.Settings.boolCell
let inputNumberCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputNumberSetting> = CellManager.Settings.inputNumberCell
let inputStringCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputStringSetting> = CellManager.Settings.inputStringCell
let stepperCell: UICollectionView.CellRegistration<UICollectionViewListCell, StepperSetting> = CellManager.Settings.stepperCell
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case let blankSetting as BlankSetting:
collectionView.dequeueConfiguredReusableCell(using: blankSettingsCellRegistration, for: indexPath, item: blankSetting)
case let boolSetting as BoolSetting:
collectionView.dequeueConfiguredReusableCell(using: boolCell, for: indexPath, item: boolSetting)
case let inputNumberSetting as InputNumberSetting:
collectionView.dequeueConfiguredReusableCell(using: inputNumberCell, for: indexPath, item: inputNumberSetting)
case let inputStringSetting as InputStringSetting:
collectionView.dequeueConfiguredReusableCell(using: inputStringCell, for: indexPath, item: inputStringSetting)
case let stepperSetting as StepperSetting:
collectionView.dequeueConfiguredReusableCell(using: stepperCell, for: indexPath, item: stepperSetting)
case let selectionSetting as SelectionSetting:
collectionView.dequeueConfiguredReusableCell(using: selectionCellRegistration, for: indexPath, item: selectionSetting)
default:
nil
}
}
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
collectionView.dequeueConfiguredReusableSupplementary(using: headerCellRegistration, for: indexPath)
}
snapshot = .init()
snapshot.appendSections(GrapeSettingsHeaders.allCases)
snapshot.sectionIdentifiers.forEach { header in
snapshot.appendItems(GrapeSettingsItems.settings(header).map { $0.setting(self) }, toSection: header)
}
Task { await dataSource.apply(snapshot) }
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
switch dataSource.itemIdentifier(for: indexPath) {
case let inputSetting as InputNumberSetting:
let alertController = UIAlertController(title: inputSetting.title,
message: "Min: \(Int(inputSetting.min)), Max: \(Int(inputSetting.max))",
preferredStyle: .alert)
alertController.addTextField {
$0.keyboardType = .numberPad
}
alertController.addAction(.init(title: "Cancel", style: .cancel))
alertController.addAction(.init(title: "Save", style: .default, handler: { _ in
guard let textFields = alertController.textFields, let textField = textFields.first, let value = textField.text as? NSString else {
return
}
UserDefaults.standard.set(value.doubleValue, forKey: inputSetting.key)
if let delegate = inputSetting.delegate {
delegate.didChangeSetting(at: indexPath)
}
}))
present(alertController, animated: true)
case let inputSetting as InputStringSetting:
let alertController = UIAlertController(title: inputSetting.title,
message: inputSetting.details,
preferredStyle: .alert)
alertController.addTextField {
$0.placeholder = inputSetting.placeholder
}
alertController.addAction(.init(title: "Cancel", style: .cancel))
alertController.addAction(.init(title: "Save", style: .default, handler: { _ in
guard let textFields = alertController.textFields, let textField = textFields.first, let value = textField.text else {
return
}
UserDefaults.standard.set(value, forKey: inputSetting.key)
if let delegate = inputSetting.delegate {
inputSetting.action()
delegate.didChangeSetting(at: indexPath)
}
}))
present(alertController, animated: true)
default:
break
}
}
}
extension GrapeSettingsController : SettingDelegate {
func didChangeSetting(at indexPath: IndexPath) {
// grape.updateSettings()
guard let sectionIdentifier = dataSource.sectionIdentifier(for: indexPath.section) else {
return
}
var snapshot = dataSource.snapshot()
let item = snapshot.itemIdentifiers(inSection: sectionIdentifier)[indexPath.item]
switch item {
case let boolSetting as BoolSetting:
boolSetting.value = UserDefaults.standard.bool(forKey: boolSetting.key)
case let inputNumberSetting as InputNumberSetting:
inputNumberSetting.value = UserDefaults.standard.double(forKey: inputNumberSetting.key)
case let inputStringSetting as InputStringSetting:
inputStringSetting.value = UserDefaults.standard.string(forKey: inputStringSetting.key)
case let stepperSetting as StepperSetting:
stepperSetting.value = UserDefaults.standard.double(forKey: stepperSetting.key)
case let selectionSetting as SelectionSetting:
selectionSetting.selectedValue = UserDefaults.standard.value(forKey: selectionSetting.key)
default:
break
}
snapshot.reloadItems([item])
Task {
await dataSource.apply(snapshot, animatingDifferences: false)
}
}
}