Skip to content

Instantly share code, notes, and snippets.

@lifeutilityapps
Created September 21, 2024 12:15
Show Gist options
  • Save lifeutilityapps/b060d8b4f0dd6a9a0871d0aecb8df61d to your computer and use it in GitHub Desktop.
Save lifeutilityapps/b060d8b4f0dd6a9a0871d0aecb8df61d to your computer and use it in GitHub Desktop.
SwiftUI Color Theme Picker
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