29
votes

Comment puis-je changer une couleur SwiftUI en UIColor?

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?


1 commentaires

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


5 Réponses :


-3
votes

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 :-(


2 commentaires

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.



23
votes

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 .


4 commentaires

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.



6
votes

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.


0 commentaires

24
votes

iOS 14 / macOS 10.16

Il existe un nouvel initialiseur qui prend une Color et renvoie un UIColor pour iOS ou NSColor pour macOS maintenant. Donc:

iOS

UIColor(Color.red).cgColor /* For iOS */
NSColor(Color.red).cgColor /* For macOS */

macOS

NSColor(Color.red)

Graphiques de base

UIColor(Color.red)

Si vous recherchez des composants de couleur, vous pouvez trouver mes extensions utiles ici dans cette réponse


1 commentaires

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)'"



0
votes

@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)
    }
}


0 commentaires