Files
Morgan Pretty 55ba2aa8ed Merge remote-tracking branch 'upstream/dev' into feature/libsession-message-wrapping
# Conflicts:
#	Session/Conversations/ConversationVC+Interaction.swift
#	Session/Conversations/ConversationVC.swift
#	Session/Conversations/Settings/ThreadSettingsViewModel.swift
#	Session/Home/GlobalSearch/GlobalSearchViewController.swift
#	Session/Meta/AppDelegate.swift
#	SessionMessagingKit/Open Groups/OpenGroupManager.swift
#	SessionMessagingKit/Sending & Receiving/MessageReceiver.swift
#	SessionMessagingKit/Utilities/MessageWrapper.swift
2025-12-17 13:23:36 +11:00

738 lines
31 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import UIKit
import SwiftUI
// MARK: - ThemeManager
public enum ThemeManager {
private static var hasSetInitialSystemTrait: Bool = false
/// **Note:** Using `weakToStrongObjects` means that the value types will continue to be maintained until the map table resizes
/// itself (ie. until a new UI element is registered to the table)
///
/// Unfortunately if we don't do this the `ThemeApplier` is immediately deallocated and we can't use it to update the theme
@MainActor private static var uiRegistry: NSMapTable<AnyObject, ThemeApplier> = NSMapTable.weakToStrongObjects()
fileprivate static let syncState: ThemeManagerSyncState = ThemeManagerSyncState(
theme: Theme.defaultTheme,
primaryColor: Theme.PrimaryColor.defaultPrimaryColor,
matchSystemNightModeSetting: false // Default to `false`
)
public static var currentTheme: Theme { syncState.state.theme }
internal static var primaryColor: Theme.PrimaryColor { syncState.state.primaryColor }
// MARK: - Styling
@MainActor public static func updateThemeState(
theme: Theme? = nil,
primaryColor: Theme.PrimaryColor? = nil,
matchSystemNightModeSetting: Bool? = nil
) {
let currentState: ThemeState = syncState.state
let targetTheme: Theme = (theme ?? currentState.theme)
let targetPrimaryColor: Theme.PrimaryColor = {
switch (primaryColor, Theme.PrimaryColor(color: color(for: .defaultPrimary, in: currentState.theme, with: currentState.primaryColor))) {
case (.some(let primaryColor), _): return primaryColor
case (.none, .some(let defaultPrimaryColor)): return defaultPrimaryColor
default: return currentState.primaryColor
}
}()
let targetMatchSystemNightModeSetting: Bool = {
switch matchSystemNightModeSetting {
case .some(let value): return value
case .none: return currentState.matchSystemNightModeSetting
}
}()
let themeChanged: Bool = (currentState.theme != targetTheme || currentState.primaryColor != targetPrimaryColor)
let matchSystemChanged: Bool = (currentState.matchSystemNightModeSetting != targetMatchSystemNightModeSetting)
syncState.update(
hasLoadedTheme: true,
theme: targetTheme,
primaryColor: targetPrimaryColor
)
if matchSystemChanged {
syncState.update(matchSystemNightModeSetting: targetMatchSystemNightModeSetting)
// Note: We need to set this to 'unspecified' to force the UI to properly update as the
// 'TraitObservingWindow' won't actually trigger the trait change otherwise
SNUIKit.mainWindow?.overrideUserInterfaceStyle = .unspecified
}
// If the theme was changed then trigger a UI update and the callback for the theme settings
// change (so it gets persisted)
guard themeChanged || matchSystemChanged else {
if !hasSetInitialSystemTrait {
updateAllUI()
}
return
}
if !hasSetInitialSystemTrait || themeChanged {
updateAllUI()
}
SNUIKit.themeSettingsChanged(targetTheme, targetPrimaryColor, targetMatchSystemNightModeSetting)
}
@MainActor public static func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
let currentUserInterfaceStyle: UIUserInterfaceStyle = UITraitCollection.current.userInterfaceStyle
let currentState: ThemeState = syncState.state
// Only trigger updates if the style changed and the device is set to match the system style
guard
currentUserInterfaceStyle != currentState.theme.interfaceStyle,
currentState.matchSystemNightModeSetting
else { return }
// Swap to the appropriate light/dark mode
switch (currentUserInterfaceStyle, currentState.theme) {
case (.light, .classicDark): updateThemeState(theme: .classicLight, primaryColor: currentState.primaryColor)
case (.light, .oceanDark): updateThemeState(theme: .oceanLight, primaryColor: currentState.primaryColor)
case (.dark, .classicLight): updateThemeState(theme: .classicDark, primaryColor: currentState.primaryColor)
case (.dark, .oceanLight): updateThemeState(theme: .oceanDark, primaryColor: currentState.primaryColor)
default: break
}
}
@MainActor public static func applyNavigationStyling() {
let currentState: ThemeState = syncState.state
let textPrimary: UIColor = (color(for: .textPrimary, in: currentState.theme, with: currentState.primaryColor) ?? .white)
let backgroundColor: UIColor? = color(for: .backgroundPrimary, in: currentState.theme, with: currentState.primaryColor)
// Set the `mainWindow.tintColor` for system screens to use the right color for text
SNUIKit.mainWindow?.tintColor = textPrimary
SNUIKit.mainWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
// Update toolbars to use the right colours
UIToolbar.appearance().barTintColor = color(for: .backgroundPrimary, in: currentState.theme, with: currentState.primaryColor)
UIToolbar.appearance().isTranslucent = false
UIToolbar.appearance().tintColor = textPrimary
// Update the nav bars to use the right colours
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = backgroundColor
appearance.shadowImage = backgroundColor?.toImage()
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimary
]
appearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimary
]
// Apply the button item appearance as well
let barButtonItemAppearance = UIBarButtonItemAppearance(style: .plain)
barButtonItemAppearance.normal.titleTextAttributes = [ .foregroundColor: textPrimary ]
barButtonItemAppearance.disabled.titleTextAttributes = [ .foregroundColor: textPrimary ]
barButtonItemAppearance.highlighted.titleTextAttributes = [ .foregroundColor: textPrimary ]
barButtonItemAppearance.focused.titleTextAttributes = [ .foregroundColor: textPrimary ]
appearance.buttonAppearance = barButtonItemAppearance
appearance.backButtonAppearance = barButtonItemAppearance
appearance.doneButtonAppearance = barButtonItemAppearance
UINavigationBar.appearance().isTranslucent = false
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
// Note: 'UINavigationBar.appearance' only affects newly created nav bars so we need
// to force-update any current navigation bar (unfortunately the only way to do this
// is to remove the nav controller from the view hierarchy and then re-add it)
func updateIfNeeded(viewController: UIViewController?) {
guard let viewController: UIViewController = viewController else { return }
guard
let navController: UINavigationController = retrieveNavigationController(from: viewController),
let superview: UIView = navController.view.superview,
!navController.isNavigationBarHidden
else {
updateIfNeeded(viewController:
viewController.presentedViewController ??
viewController.navigationController?.presentedViewController
)
return
}
// Apply non-primary styling if needed
applyNavigationStylingIfNeeded(to: viewController)
// Re-attach to the UI
let wasFirstResponder: Bool = (navController.topViewController?.isFirstResponder == true)
switch navController.parent {
case let topBannerController as TopBannerController:
navController.view.removeFromSuperview()
topBannerController.attachChild()
default:
navController.view.removeFromSuperview()
superview.addSubview(navController.view)
}
navController.topViewController?.setNeedsStatusBarAppearanceUpdate()
if wasFirstResponder { navController.topViewController?.becomeFirstResponder() }
// Recurse through the rest of the UI
updateIfNeeded(viewController:
viewController.presentedViewController ??
viewController.navigationController?.presentedViewController
)
}
updateIfNeeded(viewController: SNUIKit.mainWindow?.rootViewController)
}
@MainActor public static func applyNavigationStylingIfNeeded(to viewController: UIViewController) {
// Will use the 'primary' style for all other cases
guard
let navController: UINavigationController = ((viewController as? UINavigationController) ?? viewController.navigationController),
let navigationBackground: ThemeValue = (viewController as? ThemedNavigation)?.navigationBackground
else { return }
let currentState: ThemeState = syncState.state
let navigationBackgroundColor: UIColor? = color(for: navigationBackground, in: currentState.theme, with: currentState.primaryColor)
navController.navigationBar.barTintColor = navigationBackgroundColor
navController.navigationBar.shadowImage = navigationBackgroundColor?.toImage()
let textPrimary: UIColor = (color(for: .textPrimary, in: currentState.theme, with: currentState.primaryColor) ?? .white)
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = navigationBackgroundColor
appearance.shadowImage = navigationBackgroundColor?.toImage()
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimary
]
appearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimary
]
navController.navigationBar.standardAppearance = appearance
navController.navigationBar.scrollEdgeAppearance = appearance
}
@MainActor public static func applyWindowStyling() {
let currentState: ThemeState = syncState.state
SNUIKit.mainWindow?.overrideUserInterfaceStyle = {
guard !currentState.matchSystemNightModeSetting else { return .unspecified }
switch currentState.theme.interfaceStyle {
case .light: return .light
case .dark, .unspecified: return .dark
@unknown default: return .dark
}
}()
SNUIKit.mainWindow?.backgroundColor = color(for: .backgroundPrimary, in: currentState.theme, with: currentState.primaryColor)
}
@MainActor public static func onThemeChange(observer: AnyObject, callback: @escaping @MainActor (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) {
ThemeManager.storeAndApply(
observer,
info: []
) { theme in
callback(theme, syncState.state.primaryColor, { value -> UIColor? in
ThemeManager.color(for: value, in: theme, with: syncState.state.primaryColor)
})
}
}
internal static func color<T: ColorType>(
for value: ThemeValue,
in theme: Theme,
with primaryColor: Theme.PrimaryColor
) -> T? {
switch value {
case .value(let value, let alpha):
let color: T? = color(for: value, in: theme, with: primaryColor)
return color?.alpha(alpha) as? T
case .primary: return T.resolve(primaryColor)
case .explicitPrimary(let explicitPrimary): return T.resolve(explicitPrimary)
case .highlighted(let value, let alwaysDarken):
let color: T? = color(for: value, in: theme, with: primaryColor)!
switch (syncState.state.theme.interfaceStyle, alwaysDarken) {
case (.light, _), (_, true): return color?.brighten(-0.06) as? T
default: return color?.brighten(0.08) as? T
}
case .dynamicForInterfaceStyle(let light, let dark):
switch syncState.state.theme.interfaceStyle {
case .light: return color(for: light, in: theme, with: primaryColor)
default: return color(for: dark, in: theme, with: primaryColor)
}
case .dynamicForPrimary(let targetPrimaryColor, let colorIfPrimaryMatches, let fallbackColor):
return color(
for: (primaryColor == targetPrimaryColor ?
colorIfPrimaryMatches :
fallbackColor
),
in: theme,
with: primaryColor
)
default:
let result: T? = T.resolve(value, for: theme)
/// Since our `primary` colour is no longer based on a `dynamicProvider` we now need to custom handle
/// when a `ThemeValue` tries to resolve to it
if result?.isPrimary == true {
return T.resolve(primaryColor)
}
return result
}
}
// MARK: - Internal Functions
@MainActor private static func updateAllUI() {
let currentState: ThemeState = syncState.state
ThemeManager.uiRegistry.objectEnumerator()?.forEach { applier in
(applier as? ThemeApplier)?.apply(theme: currentState.theme)
}
applyNavigationStyling()
applyWindowStyling()
if !hasSetInitialSystemTrait {
traitCollectionDidChange(nil)
hasSetInitialSystemTrait = true
}
}
private static func retrieveNavigationController(from viewController: UIViewController) -> UINavigationController? {
switch viewController {
case let navController as UINavigationController: return navController
case let topBannerController as TopBannerController:
return (topBannerController.children.first as? UINavigationController)
default: return viewController.navigationController
}
}
@MainActor internal static func storeAndApply<T: AnyObject>(
_ view: T,
info: [ThemeApplier.Info],
applyTheme: @escaping @MainActor (Theme) -> ()
) {
let applier: ThemeApplier = ThemeApplier(
existingApplier: ThemeManager.get(for: view),
info: info,
applyTheme: applyTheme
)
ThemeManager.uiRegistry.setObject(applier, forKey: view)
applier.performInitialApplicationIfNeeded()
}
@MainActor internal static func set<T: AnyObject>(
_ view: T,
keyPath: ReferenceWritableKeyPath<T, UIColor?>,
to value: ThemeValue?,
as info: ThemeApplier.Info = .other
) {
ThemeManager.storeAndApply(
view,
info: [ .keyPath(keyPath), .color(value), info ]
) { [weak view] theme in
guard let value: ThemeValue = value else {
view?[keyPath: keyPath] = nil
return
}
let currentState: ThemeState = syncState.state
view?[keyPath: keyPath] = ThemeManager.resolvedColor(
ThemeManager.color(for: value, in: currentState.theme, with: currentState.primaryColor)
)
}
}
@MainActor internal static func remove<T: AnyObject>(
_ view: T,
keyPath: ReferenceWritableKeyPath<T, UIColor?>
) {
// Note: Need to explicitly remove (setting to 'nil' won't actually remove it)
guard let updatedApplier: ThemeApplier = ThemeManager.get(for: view)?.removing(allWith: .keyPath(keyPath)) else {
ThemeManager.uiRegistry.removeObject(forKey: view)
return
}
ThemeManager.uiRegistry.setObject(updatedApplier, forKey: view)
}
@MainActor internal static func set<T: AnyObject>(
_ view: T,
keyPath: ReferenceWritableKeyPath<T, CGColor?>,
to value: ThemeValue?,
as info: ThemeApplier.Info = .other
) {
ThemeManager.storeAndApply(
view,
info: [ .keyPath(keyPath), .color(value), info ]
) { [weak view] theme in
guard let value: ThemeValue = value else {
view?[keyPath: keyPath] = nil
return
}
let currentState: ThemeState = syncState.state
view?[keyPath: keyPath] = ThemeManager.resolvedColor(
ThemeManager.color(for: value, in: currentState.theme, with: currentState.primaryColor)
)?.cgColor
}
}
@MainActor internal static func remove<T: AnyObject>(
_ view: T,
keyPath: ReferenceWritableKeyPath<T, CGColor?>
) {
ThemeManager.uiRegistry.setObject(
ThemeManager.get(for: view)?
.removing(allWith: .keyPath(keyPath)),
forKey: view
)
}
@MainActor internal static func set<T: AttributedTextAssignable>(
_ view: T,
keyPath: ReferenceWritableKeyPath<T, ThemedAttributedString?>,
to value: ThemedAttributedString?
) {
ThemeManager.storeAndApply(
view,
info: [ .keyPath(keyPath), .other ]
) { [weak view] theme in
guard let originalThemedString: ThemedAttributedString = value else {
view?[keyPath: keyPath] = nil
return
}
let newAttrString: NSMutableAttributedString = NSMutableAttributedString()
let originalAttrString: NSAttributedString = originalThemedString.attributedString
let fullRange: NSRange = NSRange(location: 0, length: originalAttrString.length)
let currentState: ThemeState = syncState.state
originalAttrString.enumerateAttributes(in: fullRange, options: []) { attributes, range, _ in
var newAttributes: [NSAttributedString.Key: Any] = attributes
var foundTextColor: Bool = false
/// Retrieve our custom alpha multiplier attribute
let alphaMultiplier: CGFloat = ((newAttributes[.themeAlphaMultiplier] as? CGFloat) ?? 1.0)
newAttributes.removeValue(forKey: .themeAlphaMultiplier)
/// Convert any of our custom attributes to their normal ones
NSAttributedString.Key.themedKeys.forEach { key in
guard let themeValue: ThemeValue = newAttributes[key] as? ThemeValue else { return }
newAttributes.removeValue(forKey: key)
guard
let originalKey: NSAttributedString.Key = key.originalKey,
let color: UIColor = ThemeManager.color(for: themeValue, in: currentState.theme, with: currentState.primaryColor)
else { return }
foundTextColor = true
newAttributes[originalKey] = (alphaMultiplier < 1 ?
ThemeManager.resolvedColor(color)?.withAlphaComponent(alphaMultiplier):
ThemeManager.resolvedColor(color)
)
}
/// If we didn't find an explicit text color but have set `themeAlphaMultiplier` then we need to try to extract
/// the current text color from the component and add that
if !foundTextColor && alphaMultiplier < 1 {
let maybeThemeValue: ThemeValue? = view
.map { ThemeManager.get(for: $0) }?
.map { $0.allInfo }
.map { $0.first(where: { $0.contains(.textColor) }) }?
.map { $0.compactMap { $0.storedValue as? ThemeValue } }?
.first
if
let originalKey: NSAttributedString.Key = .themeForegroundColor.originalKey,
let themeValue: ThemeValue = maybeThemeValue,
let color: UIColor = ThemeManager.color(for: themeValue, in: currentState.theme, with: currentState.primaryColor)
{
newAttributes[originalKey] = ThemeManager.resolvedColor(color)?
.withAlphaComponent(alphaMultiplier)
}
}
let newAttrSubstring: NSAttributedString
let substring: String = originalAttrString.attributedSubstring(from: range).string
/// Retrieve our custom user mention image attribute
///
/// If this has been set then we actually want to entirely replace the tagged content with an image attachment
if let currentUserMentionImage: UIImage = newAttributes[.themeCurrentUserMentionImage] as? UIImage {
newAttrSubstring = MentionUtilities.currentUserMentionImageString(
substring: substring,
currentUserMentionImage: currentUserMentionImage
)
newAttributes.removeValue(forKey: .themeCurrentUserMentionImage)
}
else {
/// Otherwise we can just extract the raw string from the original
newAttrSubstring = NSAttributedString(string: substring, attributes: newAttributes)
}
/// Add the themed substring to `newAttrString`
newAttrString.append(newAttrSubstring)
}
view?[keyPath: keyPath] = ThemedAttributedString(attributedString: newAttrString)
}
}
@MainActor internal static func set<T: AnyObject>(
_ view: T,
to applier: ThemeApplier?
) {
ThemeManager.uiRegistry.setObject(applier, forKey: view)
}
/// Using a `UIColor(dynamicProvider:)` unfortunately doesn't seem to work properly for some controls (eg. UISwitch) so
/// since we are already explicitly updating all UI when changing colors & states we just force-resolve the primary color to avoid
/// running into these glitches
internal static func resolvedColor(_ color: UIColor?) -> UIColor? {
return color?.resolvedColor(with: UITraitCollection())
}
@MainActor internal static func get(for view: AnyObject) -> ThemeApplier? {
return ThemeManager.uiRegistry.object(forKey: view)
}
}
// MARK: - SyncState
private struct ThemeState {
let theme: Theme
let primaryColor: Theme.PrimaryColor
let matchSystemNightModeSetting: Bool
}
private final class ThemeManagerSyncState {
private let lock: NSLock = NSLock()
private var _hasLoadedTheme: Bool = false
private var _theme: Theme
private var _primaryColor: Theme.PrimaryColor
private var _matchSystemNightModeSetting: Bool
fileprivate var hasLoadedTheme: Bool { lock.withLock { _hasLoadedTheme } }
fileprivate var state: ThemeState {
lock.withLock {
ThemeState(
theme: _theme,
primaryColor: _primaryColor,
matchSystemNightModeSetting: _matchSystemNightModeSetting
)
}
}
fileprivate init(
theme: Theme,
primaryColor: Theme.PrimaryColor,
matchSystemNightModeSetting: Bool
) {
self._theme = theme
self._primaryColor = primaryColor
self._matchSystemNightModeSetting = matchSystemNightModeSetting
}
fileprivate func update(
hasLoadedTheme: Bool? = nil,
theme: Theme? = nil,
primaryColor: Theme.PrimaryColor? = nil,
matchSystemNightModeSetting: Bool? = nil
) {
lock.withLock {
self._hasLoadedTheme = (hasLoadedTheme ?? self._hasLoadedTheme)
self._theme = (theme ?? self._theme)
self._primaryColor = (primaryColor ?? self._primaryColor)
self._matchSystemNightModeSetting = (matchSystemNightModeSetting ?? self._matchSystemNightModeSetting)
}
}
}
// MARK: - ThemeApplier
internal class ThemeApplier {
enum Info: Equatable {
case keyPath(AnyHashable)
case state(UIControl.State)
case color(ThemeValue?)
case textColor
case backgroundColor
case other
public var storedValue: Any? {
switch self {
case .keyPath(let value): return value
case .color(let value): return value
case .state(let value): return value
case .textColor, .backgroundColor, .other: return nil
}
}
}
private let applyTheme: @MainActor (Theme) -> ()
private let info: [Info]
private var otherAppliers: [ThemeApplier]?
public var allInfo: [[Info]] {
return [info] + (otherAppliers?.flatMap { $0.allInfo } ?? [])
}
@MainActor init(
existingApplier: ThemeApplier?,
info: [Info],
applyTheme: @escaping @MainActor (Theme) -> ()
) {
self.applyTheme = applyTheme
self.info = info
// Store any existing "appliers" (removing their 'otherApplier' references to prevent
// loops and excluding any which match the current "info" as they should be replaced
// by this applier)
self.otherAppliers = [existingApplier]
.appending(contentsOf: existingApplier?.otherAppliers)
.compactMap { $0?.clearingOtherAppliers() }
.filter { $0.info != info }
}
// MARK: - Functions
public func removing(allWith info: Info) -> ThemeApplier? {
let remainingAppliers: [ThemeApplier] = [self]
.appending(contentsOf: self.otherAppliers)
.filter { applier in !applier.info.contains(info) }
guard !remainingAppliers.isEmpty else { return nil }
guard remainingAppliers.count != ((self.otherAppliers ?? []).count + 1) else { return self }
// Remove the 'otherAppliers' references on self
self.otherAppliers = nil
// Attach the 'otherAppliers' to the new first remaining applier (just in case self
// was removed)
let firstApplier: ThemeApplier? = remainingAppliers.first
firstApplier?.otherAppliers = Array(remainingAppliers.suffix(from: 1))
return firstApplier
}
private func clearingOtherAppliers() -> ThemeApplier {
self.otherAppliers = nil
return self
}
@MainActor fileprivate func performInitialApplicationIfNeeded() {
// Only perform the initial application if both the database and theme have been setup
if SNUIKit.config?.isStorageValid == true || ThemeManager.syncState.hasLoadedTheme {
apply(theme: ThemeManager.syncState.state.theme, isInitialApplication: true)
}
}
@MainActor fileprivate func apply(theme: Theme, isInitialApplication: Bool = false) {
self.applyTheme(theme)
// For the initial application of a ThemeApplier we don't want to apply the other
// appliers (they should have already been applied so doing so is redundant
guard !isInitialApplication else { return }
// If there are otherAppliers stored against this one then trigger those as well
self.otherAppliers?.forEach { applier in
applier.applyTheme(theme)
}
}
}
// MARK: - Convenience Extensions
extension Array {
fileprivate func appending(contentsOf other: [Element]?) -> [Element] {
guard let other: [Element] = other else { return self }
var updatedArray: [Element] = self
updatedArray.append(contentsOf: other)
return updatedArray
}
}
// MARK: - ColorType
internal protocol ColorType {
/// Apple have done some odd schenanigans with `UIColor` where some types aren't _actually_ `UIColor` but a special
/// type (eg. `UIColor.black` and `UIColor.white` are `UICachedDeviceWhiteColor`), due to this casting to
/// `Self` in an extension on `UIColor` ends up failing (because calling `alpha(_)` on a `UICachedDeviceWhiteColor`
/// expects you to return a `UICachedDeviceWhiteColor`, but the alpha-applied output is a standard `UIColor` which can't
/// convert to `Self`), by defining an explicit `BaseColorType` we return an explicit type and avoid weird private types
associatedtype BaseColorType
var isPrimary: Bool { get }
func alpha(_ alpha: Double) -> BaseColorType?
func brighten(_ amount: Double) -> BaseColorType?
}
extension UIColor: ColorType {
internal var isPrimary: Bool { self == UIColor.primary() }
internal func alpha(_ alpha: Double) -> UIColor? {
return self.withAlphaComponent(CGFloat(alpha))
}
internal func brighten(_ amount: Double) -> UIColor? {
return self.brighten(by: amount)
}
}
extension Color: ColorType {
internal var isPrimary: Bool { self == Color.primary() }
internal func alpha(_ alpha: Double) -> Color? {
return self.opacity(alpha)
}
internal func brighten(_ amount: Double) -> Color? {
guard amount > 0 else {
return (self.grayscale(amount) as? Color)
}
return (self.brightness(amount) as? Color)
}
}
// MARK: - Previews
private struct PreviewThemeKey: EnvironmentKey {
static let defaultValue: (Theme, Theme.PrimaryColor)? = nil
}
extension EnvironmentValues {
var previewTheme: (Theme, Theme.PrimaryColor)? {
get { self[PreviewThemeKey.self] }
set { self[PreviewThemeKey.self] = newValue }
}
}
public struct PreviewThemeWrapper<Content: View>: View {
let theme: Theme
let primaryColor: Theme.PrimaryColor
let content: Content
public init(theme: Theme, primaryColor: Theme.PrimaryColor? = nil, @ViewBuilder content: () -> Content) {
self.theme = theme
self.primaryColor = (primaryColor ?? theme.defaultPrimary)
self.content = content()
}
public var body: some View {
content
.environment(\.previewTheme, (theme, primaryColor))
}
}