J'essaye de changer une couleur SwiftUI en une instance d'UIColor.
Je peux facilement obtenir le RGBA de l'UIColor, mais je ne sais pas comment obtenir l'instance "Color" pour renvoyer les valeurs RVB et d'opacité correspondantes.
@EnvironmentObject var colorStore: ColorStore
init() {
let red = //get red value from colorStore.primaryThemeColor
let green = //get green value from colorStore.primaryThemeColor
let blue = //get blue value from colorStore.primaryThemeColor
let alpha = //get alpha value from colorStore.primaryThemeColor
let color = UIColor(red: red, green: green, blue: blue, alpha: alpha)
UINavigationBar.appearance().tintColor = color
}
... ou peut-être existe-t-il une meilleure façon d'accomplir ce que je recherche?
5 Réponses :
Ce n'est pas ainsi que SwiftUI fonctionne. Ce que vous essayez de faire est très similaire à UIKit. Dans SwiftUI, vous interrogez rarement une vue pour un paramètre. Pour le moment, Color n'a pas de méthode ou de propriétés qui renvoient ses valeurs RVB. Et je doute qu'il y en ait jamais.
En général, avec SwiftUI, vous devez accéder à la source, c'est-à-dire à la variable que vous avez utilisée pour créer la couleur en premier lieu. Par exemple:
let mycolor = Color.red
Il n'y a rien de tel:
let green = g
Au lieu de cela, vous devez vérifier la variable g (la variable que vous avez utilisée pour créer la couleur):
let green = mycolor.greenComponent()
Je sais que cela semble étrange, mais c'est ainsi que le cadre a été conçu. Cela peut prendre du temps pour s'y habituer, mais vous finirez par le faire.
Vous pouvez demander, mais que faire si mycolor a été créé comme:
let r = 0.9 let g = 0.4 let b = 0.7 let mycolor = Color(red: r, green: g, b, opacity: o)
Dans ce cas particulier, vous n'avez pas de chance :-(
Hmm, donc les couleurs de mon application changent dynamiquement à partir d'un magasin qui ne renvoie que des objets Color. Donc, même si la solution que j'ai essayée ne fonctionne pas, y a-t-il un autre moyen de changer la teinte de la UINavigationBar en valeur de couleur dynamique?
UINavigationBar est UIKit. Vous devez attendre que SwiftUI lui-même donne accès à la barre de navigation.
Et cette solution?
let uiColor = myColor.uiColor()
Usage:
extension Color {
func uiColor() -> UIColor {
if #available(iOS 14.0, *) {
return UIColor(self)
}
let components = self.components()
return UIColor(red: components.r, green: components.g, blue: components.b, alpha: components.a)
}
private func components() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
let scanner = Scanner(string: self.description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted))
var hexNumber: UInt64 = 0
var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0
let result = scanner.scanHexInt64(&hexNumber)
if result {
r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
a = CGFloat(hexNumber & 0x000000ff) / 255
}
return (r, g, b, a)
}
}
C'est un peu un hack, mais c'est au moins quelque chose jusqu'à ce que nous obtenions une méthode valide pour cela. La clé ici est self.description qui donne une description hexadécimale de la couleur (si ce n'est pas dynamique, je devrais ajouter). Et le reste n'est que des calculs pour obtenir les composants de couleur et créer un UIColor .
Cela peut être piraté, mais c'est la bonne réponse à mon avis. Il y a certainement des raisons de vouloir interroger le type Color. Par exemple, j'ai une extension sur Color qui renvoie une couleur de texte appropriée pour la couleur donnée comme arrière-plan. Quand tout ce avec quoi je dois travailler pour commencer, c'est la var accentColor: Color , alors je dois pouvoir interroger le maquillage de cette couleur. Voté pour la seule créativité et j'espère qu'Apple aura plus à ajouter à cette année prochaine.
NB: cela ne fonctionne pas si vous faites référence à une couleur dans un catalogue d'actifs (où .description "NamedColor(name: \"Dark Roasts/Espresso100\", bundle: nil)" ou similaire)
cela ne fonctionne pas si la couleur est .red , la variable de result renvoie false dans ce cas mais je ne sais pas pourquoi.
@JAHelia C'est parce que Color.red est dynamique, c'est-à-dire qu'il change en fonction du mode actuel. Lorsque le mode sombre est activé, le rouge devient également plus sombre. Donc, il n'a pas de valeur unique, c'est pourquoi result renvoie false.
Actuellement, cela n'est pas directement disponible dans l'API SwiftUI. Cependant, j'ai réussi à faire fonctionner un initialiseur de fortune qui utilise des impressions de débogage et un dump . J'ai trouvé que toutes les autres solutions ne .displayP3 pas compte d'une Color initialisée à partir d'un nom, d'un bundle, de l'espace colorimétrique .displayP3 , d'un UIColor , d'un système statique Color ou de toute couleur dont l'opacité était modifiée. Ma solution tient compte de toutes les baisses susmentionnées.
fileprivate struct ColorConversionError: Swift.Error {
let reason: String
}
extension Color {
@available(*, deprecated, message: "This is fragile and likely to break at some point. Hopefully it won't be required for long.")
var uiColor: UIColor {
do {
return try convertToUIColor()
} catch let error {
assertionFailure((error as! ColorConversionError).reason)
return .black
}
}
}
fileprivate extension Color {
var stringRepresentation: String { description.trimmingCharacters(in: .whitespacesAndNewlines) }
var internalType: String { "\(type(of: Mirror(reflecting: self).children.first!.value))".replacingOccurrences(of: "ColorBox<(.+)>", with: "$1", options: .regularExpression) }
func convertToUIColor() throws -> UIColor {
if let color = try OpacityColor(color: self) {
return try UIColor.from(swiftUIDescription: color.stringRepresentation, internalType: color.internalType).multiplyingAlphaComponent(by: color.opacityModifier)
}
return try UIColor.from(swiftUIDescription: stringRepresentation, internalType: internalType)
}
}
fileprivate struct OpacityColor {
let stringRepresentation: String
let internalType: String
let opacityModifier: CGFloat
init(stringRepresentation: String, internalType: String, opacityModifier: CGFloat) {
self.stringRepresentation = stringRepresentation
self.internalType = internalType
self.opacityModifier = opacityModifier
}
init?(color: Color) throws {
guard color.internalType == "OpacityColor" else {
return nil
}
let string = color.stringRepresentation
let opacityRegex = try! NSRegularExpression(pattern: #"(\d+% )"#)
let opacityLayerCount = opacityRegex.numberOfMatches(in: string, options: [], range: NSRange(string.startIndex..<string.endIndex, in: string))
var dumpStr = ""
dump(color, to: &dumpStr)
dumpStr = dumpStr.replacingOccurrences(of: #"^(?:.*\n){\#(4 * opacityLayerCount)}.*?base: "#, with: "", options: .regularExpression)
let opacityModifier = dumpStr.split(separator: "\n")
.suffix(1)
.lazy
.map { $0.replacingOccurrences(of: #"\s+-\s+opacity: "#, with: "", options: .regularExpression) }
.map { CGFloat(Double($0)!) }
.reduce(1, *)
let internalTypeRegex = try! NSRegularExpression(pattern: #"^.*\n.*ColorBox<.*?([A-Za-z0-9]+)>"#)
let matches = internalTypeRegex.matches(in: dumpStr, options: [], range: NSRange(dumpStr.startIndex..<dumpStr.endIndex, in: dumpStr))
guard let match = matches.first, matches.count == 1, match.numberOfRanges == 2 else {
throw ColorConversionError(reason: "Could not parse internalType from \"\(dumpStr)\"")
try! self.init(color: Color.black.opacity(1))
}
self.init(
stringRepresentation: String(dumpStr.prefix { !$0.isNewline }),
internalType: String(dumpStr[Range(match.range(at: 1), in: dumpStr)!]),
opacityModifier: opacityModifier
)
}
}
fileprivate extension UIColor {
static func from(swiftUIDescription description: String, internalType: String) throws -> UIColor {
switch internalType {
case "SystemColorType":
guard let uiColor = UIColor.from(systemColorName: description) else {
throw ColorConversionError(reason: "Could not parse SystemColorType from \"\(description)\"")
}
return uiColor
case "_Resolved":
guard description.range(of: "^#[0-9A-F]{8}$", options: .regularExpression) != nil else {
throw ColorConversionError(reason: "Could not parse hex from \"\(description)\"")
}
let components = description
.dropFirst()
.chunks(of: 2)
.compactMap { CGFloat.decimalFromHexPair(String($0)) }
guard components.count == 4, let cgColor = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.linearSRGB)!, components: components) else {
throw ColorConversionError(reason: "Could not parse hex from \"\(description)\"")
}
return UIColor(cgColor: cgColor)
case "UIColor":
let sections = description.split(separator: " ")
let colorSpace = String(sections[0])
let components = sections[1...]
.compactMap { Double($0) }
.map { CGFloat($0) }
guard components.count == 4 else {
throw ColorConversionError(reason: "Could not parse UIColor components from \"\(description)\"")
}
let (r, g, b, a) = (components[0], components[1], components[2], components[3])
return try UIColor(red: r, green: g, blue: b, alpha: a, colorSpace: colorSpace)
case "DisplayP3":
let regex = try! NSRegularExpression(pattern: #"^DisplayP3\(red: (-?\d+(?:\.\d+)?), green: (-?\d+(?:\.\d+)?), blue: (-?\d+(?:\.\d+)?), opacity: (-?\d+(?:\.\d+)?)"#)
let matches = regex.matches(in: description, options: [], range: NSRange(description.startIndex..<description.endIndex, in: description))
guard let match = matches.first, matches.count == 1, match.numberOfRanges == 5 else {
throw ColorConversionError(reason: "Could not parse DisplayP3 from \"\(description)\"")
}
let components = (0..<match.numberOfRanges)
.dropFirst()
.map { Range(match.range(at: $0), in: description)! }
.compactMap { Double(String(description[$0])) }
.map { CGFloat($0) }
guard components.count == 4 else {
throw ColorConversionError(reason: "Could not parse DisplayP3 components from \"\(description)\"")
}
let (r, g, b, a) = (components[0], components[1], components[2], components[3])
return UIColor(displayP3Red: r, green: g, blue: b, alpha: a)
case "NamedColor":
guard description.range(of: #"^NamedColor\(name: "(.*)", bundle: .*\)$"#, options: .regularExpression) != nil else {
throw ColorConversionError(reason: "Could not parse NamedColor from \"\(description)\"")
}
let nameRegex = try! NSRegularExpression(pattern: #"name: "(.*)""#)
let name = nameRegex.matches(in: description, options: [], range: NSRange(description.startIndex..<description.endIndex, in: description))
.first
.flatMap { Range($0.range(at: 1), in: description) }
.map { String(description[$0]) }
guard let colorName = name else {
throw ColorConversionError(reason: "Could not parse NamedColor name from \"\(description)\"")
}
let bundleRegex = try! NSRegularExpression(pattern: #"bundle: .*NSBundle <(.*)>"#)
let bundlePath = bundleRegex.matches(in: description, options: [], range: NSRange(description.startIndex..<description.endIndex, in: description))
.first
.flatMap { Range($0.range(at: 1), in: description) }
.map { String(description[$0]) }
let bundle = bundlePath.map { Bundle(path: $0)! }
return UIColor(named: colorName, in: bundle, compatibleWith: nil)!
default:
throw ColorConversionError(reason: "Unhandled type \"\(internalType)\"")
}
}
static func from(systemColorName: String) -> UIColor? {
switch systemColorName {
case "clear": return .clear
case "black": return .black
case "white": return .white
case "gray": return .systemGray
case "red": return .systemRed
case "green": return .systemGreen
case "blue": return .systemBlue
case "orange": return .systemOrange
case "yellow": return .systemYellow
case "pink": return .systemPink
case "purple": return .systemPurple
case "primary": return .label
case "secondary": return .secondaryLabel
default: return nil
}
}
convenience init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat, colorSpace: String) throws {
if colorSpace == "UIDisplayP3ColorSpace" {
self.init(displayP3Red: red, green: green, blue: blue, alpha: alpha)
} else if colorSpace == "UIExtendedSRGBColorSpace" {
self.init(red: red, green: green, blue: blue, alpha: alpha)
} else if colorSpace == "kCGColorSpaceModelRGB" {
let colorSpace = CGColorSpace(name: CGColorSpace.linearSRGB)!
let components = [red, green, blue, alpha]
let cgColor = CGColor(colorSpace: colorSpace, components: components)!
self.init(cgColor: cgColor)
} else {
throw ColorConversionError(reason: "Unhandled colorSpace \"\(colorSpace)\"")
}
}
func multiplyingAlphaComponent(by multiplier: CGFloat?) -> UIColor {
var a: CGFloat = 0
getWhite(nil, alpha: &a)
return withAlphaComponent(a * (multiplier ?? 1))
}
}
// MARK: Helper extensions
extension StringProtocol {
func chunks(of size: Int) -> [Self.SubSequence] {
stride(from: 0, to: count, by: size).map {
let start = index(startIndex, offsetBy: $0)
let end = index(start, offsetBy: size, limitedBy: endIndex) ?? endIndex
return self[start..<end]
}
}
}
extension Int {
init?(hexString: String) {
self.init(hexString, radix: 16)
}
}
extension FloatingPoint {
static func decimalFromHexPair(_ hexPair: String) -> Self? {
guard hexPair.count == 2, let value = Int(hexString: hexPair) else {
return nil
}
return Self(value) / Self(255)
}
}
Remarque: bien que ce ne soit pas une solution à long terme pour le problème actuel, car il dépend des détails de mise en œuvre de Color qui peuvent changer à un moment donné, cela devrait fonctionner entre-temps pour la plupart des couleurs, sinon toutes.
Il existe un nouvel initialiseur qui prend une Color et renvoie un UIColor pour iOS ou NSColor pour macOS maintenant. Donc:
UIColor(Color.red).cgColor /* For iOS */ NSColor(Color.red).cgColor /* For macOS */
NSColor(Color.red)
UIColor(Color.red)
Si vous recherchez des composants de couleur, vous pouvez trouver mes extensions utiles ici dans cette réponse
les graphiques de base semblent être faux. Faire exactement cela renvoie "Impossible d'appeler l'initialiseur pour le type 'CGColor' avec une liste d'arguments de type '(Couleur)'"
@turingtested Mise à jour de votre réponse pour vous débarrasser du long crash de tuple.
extension Color {
func uiColor() -> UIColor {
if #available(iOS 14.0, *) {
return UIColor(self)
}
let scanner = Scanner(string: description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted))
var hexNumber: UInt64 = 0
var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0
let result = scanner.scanHexInt64(&hexNumber)
if result {
r = CGFloat((hexNumber & 0xFF000000) >> 24) / 255
g = CGFloat((hexNumber & 0x00FF0000) >> 16) / 255
b = CGFloat((hexNumber & 0x0000FF00) >> 8) / 255
a = CGFloat(hexNumber & 0x000000FF) / 255
}
return UIColor(red: r, green: g, blue: b, alpha: a)
}
}
Faites-moi savoir si vous trouvez un moyen. Je suis confronté au même problème. Je commence par swiftui mais il y a des endroits où j'ai besoin d'UIColor donc ce serait super d'avoir une extension à lancer entre eux comme avec UIColor-CGColor