Created
September 21, 2024 12:15
-
-
Save lifeutilityapps/b060d8b4f0dd6a9a0871d0aecb8df61d to your computer and use it in GitHub Desktop.
SwiftUI Color Theme Picker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
enum EStandardColorScheme: String, CaseIterable, Identifiable { | |
case system = "system" | |
case light = "light" | |
case dark = "dark" | |
var id: String { | |
return self.rawValue | |
} | |
var displayShortName: String { | |
var result = "" | |
switch(self) { | |
case .dark: | |
result = "Dark" | |
break | |
case .light: | |
result = "Light" | |
break | |
case .system: | |
if(SCL.isIOSAppOnMac || SCL.isMac) { | |
result = "My MacBook" | |
break | |
} | |
if(SCL.isPad) { | |
result = "My iPad" | |
} | |
if(SCL.isPhone) { | |
result = "My iPhone" | |
} | |
break | |
} | |
return result | |
} | |
var displayName: String { | |
var result = "" | |
switch(self) { | |
case .dark: | |
result = "Dark Theme" | |
break | |
case .light: | |
result = "Light Theme" | |
break | |
case .system: | |
result = "Device Theme" | |
break | |
default: | |
break | |
} | |
return result | |
} | |
var icon: String { | |
switch self { | |
case .system: | |
return SCL.icons.iphoneGen3 | |
case .light: | |
return SCL.icons.sunMax.iconFill() | |
case .dark: | |
return SCL.icons.moonStars.iconFill() | |
} | |
} | |
var textColor: Color { | |
switch self { | |
case .system: | |
return SCL.colors.labelColor | |
case .light: | |
return SCL.colors.black | |
case .dark: | |
return SCL.colors.white | |
} | |
} | |
var imageName: String { | |
var result = "color-scheme-light" | |
switch self { | |
case .system: | |
if(SCL.isIOSAppOnMac) { | |
result = "laptop-1" | |
} else if(SCL.isPad) { | |
result = "ipad-2" | |
} else { | |
result = "iphone-2" | |
} | |
break | |
case .light: | |
result = "color-scheme-light" | |
break | |
case .dark: | |
result = "color-scheme-dark" | |
break | |
} | |
return result | |
} | |
func getScheme() -> ColorScheme? { | |
var result: ColorScheme? = nil | |
switch(self) { | |
case .dark: | |
result = .dark | |
break | |
case .light: | |
result = .light | |
break | |
case .system: | |
result = nil | |
break | |
} | |
return result | |
} | |
func isActive(_ currentScheme: EStandardColorScheme) -> Bool { | |
return self == currentScheme | |
} | |
static func createFromString(_ value: String) -> EStandardColorScheme { | |
var result = EStandardColorScheme.system | |
if let colorScheme = EStandardColorScheme(rawValue: value) { | |
result = colorScheme | |
} | |
return result | |
} | |
static let UD_KEY_PREFFERED_COLOR_SCHEME = "ud-user-color-scheme-preference" | |
} | |
struct StandardColorSchemePicker: View { | |
@Environment(\.colorScheme) private var colorScheme | |
@AppStorage(EStandardColorScheme.UD_KEY_PREFFERED_COLOR_SCHEME) var userColorSchemePreference = EStandardColorScheme.system.rawValue | |
@State private var isSheetOpen = false | |
@State private var isAlertPresented = false | |
func handleOpen() { | |
isSheetOpen = true | |
} | |
func handleClose() { | |
isSheetOpen = false | |
} | |
var scheme: EStandardColorScheme { | |
let result = EStandardColorScheme.createFromString(userColorSchemePreference) | |
return result | |
} | |
var displayValue: String { | |
return scheme.displayShortName | |
} | |
var displayIcon: String { | |
return scheme.icon | |
} | |
func handleSetColorScheme(_ newValue: String) { | |
SCL.util.vibrateDevice(.medium) | |
userColorSchemePreference = newValue | |
let isSystem = newValue == EStandardColorScheme.system.rawValue | |
if(!isSystem) { | |
SCL.util.asyncAfter { | |
handleClose() | |
} | |
} else { | |
isAlertPresented = true | |
} | |
} | |
func isActive(_ newValue: String) -> Bool { | |
return newValue == userColorSchemePreference | |
} | |
var body: some View { | |
Button { | |
handleOpen() | |
} label: { | |
StandardColorChip(text: displayValue, icon: displayIcon, weight: .regular) | |
}.sheet(isPresented: $isSheetOpen) { | |
VStack(alignment: .center, spacing: 0) { | |
StandardSheetTitle(title: "Color Theme", subtitle: "Update your \(Global.Legal.appDisplayName) theme preference", hideClose: true) | |
Spacer() | |
VStack(alignment: .center, spacing: 20) { | |
HStack(spacing: 20) { | |
ColorSchemeButton(scheme: .light, onClick: { | |
handleSetColorScheme(EStandardColorScheme.light.rawValue) | |
}, isActive: EStandardColorScheme.light.isActive(scheme)) | |
ColorSchemeButton(scheme: .dark, onClick: { | |
handleSetColorScheme(EStandardColorScheme.dark.rawValue) | |
}, isActive: EStandardColorScheme.dark.isActive(scheme)) | |
} | |
HStack { | |
StandardAlert(content: { | |
ColorSchemeButton(scheme: .system, onClick: { | |
handleSetColorScheme(EStandardColorScheme.system.rawValue) | |
}, isActive: EStandardColorScheme.system.isActive(scheme)) | |
}, isAlertOpen: $isAlertPresented, title: "Force Close Required", description: "This color theme requires you to force close \(Global.Legal.appDisplayName) to take effect.", variant: .info) | |
} | |
} | |
Spacer() | |
} | |
.scrollIndicators(.never) | |
.presentationDetents(EStandardSheetDetentSize.half.getDetents()) | |
} | |
} | |
struct ColorSchemeButton: View { | |
@Environment(\.colorScheme) private var colorScheme | |
let scheme: EStandardColorScheme | |
let onClick: () -> Void | |
let isActive: Bool | |
var isSystem: Bool { | |
return scheme == .system | |
} | |
var width: CGFloat { | |
var result = 0.0 | |
result = 120 | |
if(isSystem) { | |
result = (120 * 2) + 20 | |
} | |
return result | |
} | |
var height: CGFloat { | |
var result = 0.0 | |
result = 120 | |
if(isSystem) { | |
result = 95 | |
} | |
return result | |
} | |
var imageSize: CGFloat { | |
var result = 0.0 | |
result = 75 | |
if(isSystem) { | |
result = 45 | |
} | |
return result | |
} | |
var backgroundColor: Color { | |
var result = SCL.colors.white | |
if(scheme == .dark) { | |
result = SCL.colors.black | |
} | |
if(scheme == .light) { | |
result = SCL.colors.white | |
if(colorScheme.isDarkMode) { | |
result = SCL.colors.white.opacity(0.15) | |
} | |
} | |
if(scheme == .system) { | |
if(isActive) { | |
if(colorScheme.isDarkMode) { | |
result = SCL.colors.black | |
} else { | |
result = SCL.colors.white | |
} | |
} else { | |
result = SCL.colors.gray.opacity(0.1) | |
} | |
} | |
return result | |
} | |
var textColor: Color { | |
var result = SCL.colors.labelColor | |
if(scheme == .dark && colorScheme.isLightMode) { | |
result = SCL.colors.white | |
} | |
return result | |
} | |
func handleClick() { | |
if(!isActive) { | |
onClick() | |
} | |
} | |
var body: some View { | |
Button { | |
handleClick() | |
} label: { | |
VStack(alignment: .center) { | |
Image(scheme.imageName) | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
.frame(width: imageSize) | |
.padding(.top, isSystem ? 10 : 0) | |
Text(scheme.displayShortName) | |
.font(.headline) | |
.multilineTextAlignment(.center) | |
.foregroundStyle(textColor) | |
.useConditionalModifier { | |
if(isSystem) { | |
$0 | |
} else { | |
$0 | |
.padding(.bottom) | |
} | |
} | |
if(isSystem) { | |
StandardColorChip(text: "Sync with Device", icon: SCL.icons.sync, variant: .small, weight: .regular) | |
.padding(.bottom) | |
} | |
} | |
.frame(minWidth: width, minHeight: height) | |
.background( | |
RoundedRectangle(cornerRadius: 10) | |
.fill(backgroundColor) | |
.useConditionalModifier { | |
if(isActive) { | |
$0 | |
.shadow(color: SCL.colors.BrandColor.greenDeep, radius: 1) | |
.overlay { | |
VStack { | |
HStack { | |
Spacer() | |
StandardCircleIcon(iconName: SCL.icons.checkmark, color: SCL.colors.BrandColor.greenDeep, size: 25, isSolidColor: true) | |
.offset(x: 8, y: -10) | |
} | |
Spacer() | |
} | |
} | |
} else { | |
$0.shadow(color: SCL.colors.labelSecondaryColor.opacity(0.3),radius: 3) | |
} | |
} | |
) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment