1
0
forked from MeloNX/MeloNX

Navigation palette + iOS 26 glass effects

This commit is contained in:
Daniil Vinogradov 2025-07-14 16:56:32 +02:00
parent 273a31dc14
commit 89ad9b6457
8 changed files with 338 additions and 151 deletions

View File

@ -28,6 +28,7 @@
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; }; 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
7CD054B52E253D2F00287A89 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */; };
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -196,6 +197,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7CD054B52E253D2F00287A89 /* SwiftUIIntrospect in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */, 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
@ -295,6 +297,7 @@
packageProductDependencies = ( packageProductDependencies = (
4EA5AE812D16807500AD0B9F /* SwiftSVG */, 4EA5AE812D16807500AD0B9F /* SwiftSVG */,
4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */, 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */,
7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */,
); );
productName = MeloNX; productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */; productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@ -387,6 +390,7 @@
packageReferences = ( packageReferences = (
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */, 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */,
7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
); );
preferredProjectObjectVersion = 56; preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */; productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@ -642,7 +646,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = D59DHVRS87;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO; ENABLE_TESTABILITY = NO;
@ -782,6 +786,9 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1041,9 +1048,15 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.xitrix.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
@ -1066,7 +1079,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = D59DHVRS87;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -1206,6 +1219,9 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1465,9 +1481,15 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.xitrix.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
@ -1682,6 +1704,14 @@
kind = branch; kind = branch;
}; };
}; };
7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/siteline/swiftui-introspect.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -1695,6 +1725,11 @@
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;
productName = SwiftSVG; productName = SwiftSVG;
}; };
7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */ = {
isa = XCSwiftPackageProductDependency;
package = 7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */;
productName = SwiftUIIntrospect;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 4E80A9852CD6F54500029585 /* Project object */; rootObject = 4E80A9852CD6F54500029585 /* Project object */;

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390", "originHash" : "ae170c69ea1ccacb2f3982b1a91b689a09bc4dcc59ed970f1df977bf7c7aed6f",
"pins" : [ "pins" : [
{ {
"identity" : "cocoaasyncsocket", "identity" : "cocoaasyncsocket",
@ -18,6 +18,15 @@
"branch" : "master", "branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d" "revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
} }
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/swiftui-introspect.git",
"state" : {
"revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336",
"version" : "1.3.0"
}
} }
], ],
"version" : 3 "version" : 3

View File

@ -0,0 +1,83 @@
//
// NavigationItemPalette.swift
// iTorrent
//
// Created by Daniil Vinogradov on 14.11.2024.
//
import UIKit
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
public extension View {
func navigationItemBottomPalette(@ViewBuilder body: () -> (some View)) -> some View {
modifier(NavitaionItemBottomPaletteContent(content: body().asController))
}
}
struct NavitaionItemBottomPaletteContent: ViewModifier {
let content: UIViewController
func body(content: Content) -> some View {
content
.introspect(.viewController, on: .iOS(.v14...), customize: { viewController in
let view = self.content.view!
view.backgroundColor = .clear
let size = view.systemLayoutSizeFitting(.init(width: viewController.view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
viewController.navigationItem.setBottomPalette(view, height: size.height)
})
}
}
extension UINavigationItem {
func setBottomPalette(_ contentView: UIView?, height: CGFloat = 44) {
/// "_setBottomPalette:"
let selector = NSSelectorFromBase64String("X3NldEJvdHRvbVBhbGV0dGU6")
guard responds(to: selector) else { return }
perform(selector, with: Self.makeNavigationItemPalette(with: contentView, height: height))
}
private static func makeNavigationItemPalette(with contentView: UIView?, height: CGFloat) -> UIView? {
guard let contentView else { return nil }
contentView.translatesAutoresizingMaskIntoConstraints = false
let contentViewHolder = UIView(frame: .init(x: 0, y: 0, width: 0, height: height))
contentViewHolder.autoresizingMask = [.flexibleHeight]
contentViewHolder.addSubview(contentView)
NSLayoutConstraint.activate([
contentViewHolder.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
contentViewHolder.topAnchor.constraint(equalTo: contentView.topAnchor),
contentView.trailingAnchor.constraint(equalTo: contentViewHolder.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentViewHolder.bottomAnchor),
])
/// "_UINavigationBarPalette"
guard let paletteClass = NSClassFromBase64String("X1VJTmF2aWdhdGlvbkJhclBhbGV0dGU=") as? UIView.Type
else { return nil }
/// "alloc"
/// "initWithContentView:"
guard let palette = paletteClass.perform(NSSelectorFromBase64String("YWxsb2M="))
.takeUnretainedValue()
.perform(NSSelectorFromBase64String("aW5pdFdpdGhDb250ZW50Vmlldzo="), with: contentViewHolder)
.takeUnretainedValue() as? UIView
else { return nil }
palette.preservesSuperviewLayoutMargins = true
return palette
}
}
func NSSelectorFromBase64String(_ base64String: String) -> Selector {
NSSelectorFromString(String(base64: base64String))
}
func NSClassFromBase64String(_ aBase64ClassName: String) -> AnyClass? {
NSClassFromString(String(base64: aBase64ClassName))
}
extension String {
init(base64: String) {
self.init(data: Data(base64Encoded: base64)!, encoding: .utf8)!
}
}

View File

@ -0,0 +1,36 @@
//
// UIKitSwiftUIInarop.swift
// iTorrent
//
// Created by Daniil Vinogradov on 01/11/2023.
//
import SwiftUI
private struct GenericControllerView: UIViewControllerRepresentable {
let viewController: UIViewController
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { /* Ignore */ }
}
extension View {
@MainActor
var asController: UIHostingController<Self> {
let vc = UIHostingController<Self>(rootView: self)
if #available(iOS 16.4, *) {
vc.safeAreaRegions = []
}
return vc
}
}
extension UIViewController {
var asView: some View {
GenericControllerView(viewController: self)
}
}

View File

@ -96,9 +96,15 @@ struct GameInfoSheet: View {
.navigationTitle(game.titleName) .navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .topBarTrailing) {
Button("Dismiss") { if #available(iOS 26, *) {
presentationMode.wrappedValue.dismiss() Button(role: .close) {
presentationMode.wrappedValue.dismiss()
}
} else {
Button("Dismiss") {
presentationMode.wrappedValue.dismiss()
}
} }
} }
} }

View File

@ -78,28 +78,38 @@ struct GameLibraryView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ZStack { Group {
// Background color // Game list
Color(UIColor.systemBackground) if Ryujinx.shared.games.isEmpty {
.ignoresSafeArea() EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile)
} else {
VStack(spacing: 0) { gameListView
// Header with stats .animation(.easeInOut(duration: 0.3), value: searchText)
if !Ryujinx.shared.games.isEmpty { }
GameLibraryHeader( }
totalGames: Ryujinx.shared.games.count, .navigationItemBottomPalette {
recentGames: realRecentGames.count, if !Ryujinx.shared.games.isEmpty {
firmwareVersion: firmwareversion GameLibraryHeader(
) totalGames: Ryujinx.shared.games.count,
} recentGames: realRecentGames.count,
firmwareVersion: firmwareversion
// Game list )
if Ryujinx.shared.games.isEmpty { .overlay(Group {
EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile) if ryujinx.jitenabled {
} else { VStack {
gameListView HStack {
.animation(.easeInOut(duration: 0.3), value: searchText) Spacer()
} Circle()
.frame(width: 12, height: 12)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.foregroundColor(Color.green)
.padding()
}
Spacer()
}
}
})
} }
} }
.navigationTitle("Game Library") .navigationTitle("Game Library")
@ -164,27 +174,11 @@ struct GameLibraryView: View {
} }
} }
} }
.overlay(Group {
if ryujinx.jitenabled {
VStack {
HStack {
Spacer()
Circle()
.frame(width: 12, height: 12)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.foregroundColor(Color.green)
.padding()
}
Spacer()
}
}
})
.onChange(of: startemu) { game in .onChange(of: startemu) { game in
guard let game else { return } guard let game else { return }
addToRecentGames(game) addToRecentGames(game)
} }
// .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers") .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers")
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty isSearching = !searchText.isEmpty
} }
@ -654,7 +648,7 @@ struct GameLibraryHeader: View {
// Stats cards // Stats cards
StatCard( StatCard(
icon: "gamecontroller.fill", icon: "gamecontroller.fill",
title: "Total Games", title: "Games",
value: "\(totalGames)", value: "\(totalGames)",
color: .blue color: .blue
) )
@ -674,8 +668,7 @@ struct GameLibraryHeader: View {
) )
} }
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.bottom, 8)
.padding(.bottom, 4)
} }
} }
@ -684,7 +677,9 @@ struct StatCard: View {
let title: String let title: String
let value: String let value: String
let color: Color let color: Color
@Namespace var statCardIdNamespace
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {
@ -700,11 +695,24 @@ struct StatCard: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(10) .padding(10)
.background(color.opacity(0.1)) .apply {
.cornerRadius(10) if #available(iOS 26, *) {
$0
.glassEffect(.regular.tint(color.opacity(0.05)), in: RoundedRectangle(cornerRadius: 16))
.glassEffectID("StatCardID", in: statCardIdNamespace)
} else {
$0
.background(color.opacity(0.1))
.cornerRadius(10)
}
}
} }
} }
extension View {
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}
// MARK: - Game Card View // MARK: - Game Card View
struct GameCardView: View { struct GameCardView: View {
let game: Game let game: Game
@ -881,7 +889,9 @@ struct GameListRow: View {
} }
} }
} }
Spacer()
if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) { if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) {
Image(systemName: "gearshape.circle") Image(systemName: "gearshape.circle")
.resizable() .resizable()
@ -889,8 +899,7 @@ struct GameListRow: View {
.foregroundStyle(.blue) .foregroundStyle(.blue)
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
} }
Spacer()
VStack(alignment: .leading) { VStack(alignment: .leading) {
// Compatibility badges // Compatibility badges

View File

@ -168,50 +168,44 @@ struct PerGameSettingsView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ZStack { // Settings content
Color(UIColor.systemBackground) ScrollView {
.ignoresSafeArea() VStack(spacing: 24) {
switch selectedCategory {
VStack(spacing: 0) { case .graphics:
ScrollView(.horizontal, showsIndicators: false) { graphicsSettings
HStack(spacing: 12) { .padding(.top)
ForEach(PerSettingsCategory.allCases, id: \.id) { category in case .system:
CategoryButton( systemSettings
title: category.rawValue, .padding(.top)
icon: category.icon, case .advanced:
isSelected: selectedCategory == category advancedSettings
) { .padding(.top)
selectedCategory = category
}
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
Divider()
// Settings content
ScrollView {
VStack(spacing: 24) {
switch selectedCategory {
case .graphics:
graphicsSettings
.padding(.top)
case .system:
systemSettings
.padding(.top)
case .advanced:
advancedSettings
.padding(.top)
}
Spacer(minLength: 50)
}
.padding(.bottom)
} }
.scrollDismissesKeyboardIfAvailable()
Spacer(minLength: 50)
}
.padding(.bottom)
}
.background(Color(uiColor: .systemGroupedBackground))
.scrollDismissesKeyboardIfAvailable()
.navigationItemBottomPalette {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(PerSettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.defaultScrollAnchorIsAvailable(.center)
.padding(.horizontal)
.padding(.vertical, 8)
} }
} }
.navigationTitle("Settings") .navigationTitle("Settings")

View File

@ -469,58 +469,32 @@ struct SettingsViewNew: View {
var iOSSettings: some View { var iOSSettings: some View {
iOSNav { iOSNav {
ZStack { // Settings content
// Background color ScrollView {
Color(UIColor.systemBackground) VStack(spacing: 24) {
.ignoresSafeArea() deviceInfoCard
VStack(spacing: 0) {
// Category selector
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(SettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.padding(.horizontal) .padding(.horizontal)
.padding(.vertical, 8) .padding(.top)
switch selectedCategory {
case .graphics:
graphicsSettings
case .input:
inputSettings
case .system:
systemSettings
case .advanced:
advancedSettings
case .misc:
miscSettings
} }
Divider() Spacer(minLength: 50)
// Settings content
ScrollView {
VStack(spacing: 24) {
deviceInfoCard
.padding(.horizontal)
.padding(.top)
switch selectedCategory {
case .graphics:
graphicsSettings
case .input:
inputSettings
case .system:
systemSettings
case .advanced:
advancedSettings
case .misc:
miscSettings
}
Spacer(minLength: 50)
}
.padding(.bottom)
}
.scrollDismissesKeyboardIfAvailable()
} }
.padding(.bottom)
} }
.scrollDismissesKeyboardIfAvailable()
.background(Color(uiColor: .systemGroupedBackground))
.navigationTitle("Settings") .navigationTitle("Settings")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
// .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic)) // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
@ -533,6 +507,25 @@ struct SettingsViewNew: View {
settingsManager.saveSettings() settingsManager.saveSettings()
} }
} }
.navigationItemBottomPalette {
// Category selector
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(SettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.defaultScrollAnchorIsAvailable(.center)
.padding(.horizontal)
.padding(.vertical, 8)
}
}
} }
} }
@ -1517,12 +1510,23 @@ struct CategoryButton: View {
} }
.foregroundColor(isSelected ? .blue : .secondary) .foregroundColor(isSelected ? .blue : .secondary)
.frame(width: 70, height: 56) .frame(width: 70, height: 56)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
)
.animation(.bouncy(duration: 0.3), value: isSelected) .animation(.bouncy(duration: 0.3), value: isSelected)
} }
.apply {
if #available(iOS 26, *) {
if isSelected {
$0.glassEffect(.regular.tint(Color.blue.opacity(0.15)), in: RoundedRectangle(cornerRadius: 16))
} else {
$0
}
} else {
$0.background(
RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
)
}
}
} }
} }
@ -1561,7 +1565,7 @@ struct SettingsCard<Content: View>: View {
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color.white) .fill(Color(.secondarySystemGroupedBackground))
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
) )
.padding(.horizontal) .padding(.horizontal)
@ -1686,3 +1690,14 @@ extension View {
} }
} }
// this code is used to enable the keyboard to be dismissed when scrolling if available on iOS 16+
extension View {
@ViewBuilder
func defaultScrollAnchorIsAvailable(_ anchor: UnitPoint?) -> some View {
if #available(iOS 17.0, *) {
self.defaultScrollAnchor(anchor)
} else {
self
}
}
}