diff --git a/SwiftLeeds.xcodeproj/project.pbxproj b/SwiftLeeds.xcodeproj/project.pbxproj index f2bd57c..e4f2b05 100644 --- a/SwiftLeeds.xcodeproj/project.pbxproj +++ b/SwiftLeeds.xcodeproj/project.pbxproj @@ -120,8 +120,16 @@ AEDC22532898281300746247 /* MyConferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDC22522898281300746247 /* MyConferenceViewModel.swift */; }; AEDC22552898288F00746247 /* Schedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDC22542898288F00746247 /* Schedule.swift */; }; AEDC2257289C65D500746247 /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDC2256289C65D500746247 /* Calendar.swift */; }; - E3569AEE2E5A1D0200BC9556 /* ShimmerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AED2E5A1D0200BC9556 /* ShimmerView.swift */; }; E3569AEF2E5A1D0200BC9556 /* ShimmerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AED2E5A1D0200BC9556 /* ShimmerView.swift */; }; + E3569AF42E5A2F1D00BC9556 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AF12E5A2F1D00BC9556 /* SettingsView.swift */; }; + E3569AF52E5A2F1D00BC9556 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AF22E5A2F1D00BC9556 /* SettingsViewModel.swift */; }; + E3569AF92E5A301D00BC9556 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AF82E5A301D00BC9556 /* ThemeManager.swift */; }; + E3569B002E5A55D000BC9556 /* UserDefaultsKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AFF2E5A55D000BC9556 /* UserDefaultsKeys.swift */; }; + E3569B052E5B902B00BC9556 /* UserDefaultsKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AFF2E5A55D000BC9556 /* UserDefaultsKeys.swift */; }; + E3569B062E5B903800BC9556 /* ShimmerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AED2E5A1D0200BC9556 /* ShimmerView.swift */; }; + E3569B072E5B904400BC9556 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AF82E5A301D00BC9556 /* ThemeManager.swift */; }; + E3569B082E5B905100BC9556 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AF12E5A2F1D00BC9556 /* SettingsView.swift */; }; + E3569B092E5B905700BC9556 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3569AF22E5A2F1D00BC9556 /* SettingsViewModel.swift */; }; FA1F7EF7287CB71600E12F8C /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1F7EF6287CB71600E12F8C /* HeaderView.swift */; }; FA534D8228A1909300A3BFBB /* Local.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA534D8128A1909300A3BFBB /* Local.swift */; }; FA534D8828A1939500A3BFBB /* LocalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA534D8728A1939500A3BFBB /* LocalViewModel.swift */; }; @@ -264,6 +272,10 @@ AEDC22542898288F00746247 /* Schedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Schedule.swift; sourceTree = ""; }; AEDC2256289C65D500746247 /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; E3569AED2E5A1D0200BC9556 /* ShimmerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerView.swift; sourceTree = ""; }; + E3569AF12E5A2F1D00BC9556 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + E3569AF22E5A2F1D00BC9556 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + E3569AF82E5A301D00BC9556 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; + E3569AFF2E5A55D000BC9556 /* UserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsKeys.swift; sourceTree = ""; }; FA1F7EF6287CB71600E12F8C /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; FA534D8128A1909300A3BFBB /* Local.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Local.swift; sourceTree = ""; }; FA534D8728A1939500A3BFBB /* LocalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalViewModel.swift; sourceTree = ""; }; @@ -487,6 +499,7 @@ AEDC0DAF28675D060078A153 /* My Conference */, 0B4B1A492A4858DC00ED7EA9 /* Sponsors */, AEDC0DAC286759570078A153 /* Tab */, + E3569AF32E5A2F1D00BC9556 /* Settings */, ); path = Views; sourceTree = ""; @@ -498,6 +511,8 @@ AECB29832741ABFD00CDC983 /* Info.plist */, AECB295627417F9D00CDC983 /* SwiftLeedsApp.swift */, 740162D92A7053A000C2D1B3 /* AppState.swift */, + E3569AF82E5A301D00BC9556 /* ThemeManager.swift */, + E3569AFF2E5A55D000BC9556 /* UserDefaultsKeys.swift */, ); path = App; sourceTree = ""; @@ -546,6 +561,15 @@ path = "My Conference"; sourceTree = ""; }; + E3569AF32E5A2F1D00BC9556 /* Settings */ = { + isa = PBXGroup; + children = ( + E3569AF12E5A2F1D00BC9556 /* SettingsView.swift */, + E3569AF22E5A2F1D00BC9556 /* SettingsViewModel.swift */, + ); + path = Settings; + sourceTree = ""; + }; FA57DE412875B03800911F03 /* Common */ = { isa = PBXGroup; children = ( @@ -830,6 +854,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E3569AF42E5A2F1D00BC9556 /* SettingsView.swift in Sources */, + E3569AF52E5A2F1D00BC9556 /* SettingsViewModel.swift in Sources */, 0B4CB3E028EAF58A00246E62 /* Schedule.swift in Sources */, 0B4CB3EE28EAF5FB00246E62 /* String.swift in Sources */, 0B4CB3DF28EAF58400246E62 /* Constants.swift in Sources */, @@ -840,6 +866,7 @@ 0B4CB3ED28EAF5F200246E62 /* SwiftLeedsContainer.swift in Sources */, 0B4CB3EC28EAF5E900246E62 /* ActivityView.swift in Sources */, 0B4CB3F228EAF61600246E62 /* WebView.swift in Sources */, + E3569B052E5B902B00BC9556 /* UserDefaultsKeys.swift in Sources */, AE1CDBF02AC05B2B00E83420 /* HttpMethod.swift in Sources */, 0B4B1A512A48FB6400ED7EA9 /* SponsorsViewModel.swift in Sources */, 0B4CB3EA28EAF5D900246E62 /* Color.swift in Sources */, @@ -853,10 +880,10 @@ 0B4CB3E928EAF5D200246E62 /* Local.swift in Sources */, 0B4CB3E228EAF59900246E62 /* Calendar.swift in Sources */, 0B910A352A48FEC100648B32 /* SponsorTileView.swift in Sources */, + E3569B062E5B903800BC9556 /* ShimmerView.swift in Sources */, AE1CDBED2AC05B2B00E83420 /* Request.swift in Sources */, AE9367972A9354CC00F2DB3F /* Helper.swift in Sources */, 0B4CB3E528EAF5B700246E62 /* Speaker.swift in Sources */, - E3569AEE2E5A1D0200BC9556 /* ShimmerView.swift in Sources */, 0B4CB3E828EAF5CD00246E62 /* AnnouncementCell.swift in Sources */, 0B4CB3DD28EAF57900246E62 /* MyConferenceView.swift in Sources */, 0B4CB3CA28EAF19100246E62 /* ContentView.swift in Sources */, @@ -868,6 +895,7 @@ 0B4CB3F428EAF62100246E62 /* CommonTileView.swift in Sources */, 0B4CB3C828EAF19100246E62 /* SwiftLeedsAppClipApp.swift in Sources */, 0B4CB3F328EAF61B00246E62 /* CommonTileButton.swift in Sources */, + E3569B072E5B904400BC9556 /* ThemeManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -901,6 +929,7 @@ AED26F7828676A9900E06064 /* AboutView.swift in Sources */, FA57DE502875B09900911F03 /* SponsorTileView.swift in Sources */, 394653AB288BB7C800212E1C /* SpeakerView.swift in Sources */, + E3569B002E5A55D000BC9556 /* UserDefaultsKeys.swift in Sources */, FA57DE4D2875B08600911F03 /* LinearGradient.swift in Sources */, 394653A9288BB47A00212E1C /* SectionHeader.swift in Sources */, FA57DE552875B0C100911F03 /* SquishyButtonStyle.swift in Sources */, @@ -917,13 +946,16 @@ AE8C1B2428BFCFC700AF7318 /* Presentation.swift in Sources */, AE1CDBE92AC058C300E83420 /* URLSession.swift in Sources */, 2A3831122884A96600030002 /* FancyHeaderView.swift in Sources */, + E3569B082E5B905100BC9556 /* SettingsView.swift in Sources */, FA57DE4B2875B06B00911F03 /* SwiftLeedsContainer.swift in Sources */, 74F5EF892A49CECB008D9413 /* SidebarView.swift in Sources */, FAAB819128844EBC001159BB /* View+MeasureSize.swift in Sources */, + E3569AF92E5A301D00BC9556 /* ThemeManager.swift in Sources */, AECB298227418DA200CDC983 /* Color.swift in Sources */, AE1CDBE12AC0586500E83420 /* HttpMethod.swift in Sources */, FA534D8828A1939500A3BFBB /* LocalViewModel.swift in Sources */, AE5EFD75289E7CBF00464FE1 /* ActivityView.swift in Sources */, + E3569B092E5B905700BC9556 /* SettingsViewModel.swift in Sources */, AED26F7028675DA000E06064 /* AnnouncementCell.swift in Sources */, AEB06BD128CF8D2100E51967 /* WebView.swift in Sources */, AE8C1B2828C4B36B00AF7318 /* AppDelegate+Push.swift in Sources */, @@ -997,6 +1029,7 @@ 0B4CB3D628EAF19100246E62 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-Space AppIcon-Olympics"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -1035,6 +1068,7 @@ 0B4CB3D728EAF19100246E62 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-Space AppIcon-Olympics"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -1252,6 +1286,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-Space AppIcon-Olympics"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SwiftLeeds/SwiftLeeds.entitlements; @@ -1288,6 +1323,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-Space AppIcon-Olympics"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SwiftLeeds/SwiftLeeds.entitlements; diff --git a/SwiftLeeds/App/AppState.swift b/SwiftLeeds/App/AppState.swift index 9dada62..f453e6c 100644 --- a/SwiftLeeds/App/AppState.swift +++ b/SwiftLeeds/App/AppState.swift @@ -7,7 +7,7 @@ import Foundation enum TabItems: Int { - case conference, location, about, sponsors + case conference, location, about, sponsors, settings } final class AppState: ObservableObject { diff --git a/SwiftLeeds/App/SwiftLeedsApp.swift b/SwiftLeeds/App/SwiftLeedsApp.swift index 58becd1..25e7da9 100644 --- a/SwiftLeeds/App/SwiftLeedsApp.swift +++ b/SwiftLeeds/App/SwiftLeedsApp.swift @@ -12,11 +12,13 @@ struct SwiftLeedsApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @StateObject private var appState = AppState() + @StateObject private var themeManager = ThemeManager.shared var body: some Scene { WindowGroup { Tabs() .environmentObject(appState) + .environmentObject(themeManager) .onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: handleUserActivity) } } diff --git a/SwiftLeeds/App/ThemeManager.swift b/SwiftLeeds/App/ThemeManager.swift new file mode 100644 index 0000000..e7a1c81 --- /dev/null +++ b/SwiftLeeds/App/ThemeManager.swift @@ -0,0 +1,87 @@ +// +// ThemeManager.swift +// SwiftLeeds +// +// Created by Adam Rush on 23/08/2025. +// + +import SwiftUI +import UIKit + +/// Available theme options for the application +enum ThemeOption: String, CaseIterable { + case system = "system" + case light = "light" + case dark = "dark" + + /// User-friendly display name for the theme option + var displayName: String { + switch self { + case .system: return "System" + case .light: return "Light" + case .dark: return "Dark" + } + } + + /// Corresponding UIUserInterfaceStyle for the theme + var userInterfaceStyle: UIUserInterfaceStyle { + switch self { + case .light: return .light + case .dark: return .dark + case .system: return .unspecified + } + } +} + +/// Manages the application's theme settings and appearance +final class ThemeManager: ObservableObject { + /// Shared singleton instance + static let shared = ThemeManager() + + /// Currently selected theme, automatically synced with UserDefaults + @Published var currentTheme: ThemeOption = .system + + private init() { + loadTheme() + applyTheme(currentTheme) + } + + /// Updates the application theme and persists the selection + /// - Parameter theme: The new theme to apply + func setTheme(_ theme: ThemeOption) { + currentTheme = theme + UserDefaults.standard.set(theme.rawValue, forKey: UserDefaultsKeys.selectedTheme) + applyTheme(theme) + } + + /// Loads the saved theme preference from UserDefaults + private func loadTheme() { + if let savedTheme = UserDefaults.standard.string(forKey: UserDefaultsKeys.selectedTheme), + let theme = ThemeOption(rawValue: savedTheme) { + currentTheme = theme + } + } + + /// Applies the specified theme to the application UI + /// - Parameter theme: The theme to apply + private func applyTheme(_ theme: ThemeOption) { + DispatchQueue.main.async { + self.updateUserInterfaceStyle(theme.userInterfaceStyle) + } + } + + /// Updates the user interface style for all windows in the current scene + /// - Parameter style: The UIUserInterfaceStyle to apply + private func updateUserInterfaceStyle(_ style: UIUserInterfaceStyle) { + guard let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first else { + print("Warning: Unable to find window scene for theme application") + return + } + + windowScene.windows.forEach { window in + window.overrideUserInterfaceStyle = style + } + } +} diff --git a/SwiftLeeds/App/UserDefaultsKeys.swift b/SwiftLeeds/App/UserDefaultsKeys.swift new file mode 100644 index 0000000..b86c27d --- /dev/null +++ b/SwiftLeeds/App/UserDefaultsKeys.swift @@ -0,0 +1,13 @@ +// +// UserDefaultsKeys.swift +// SwiftLeeds +// +// Created by Adam Rush on 23/08/2025. +// + +import Foundation + +enum UserDefaultsKeys { + static let selectedTheme = "selectedTheme" + static let selectedAppIcon = "selectedAppIcon" +} \ No newline at end of file diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/100.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/100.png new file mode 100644 index 0000000..82a7944 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/100.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/1024.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/1024.png new file mode 100644 index 0000000..06790e5 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/1024.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/114.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/114.png new file mode 100644 index 0000000..9457e97 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/114.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/120.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/120.png new file mode 100644 index 0000000..c30f906 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/120.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/144.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/144.png new file mode 100644 index 0000000..8316645 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/144.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/152.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/152.png new file mode 100644 index 0000000..5ec6185 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/152.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/167.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/167.png new file mode 100644 index 0000000..b95d22e Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/167.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/180.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/180.png new file mode 100644 index 0000000..470dcc1 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/180.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/20.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/20.png new file mode 100644 index 0000000..a284052 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/20.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/29.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/29.png new file mode 100644 index 0000000..fd17ec4 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/29.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/40.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/40.png new file mode 100644 index 0000000..840b0ef Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/40.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/50.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/50.png new file mode 100644 index 0000000..c6ce138 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/50.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/57.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/57.png new file mode 100644 index 0000000..3ed69ae Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/57.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/58.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/58.png new file mode 100644 index 0000000..7e78692 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/58.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/60.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/60.png new file mode 100644 index 0000000..5eabe33 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/60.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/72.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/72.png new file mode 100644 index 0000000..5fafacc Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/72.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/76.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/76.png new file mode 100644 index 0000000..87bb935 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/76.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/80.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/80.png new file mode 100644 index 0000000..a4c4205 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/80.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/87.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/87.png new file mode 100644 index 0000000..a1da442 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/87.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/Contents.json b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/Contents.json new file mode 100644 index 0000000..4fdf882 --- /dev/null +++ b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Olympics.appiconset/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/1024 1.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/1024 1.png new file mode 100644 index 0000000..98ce66a Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/1024 1.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/114.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/114.png new file mode 100644 index 0000000..2d0f15c Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/114.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/120 1.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/120 1.png new file mode 100644 index 0000000..c2285fb Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/120 1.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/120.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/120.png new file mode 100644 index 0000000..c2285fb Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/120.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/152.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/152.png new file mode 100644 index 0000000..a1cc0bd Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/152.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/167.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/167.png new file mode 100644 index 0000000..bfe2a68 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/167.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/180.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/180.png new file mode 100644 index 0000000..0ebfafa Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/180.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/40.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/40.png new file mode 100644 index 0000000..a8d08c2 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/40.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/58 1.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/58 1.png new file mode 100644 index 0000000..ba82b3e Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/58 1.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/60.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/60.png new file mode 100644 index 0000000..efbf016 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/60.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/76.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/76.png new file mode 100644 index 0000000..3551dba Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/76.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/80 1.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/80 1.png new file mode 100644 index 0000000..4b00950 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/80 1.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/87 1.png b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/87 1.png new file mode 100644 index 0000000..b62f64f Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/87 1.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/Contents.json b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/Contents.json new file mode 100644 index 0000000..427e1ba --- /dev/null +++ b/SwiftLeeds/Resources/Assets.xcassets/AppIcon-Space.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "58 1.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87 1.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "76.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "filename" : "114.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, + { + "filename" : "80 1.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "120 1.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "filename" : "152.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-2024.imageset/AppIcon 1.png b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-2024.imageset/AppIcon 1.png new file mode 100644 index 0000000..55700fc Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-2024.imageset/AppIcon 1.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-2024.imageset/Contents.json b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-2024.imageset/Contents.json new file mode 100644 index 0000000..345310e --- /dev/null +++ b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-2024.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AppIcon 1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Olympics.imageset/Contents.json b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Olympics.imageset/Contents.json new file mode 100644 index 0000000..8c67536 --- /dev/null +++ b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Olympics.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 110.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Olympics.imageset/Frame 110.png b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Olympics.imageset/Frame 110.png new file mode 100644 index 0000000..b5646c3 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Olympics.imageset/Frame 110.png differ diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Space.imageset/Contents.json b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Space.imageset/Contents.json new file mode 100644 index 0000000..c8b656c --- /dev/null +++ b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Space.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 139.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Space.imageset/Frame 139.png b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Space.imageset/Frame 139.png new file mode 100644 index 0000000..c636a87 Binary files /dev/null and b/SwiftLeeds/Resources/Assets.xcassets/AppIconPreview-Space.imageset/Frame 139.png differ diff --git a/SwiftLeeds/Views/Settings/SettingsView.swift b/SwiftLeeds/Views/Settings/SettingsView.swift new file mode 100644 index 0000000..4700ba8 --- /dev/null +++ b/SwiftLeeds/Views/Settings/SettingsView.swift @@ -0,0 +1,148 @@ +// +// SettingsView.swift +// SwiftLeeds +// +// Created by Adam Rush on 23/08/2025. +// + +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject var themeManager: ThemeManager + @StateObject private var viewModel = SettingsViewModel() + + var body: some View { + NavigationView { + List { + Section("App Icon") { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) { + ForEach(AppIconOption.allCases, id: \.self) { iconOption in + AppIconButton( + iconOption: iconOption, + isSelected: viewModel.currentIcon == iconOption, + action: { + viewModel.changeAppIcon(to: iconOption) + } + ) + } + } + .padding(.vertical, 8) + } + + Section("Appearance") { + Picker("Theme", selection: $themeManager.currentTheme) { + ForEach(ThemeOption.allCases, id: \.self) { theme in + Text(theme.displayName).tag(theme) + } + } + .onChange(of: themeManager.currentTheme) { newTheme in + themeManager.setTheme(newTheme) + } + } + + Section("About") { + HStack { + Text("Version") + Spacer() + Text(viewModel.appVersion) + .foregroundColor(.secondary) + } + + Button("Contact Us") { + viewModel.openContactUs() + } + + Button("Code of Conduct") { + viewModel.openCodeOfConduct() + } + } + } + .navigationTitle("Settings") + .alert("Icon Change Failed", isPresented: $viewModel.showingIconError) { + Button("OK") { } + } message: { + Text("Unable to change app icon. Please try again.") + } + } + } +} + +struct AppIconButton: View { + let iconOption: AppIconOption + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 8) { + if let uiImage = iconOption.iconImage { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 60, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 3) + ) + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.3)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "questionmark.app") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.gray) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 3) + ) + } + + Text(iconOption.displayName) + .font(.caption) + .multilineTextAlignment(.center) + .foregroundColor(isSelected ? .accentColor : .primary) + } + } + .buttonStyle(PlainButtonStyle()) + } +} + +enum AppIconOption: String, CaseIterable { + case generic = "AppIcon" + case space = "AppIcon-Space" + case olympics = "AppIcon-Olympics" + + var displayName: String { + switch self { + case .generic: return "General" + case .space: return "Space" + case .olympics: return "Sports" + } + } + + var iconImage: UIImage? { + switch self { + case .generic: + return UIImage(named: "AppIconPreview-2024") + case .space: + return UIImage(named: "AppIconPreview-Space") + case .olympics: + return UIImage(named: "AppIconPreview-Olympics") + } + } + + var iconName: String? { + return self == .generic ? nil : rawValue + } +} + + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView() + .environmentObject(ThemeManager.shared) + } +} diff --git a/SwiftLeeds/Views/Settings/SettingsViewModel.swift b/SwiftLeeds/Views/Settings/SettingsViewModel.swift new file mode 100644 index 0000000..e899363 --- /dev/null +++ b/SwiftLeeds/Views/Settings/SettingsViewModel.swift @@ -0,0 +1,60 @@ +// +// SettingsViewModel.swift +// SwiftLeeds +// +// Created by Adam Rush on 23/08/2025. +// + +import SwiftUI +import UIKit + +final class SettingsViewModel: ObservableObject { + @Published var currentIcon: AppIconOption = .generic + @Published var showingIconError = false + + var appVersion: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" + } + + init() { + loadCurrentIcon() + } + + func changeAppIcon(to iconOption: AppIconOption) { + guard UIApplication.shared.supportsAlternateIcons else { + showingIconError = true + return + } + + UIApplication.shared.setAlternateIconName(iconOption.iconName) { [weak self] error in + DispatchQueue.main.async { + if error != nil { + self?.showingIconError = true + } else { + self?.currentIcon = iconOption + UserDefaults.standard.set(iconOption.rawValue, forKey: UserDefaultsKeys.selectedAppIcon) + } + } + } + } + + func openContactUs() { + if let url = URL(string: "mailto:info@swiftleeds.co.uk") { + UIApplication.shared.open(url) + } + } + + func openCodeOfConduct() { + if let url = URL(string: "https://swiftleeds.co.uk/conduct") { + UIApplication.shared.open(url) + } + } + + private func loadCurrentIcon() { + if let savedIcon = UserDefaults.standard.string(forKey: UserDefaultsKeys.selectedAppIcon), + let iconOption = AppIconOption(rawValue: savedIcon) { + currentIcon = iconOption + } + } + +} diff --git a/SwiftLeeds/Views/Tab/SidebarMainView.swift b/SwiftLeeds/Views/Tab/SidebarMainView.swift index 0ddbd62..167b13b 100644 --- a/SwiftLeeds/Views/Tab/SidebarMainView.swift +++ b/SwiftLeeds/Views/Tab/SidebarMainView.swift @@ -23,6 +23,8 @@ struct SidebarMainView: View { LocalView() case .sponsors: SponsorsView() + case .settings: + SettingsView() } } diff --git a/SwiftLeeds/Views/Tab/SidebarView.swift b/SwiftLeeds/Views/Tab/SidebarView.swift index 133a721..7c20734 100644 --- a/SwiftLeeds/Views/Tab/SidebarView.swift +++ b/SwiftLeeds/Views/Tab/SidebarView.swift @@ -15,7 +15,7 @@ struct SidebarView: View { NavigationLink(destination: MyConferenceView().onAppear { appState.selectedTab = .conference }) { - Label("My Conference", systemImage: "person.fill") + Label("Schedule", systemImage: "person.fill") } NavigationLink(destination: LocalView().onAppear { appState.selectedTab = .location diff --git a/SwiftLeeds/Views/Tab/TabsMainView.swift b/SwiftLeeds/Views/Tab/TabsMainView.swift index 9e86237..1951b9d 100644 --- a/SwiftLeeds/Views/Tab/TabsMainView.swift +++ b/SwiftLeeds/Views/Tab/TabsMainView.swift @@ -15,7 +15,7 @@ struct TabsMainView: View { TabView(selection: $appState.selectedTab) { MyConferenceView() .tabItem { - Label("My Conference", systemImage: "person.fill") + Label("Schedule", systemImage: "person.fill") } .tag(TabItems.conference) @@ -35,6 +35,12 @@ struct TabsMainView: View { Label("Sponsors", systemImage: "sparkles") } .tag(TabItems.sponsors) + + SettingsView() + .tabItem { + Label("Settings", systemImage: "gearshape.fill") + } + .tag(TabItems.settings) } } }