Skip to content

Commit accc5df

Browse files
authored
Add NSColor colorProvider support (#320)
1 parent 90ef7cd commit accc5df

File tree

8 files changed

+244
-11
lines changed

8 files changed

+244
-11
lines changed

Example/HostingExample/Examples/AppearanceActionModifierExample.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ struct AppearanceActionModifierExample: View {
1313
@State private var first = true
1414

1515
var color: Color {
16-
#if os(macOS) // TODO:
17-
Color.red
16+
#if os(macOS)
17+
Color(nsColor: first ? .red : .blue)
1818
#else
1919
Color(uiColor: first ? .red : .blue)
2020
#endif

Sources/COpenSwiftUI/AppKit/OpenSwiftUI+NSColor.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN
1919
OPENSWIFTUI_EXPORT
2020
BOOL _NSColorDependsOnAppearance(NSColor *color);
2121

22-
@interface NSColor (OpenSwiftUI)
22+
@interface NSColor (OpenSwiftUI_NSColor)
2323

2424
// Workaround Swift initializer limitation
2525
- (instancetype)initWithColor__openSwiftUI__:(NSColor *)color;

Sources/COpenSwiftUI/AppKit/OpenSwiftUI+NSColor.m

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// OpenSwiftUI+NSColor.m
33
// COpenSwiftUI
44
//
5-
// Audited for macOS 15.0
5+
// Audited for 6.5.4
66
// Status: WIP
77

88
#import "OpenSwiftUI+NSColor.h"
@@ -13,8 +13,7 @@ BOOL _NSColorDependsOnAppearance(NSColor *color) {
1313
return [color isKindOfClass:NSClassFromString(@"NSDynamicNamedColor")];
1414
}
1515

16-
17-
@implementation NSColor (OpenSwiftUI)
16+
@implementation NSColor (OpenSwiftUI_NSColor)
1817

1918
- (instancetype)initWithColor__openSwiftUI__:(NSColor *)color {
2019
self = color;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// AppKitAppearanceConversions.swift
3+
// OpenSwiftUI
4+
//
5+
// Status: WIP
6+
// ID: FE0226775232C57AACFCDAD271FF7831 (SwiftUI)
7+
8+
#if canImport(AppKit)
9+
10+
import AppKit
11+
import OpenSwiftUI_SPI
12+
13+
// MARK: - NSAppearance Conversions [6.5.4]
14+
15+
extension NSAppearance {
16+
func apply(to environment: inout EnvironmentValues, vibrantBlendingStyle: NSViewVibrantBlendingStyle) {
17+
// TODO
18+
}
19+
20+
convenience init?(_ scheme: ColorScheme) {
21+
// TODO
22+
return nil
23+
}
24+
25+
var bestColorScheme: ColorScheme? {
26+
// TODO
27+
nil
28+
}
29+
30+
// static func makeAppearance(_ appearance: _ResolvedAppearance) -> NSAppearance? {
31+
// // TODO
32+
// nil
33+
// }
34+
35+
static func appearance(from environment: EnvironmentValues, allowsVibrantBlending: Bool?) -> NSAppearance? {
36+
// TODO
37+
nil
38+
}
39+
}
40+
41+
// private func lastAppearance(in appearance: _ResolvedAppearance) -> CatalogAppearance?
42+
43+
#endif
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//
2+
// AppKitColorConversions.swift
3+
// OpenSwiftUI
4+
//
5+
// Status: Complete
6+
// ID: 7137BB7EE57FAC34F81DC437C151F7AB (SwiftUI)
7+
8+
#if canImport(AppKit)
9+
10+
public import OpenSwiftUICore
11+
public import AppKit
12+
import COpenSwiftUI
13+
14+
// MARK: - NSColor Conversions [6.5.4]
15+
16+
@available(iOS, unavailable)
17+
@available(macOS, introduced: 10.15, deprecated: 100000.0, message: "Use Color(nsColor:) when converting a NSColor, or create a standard Color directly")
18+
@available(tvOS, unavailable)
19+
@available(watchOS, unavailable)
20+
@available(visionOS, unavailable)
21+
extension Color {
22+
/// Creates a color from an AppKit color.
23+
///
24+
/// Use this method to create a SwiftUI color from an
25+
/// [NSColor](https://developer.apple.com/documentation/AppKit/NSColor) instance.
26+
/// The new color preserves the adaptability of the original.
27+
/// For example, you can create a rectangle using
28+
/// [linkColor](https://developer.apple.com/documentation/AppKit/NSColor/linkColor)
29+
/// to see how the shade adjusts to match the user's system settings:
30+
///
31+
/// struct Box: View {
32+
/// var body: some View {
33+
/// Color(NSColor.linkColor)
34+
/// .frame(width: 200, height: 100)
35+
/// }
36+
/// }
37+
///
38+
/// The `Box` view defined above automatically changes its
39+
/// appearance when the user turns on Dark Mode. With the light and dark
40+
/// appearances placed side by side, you can see the subtle difference
41+
/// in shades:
42+
///
43+
/// ![A side by side comparison of light and dark appearance screenshots of
44+
/// rectangles rendered with the link color. The light variant appears on
45+
/// the left, and the dark variant on the right.](Color-init-4)
46+
///
47+
/// > Note: Use this initializer only if you need to convert an existing
48+
/// [NSColor](https://developer.apple.com/documentation/AppKit/NSColor) to a
49+
/// SwiftUI color. Otherwise, create a OpenSwiftUI ``Color`` using an
50+
/// initializer like ``init(_:red:green:blue:opacity:)``, or use a system
51+
/// color like ``ShapeStyle/blue``.
52+
///
53+
/// - Parameter color: An
54+
/// [NSColor](https://developer.apple.com/documentation/AppKit/NSColor) instance
55+
/// from which to create a color.
56+
@_disfavoredOverload
57+
nonisolated public init(_ color: NSColor) {
58+
self.init(nsColor: color)
59+
}
60+
}
61+
62+
@available(OpenSwiftUI_v3_0, *)
63+
@available(iOS, unavailable)
64+
@available(tvOS, unavailable)
65+
@available(watchOS, unavailable)
66+
@available(visionOS, unavailable)
67+
extension Color {
68+
/// Creates a color from an AppKit color.
69+
///
70+
/// Use this method to create a SwiftUI color from an
71+
/// [NSColor](https://developer.apple.com/documentation/AppKit/NSColor) instance.
72+
/// The new color preserves the adaptability of the original.
73+
/// For example, you can create a rectangle using
74+
/// [linkColor](https://developer.apple.com/documentation/AppKit/NSColor/linkColor)
75+
/// to see how the shade adjusts to match the user's system settings:
76+
///
77+
/// struct Box: View {
78+
/// var body: some View {
79+
/// Color(nsColor: .linkColor)
80+
/// .frame(width: 200, height: 100)
81+
/// }
82+
/// }
83+
///
84+
/// The `Box` view defined above automatically changes its
85+
/// appearance when the user turns on Dark Mode. With the light and dark
86+
/// appearances placed side by side, you can see the subtle difference
87+
/// in shades:
88+
///
89+
/// ![A side by side comparison of light and dark appearance screenshots of
90+
/// rectangles rendered with the link color. The light variant appears on
91+
/// the left, and the dark variant on the right.](Color-init-4)
92+
///
93+
/// > Note: Use this initializer only if you need to convert an existing
94+
/// [NSColor](https://developer.apple.com/documentation/AppKit/NSColor) to a
95+
/// SwiftUI color. Otherwise, create a OpenSwiftUI ``Color`` using an
96+
/// initializer like ``init(_:red:green:blue:opacity:)``, or use a system
97+
/// color like ``ShapeStyle/blue``.
98+
///
99+
/// - Parameter color: An
100+
/// [NSColor](https://developer.apple.com/documentation/AppKit/NSColor) instance
101+
/// from which to create a color.
102+
nonisolated public init(nsColor: NSColor) {
103+
self.init(provider: nsColor)
104+
}
105+
}
106+
107+
private let dynamicColorCache: NSMapTable<ObjcColor, NSColor> = NSMapTable.strongToWeakObjects()
108+
109+
extension NSColor: ColorProvider {
110+
@available(OpenSwiftUI_v2_0, *)
111+
@available(iOS, unavailable)
112+
@available(tvOS, unavailable)
113+
@available(watchOS, unavailable)
114+
@available(visionOS, unavailable)
115+
convenience public init(_ color: Color) {
116+
if let color = color.provider.as(NSColor.self) {
117+
self.init(color__openSwiftUI__: color)
118+
} else if let cgColor = color.provider.staticColor {
119+
self.init(cgColor: cgColor)!
120+
} else {
121+
let objCColor = ObjcColor(color)
122+
let cache = dynamicColorCache
123+
if let cachedColor = cache.object(forKey: objCColor) {
124+
self.init(color__openSwiftUI__: cachedColor)
125+
} else {
126+
self.init(name: nil) { appearance in
127+
var environment = EnvironmentValues()
128+
appearance.apply(to: &environment, vibrantBlendingStyle: ._1)
129+
environment.allowsVibrantBlending = false
130+
let resolved = color.resolve(in: environment)
131+
return resolved.kitColor as! NSColor
132+
}
133+
cache.setObject(self, forKey: objCColor)
134+
}
135+
}
136+
}
137+
138+
package func resolve(in environment: EnvironmentValues) -> Color.Resolved {
139+
if _NSColorDependsOnAppearance(self) {
140+
return withColorAppearance(in: environment) {
141+
Color.Resolved(platformColor: self) ?? .clear
142+
}
143+
} else {
144+
return Color.Resolved(cgColor)
145+
}
146+
}
147+
148+
private func withColorAppearance(in environment: EnvironmentValues, _ body: () -> Color.Resolved) -> Color.Resolved {
149+
let appearance = NSAppearance.appearance(from: environment, allowsVibrantBlending: false)
150+
var color = Color.Resolved.clear
151+
if let appearance {
152+
appearance.performAsCurrentDrawingAppearance {
153+
color = body()
154+
}
155+
} else {
156+
color = body()
157+
}
158+
return color
159+
}
160+
161+
package var staticColor: CGColor? {
162+
if _NSColorDependsOnAppearance(self) {
163+
nil
164+
} else {
165+
cgColor
166+
}
167+
}
168+
}
169+
170+
#endif

Sources/OpenSwiftUI/Integration/Graphic/UIKit/UIKitConversions.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import COpenSwiftUI
1414

1515
// MARK: - UIColor Conversions
1616

17-
@available(*, deprecated, message: "Use Color(uiColor:) when converting a UIColor, or create a standard Color directly")
17+
@available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Use Color(uiColor:) when converting a UIColor, or create a standard Color directly")
1818
@available(macOS, unavailable)
19+
@available(tvOS, introduced: 13.0, deprecated: 100000.0, message: "Use Color(uiColor:) when converting a UIColor, or create a standard Color directly")
20+
@available(watchOS, introduced: 6.0, deprecated: 100000.0, message: "Use Color(uiColor:) when converting a UIColor, or create a standard Color directly")
21+
@available(visionOS, introduced: 1.0, deprecated: 100000.0, message: "Use Color(uiColor:) when converting a UIColor, or create a standard Color directly")
1922
extension Color {
2023
/// Creates a color from a UIKit color.
2124
///
@@ -98,16 +101,19 @@ extension Color {
98101
}
99102
}
100103

104+
private let dynamicColorCache: NSMapTable<ObjcColor, UIColor> = NSMapTable.strongToWeakObjects()
105+
101106
extension UIColor: ColorProvider {
102-
private static var dynamicColorCache: NSMapTable<ObjcColor, UIColor> = NSMapTable.strongToWeakObjects()
103-
107+
@available(OpenSwiftUI_v2_0, *)
104108
@available(macOS, unavailable)
105109
convenience public init(_ color: Color) {
106-
if let cgColor = color.provider.staticColor {
110+
if let color = color.provider.as(UIColor.self) {
111+
self.init(color__openSwiftUI__: color)
112+
} else if let cgColor = color.provider.staticColor {
107113
self.init(cgColor: cgColor)
108114
} else {
109115
let objCColor = ObjcColor(color)
110-
let cache = Self.dynamicColorCache
116+
let cache = dynamicColorCache
111117
if let color = cache.object(forKey: objCColor) {
112118
self.init(color__openSwiftUI__: color)
113119
} else {

Sources/OpenSwiftUICore/Graphic/Color/Color.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,16 @@ package class AnyColorBox: AnyShapeStyleBox, @unchecked Sendable {
226226
package func opacity(at level: Int, environment: EnvironmentValues) -> Float { preconditionFailure("") }
227227
}
228228

229+
extension AnyColorBox { // 6.4.41
230+
package func `as`<P>(_ type: P.Type) -> P? where P: ColorProvider {
231+
if let p = self as? ColorBox<P> {
232+
return p.base
233+
} else {
234+
return nil
235+
}
236+
}
237+
}
238+
229239
@available(*, unavailable)
230240
extension AnyColorBox : Sendable {}
231241

Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN
2121
- (void)failedTest_openswiftui_safe_wrapper:(nullable NSString *)name withFailure:(nullable NSError*)failure OPENSWIFTUI_SWIFT_NAME(failedTest(_:withFailure:));
2222
@end
2323

24+
typedef OPENSWIFTUI_ENUM(NSInteger, NSViewVibrantBlendingStyle) {
25+
NSViewVibrantBlendingStyle_0 = 0,
26+
NSViewVibrantBlendingStyle_1 = 1,
27+
};
28+
2429
OPENSWIFTUI_ASSUME_NONNULL_END
2530

2631
#endif /* AppKit.h */

0 commit comments

Comments
 (0)