【问题标题】:How can I change a SwiftUI Color to UIColor?如何将 SwiftUI 颜色更改为 UIColor?
【发布时间】:2019-12-07 00:31:38
【问题描述】:

我正在尝试将 SwiftUI 颜色更改为 UIColor 的实例。

我可以很容易地从 UIColor 中获取 RGBA,但是我不知道如何获取“Color”实例以返回相应的 RGB 和不透明度值。

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

...或者也许有更好的方法来完成我正在寻找的东西?

【问题讨论】:

    标签: ios macos swiftui


    【解决方案1】:

    SwiftUI 2.0

    现在有一个新的初始化程序接受 Color 并返回 UIColor 用于 iOSNSColor 用于 ma​​cOS。所以:

    iOS

    UIColor(Color.red)
    

    macOS

    NSColor(Color.red)
    

    核心图形

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

    如果您正在寻找颜色组件,可以找到有用的扩展程序here in this answer

    另外,请查看 How to convert UIColor to SwiftUI‘s Color

    【讨论】:

    【解决方案2】:

    这个解决方案怎么样?

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

    用法:

    let uiColor = myColor.uiColor()
    

    这有点像 hack,但至少在我们找到有效的方法之前,它是一些东西。这里的关键是self.description,它给出了颜色的十六进制描述(如果它不是动态的,我应该添加)。剩下的只是计算来获取颜色分量,并创建一个UIColor

    【讨论】:

    • 这可能很老套,但在我看来这是正确的答案。肯定有想要询问颜色类型的原因。例如,我在 Color 上有一个扩展,它为给定的 Color 返回适当的文本颜色作为背景。当我一开始只需要处理 var accentColor: Color 时,我需要能够询问该颜色的构成。仅凭创造力就获得了支持,我希望 Apple 明年能在这方面做出更多贡献。
    • 注意:如果您在资产目录中引用颜色(.description 产生 "NamedColor(name: \"Dark Roasts/Espresso100\", bundle: nil)" 或类似的颜色),这将不起作用
    • 这在颜色为.red 的情况下不起作用,result 变量在这种情况下返回 false 但我不知道为什么。
    • @JAHelia 这是因为Color.red 是动态的,即它会根据当前模式而变化。开启暗模式时,红色也会变暗。所以它没有单一值,这就是 result 返回 false 的原因。
    • 这不起作用。 let components = self.components() 总是返回 (0,0,0,0)
    【解决方案3】:

    目前,这在 SwiftUI API 中不直接可用。但是,我设法让一个临时初始化程序工作,它利用调试打印和dump。我发现所有其他解决方案都无法解释从名称、捆绑包、.displayP3 颜色空间、UIColor、静态系统Color 或任何不透明度改变的颜色初始化的Color。我的解决方案解决了上述所有问题。

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

    注意:虽然这不是解决手头问题的长期解决方案,因为它取决于Color 的实现细节,这些细节可能会在某些时候发生变化,它应该在过渡期间适用于大多数颜色,如果不是所有颜色的话。

    【讨论】:

      【解决方案4】:

      @turingtested 更新了您的答案以摆脱长元组崩溃。

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

      【讨论】:

      • 这会将 Color.white 变成 UIColor.black )
      • 这不适用于 iOS 13.1
      【解决方案5】:

      这不是 SwiftUI 的工作方式。您正在尝试做的非常像 UIKit。在 SwiftUI 中,您很少询问任何参数的视图。此时,Color 没有任何返回其 RGB 值的方法或属性。我怀疑永远不会有。

      一般来说,使用 SwiftUI 你需要找到源头,也就是你最初用来创建颜色的变量。例如:

        let r = 0.9
        let g = 0.4
        let b = 0.7
        let mycolor = Color(red: r, green: g, b, opacity: o)
      

      没有这样的:

      let green = mycolor.greenComponent()
      

      相反,您需要检查变量g(用于创建颜色的变量):

      let green = g
      

      我知道这听起来很奇怪,但这就是框架的设计方式。可能需要时间来适应它,但你最终会的。

      您可能会问,但如果 mycolor 被创建为:

      let mycolor = Color.red
      

      在那种特殊情况下,你运气不好:-(

      【讨论】:

      • 嗯,所以我的应用程序的颜色从只返回 Color 对象的商店动态更改。因此,即使我尝试的解决方案不起作用,还有其他方法可以将 UINavigationBar 的 tintColor 更改为动态 Color 值吗?
      • UINavigationBar 是 UIKit。您需要等到 SwiftUI 本身 提供对导航栏的访问权限。
      猜你喜欢
      • 2020-04-28
      • 2019-11-21
      • 2020-10-04
      • 2021-09-03
      • 2012-01-04
      • 2021-02-11
      • 1970-01-01
      • 2021-11-29
      • 2022-01-15
      相关资源
      最近更新 更多