Skip to content

Commit 591522f

Browse files
committed
Add HierarchicalPalette module and demo
Introduce a new HierarchicalPalette feature with types for semantic hierarchical colors and runtime configuration: HierarchicalPaletteShapeStyle, HierarchicalPaletteLevel, HierarchicalPaletteConfiguration, HierarchicalPaletteCompensation, and introspection utilities. Add lightweight Logger extension for diagnostics and a demo app screen/styles to preview canonical vs hierarchical-rendered swatches. Update Package.swift to depend on swift-log and expose the Logging product. Also mark two safeAreaInsets overloads with @_disfavoredOverload in InternalAPI/View+SafeAreaInsets.swift. Improve hierarchical palette logging and introspection Add a reusable child(named:in:) mirror helper and consolidate duplicated code. Make classifyOperation more robust when detecting copyStyle foreground vs other variants. Replace print-based logging with Logger.hierarchicalPalette.debug and change hierarchicalPalette from a static let to a computed static var to avoid retaining a pre-bootstrap logger instance. Also allow prepareText alongside resolveStyle when computing alpha policy.
1 parent 53a891c commit 591522f

11 files changed

Lines changed: 664 additions & 3 deletions

Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ let package = Package(
1111
.library(name: "SwiftUIHelpers", targets: ["SwiftUIHelpers"]),
1212
],
1313
dependencies: [
14-
.package(url: "https://github.com/stalkermv/SwiftHelpers.git", from: "1.0.0")
14+
.package(url: "https://github.com/stalkermv/SwiftHelpers.git", from: "1.0.0"),
15+
.package(url: "https://github.com/apple/swift-log", from: "1.10.0")
1516
],
1617
targets: [
1718
// Targets are the basic building blocks of a package, defining a module or a test suite.
@@ -20,7 +21,8 @@ let package = Package(
2021
name: "SwiftUIHelpers",
2122
dependencies: [
2223
"SwiftUIExtensions",
23-
.product(name: "SwiftHelpers", package: "SwiftHelpers")
24+
.product(name: "SwiftHelpers", package: "SwiftHelpers"),
25+
.product(name: "Logging", package: "swift-log")
2426
]
2527
),
2628
.target(
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import SwiftUI
2+
3+
@available(iOS 17.0, *)
4+
struct HierarchicalPaletteDemoScreen: View {
5+
var body: some View {
6+
ScrollView {
7+
VStack(alignment: .leading, spacing: 24) {
8+
HierarchicalPaletteDemoSection(
9+
title: "Label",
10+
rows: HierarchicalPaletteDemoCatalog.labelRows
11+
)
12+
13+
HierarchicalPaletteDemoSection(
14+
title: "Accent",
15+
rows: HierarchicalPaletteDemoCatalog.accentRows
16+
)
17+
18+
HierarchicalPaletteDemoSection(
19+
title: "Background",
20+
rows: HierarchicalPaletteDemoCatalog.backgroundRows
21+
)
22+
}
23+
.padding(16)
24+
}
25+
.navigationTitle("Hierarchical Palette")
26+
.background(Color.black.ignoresSafeArea())
27+
}
28+
}
29+
30+
private struct HierarchicalPaletteDemoSection: View {
31+
let title: String
32+
let rows: [HierarchicalPaletteDemoRow]
33+
34+
var body: some View {
35+
VStack(alignment: .leading, spacing: 12) {
36+
Text(title)
37+
.font(.headline)
38+
.foregroundStyle(.white)
39+
40+
ForEach(rows) { row in
41+
HierarchicalPaletteDemoComparisonRow(row: row)
42+
}
43+
}
44+
}
45+
}
46+
47+
private struct HierarchicalPaletteDemoComparisonRow: View {
48+
let row: HierarchicalPaletteDemoRow
49+
50+
var body: some View {
51+
VStack(alignment: .leading, spacing: 10) {
52+
Text(row.title)
53+
.font(.subheadline.weight(.semibold))
54+
.foregroundStyle(.white)
55+
56+
HStack(spacing: 12) {
57+
HierarchicalPaletteDemoColumn(
58+
title: "Canonical",
59+
hex: row.hex,
60+
style: AnyShapeStyle(Color.demoScreenHex(row.hex))
61+
)
62+
63+
HierarchicalPaletteDemoColumn(
64+
title: "Hierarchical Palette",
65+
hex: row.hex,
66+
style: row.style
67+
)
68+
}
69+
}
70+
.padding(12)
71+
.background(
72+
RoundedRectangle(cornerRadius: 12, style: .continuous)
73+
.fill(Color.white.opacity(0.06))
74+
)
75+
}
76+
}
77+
78+
private struct HierarchicalPaletteDemoColumn: View {
79+
let title: String
80+
let hex: String
81+
let style: AnyShapeStyle
82+
83+
var body: some View {
84+
VStack(alignment: .leading, spacing: 8) {
85+
Text(title)
86+
.font(.caption.weight(.semibold))
87+
.foregroundStyle(.white.opacity(0.9))
88+
89+
Text(hex)
90+
.font(.caption2.monospaced())
91+
.foregroundStyle(.white.opacity(0.7))
92+
93+
HierarchicalPaletteDemoSwatch(background: .black, style: style)
94+
HierarchicalPaletteDemoSwatch(background: .red, style: style)
95+
}
96+
.frame(maxWidth: .infinity, alignment: .leading)
97+
}
98+
}
99+
100+
private struct HierarchicalPaletteDemoSwatch: View {
101+
let background: Color
102+
let style: AnyShapeStyle
103+
104+
var body: some View {
105+
ZStack {
106+
RoundedRectangle(cornerRadius: 8, style: .continuous)
107+
.fill(background)
108+
109+
RoundedRectangle(cornerRadius: 8, style: .continuous)
110+
.fill(style)
111+
.padding(6)
112+
}
113+
.frame(height: 34)
114+
}
115+
}
116+
117+
#Preview("Visual Exact") {
118+
if #available(iOS 17.0, *) {
119+
NavigationStack {
120+
HierarchicalPaletteDemoScreen()
121+
}
122+
.hierarchicalPaletteConfiguration(.default)
123+
}
124+
}
125+
126+
#Preview("Native") {
127+
if #available(iOS 17.0, *) {
128+
NavigationStack {
129+
HierarchicalPaletteDemoScreen()
130+
}
131+
.hierarchicalPaletteConfiguration(
132+
.init(
133+
alphaPolicy: .native,
134+
loggingEnabled: false
135+
)
136+
)
137+
}
138+
}
139+
140+
private extension Color {
141+
static func demoScreenHex(_ hex: String) -> Color {
142+
let normalized = hex
143+
.trimmingCharacters(in: .whitespacesAndNewlines)
144+
.replacingOccurrences(of: "#", with: "")
145+
146+
guard normalized.count == 6, let value = UInt64(normalized, radix: 16) else {
147+
return .clear
148+
}
149+
150+
let red = Double((value >> 16) & 0xFF) / 255.0
151+
let green = Double((value >> 8) & 0xFF) / 255.0
152+
let blue = Double(value & 0xFF) / 255.0
153+
154+
return Color(red: red, green: green, blue: blue)
155+
}
156+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import SwiftUI
2+
3+
struct HierarchicalPaletteDemoRow: Identifiable {
4+
let title: String
5+
let hex: String
6+
let style: AnyShapeStyle
7+
8+
var id: String { title }
9+
}
10+
11+
12+
@available(iOS 17.0, *)
13+
enum HierarchicalPaletteDemoCatalog {
14+
static let labelRows: [HierarchicalPaletteDemoRow] = [
15+
.init(title: ".demoLabel", hex: DemoPalette.label.primary, style: AnyShapeStyle(DemoLabelShapeStyle())),
16+
.init(title: ".demoLabel.secondary", hex: DemoPalette.label.secondary, style: AnyShapeStyle(DemoLabelShapeStyle().secondary)),
17+
.init(title: ".demoLabel.tertiary", hex: DemoPalette.label.tertiary, style: AnyShapeStyle(DemoLabelShapeStyle().tertiary)),
18+
.init(title: ".demoLabel.quaternary", hex: DemoPalette.label.quaternary, style: AnyShapeStyle(DemoLabelShapeStyle().quaternary)),
19+
]
20+
21+
static let accentRows: [HierarchicalPaletteDemoRow] = [
22+
.init(title: ".demoAccent", hex: DemoPalette.accent.primary, style: AnyShapeStyle(DemoAccentShapeStyle())),
23+
.init(title: ".demoAccent.secondary", hex: DemoPalette.accent.secondary, style: AnyShapeStyle(DemoAccentShapeStyle().secondary)),
24+
.init(title: ".demoAccent.tertiary", hex: DemoPalette.accent.tertiary, style: AnyShapeStyle(DemoAccentShapeStyle().tertiary)),
25+
.init(title: ".demoAccent.quaternary", hex: DemoPalette.accent.quaternary, style: AnyShapeStyle(DemoAccentShapeStyle().quaternary)),
26+
]
27+
28+
static let backgroundRows: [HierarchicalPaletteDemoRow] = [
29+
.init(title: ".demoBackground", hex: DemoPalette.background.primary, style: AnyShapeStyle(DemoBackgroundShapeStyle())),
30+
.init(title: ".demoBackground.secondary", hex: DemoPalette.background.secondary, style: AnyShapeStyle(DemoBackgroundShapeStyle().secondary)),
31+
.init(title: ".demoBackground.tertiary", hex: DemoPalette.background.tertiary, style: AnyShapeStyle(DemoBackgroundShapeStyle().tertiary)),
32+
.init(title: ".demoBackground.quaternary", hex: DemoPalette.background.quaternary, style: AnyShapeStyle(DemoBackgroundShapeStyle().quaternary)),
33+
]
34+
}
35+
36+
private enum DemoPalette {
37+
enum background {
38+
static let primary = "#0A0A0A"
39+
static let secondary = "#101010"
40+
static let tertiary = "#1C1C1E"
41+
static let quaternary = "#1C1C1E"
42+
}
43+
44+
enum label {
45+
static let primary = "#FFFFFF"
46+
static let secondary = "#8B8F9B"
47+
static let tertiary = "#727272"
48+
static let quaternary = "#A7A9AC"
49+
}
50+
51+
enum accent {
52+
static let primary = "#8460BD"
53+
static let secondary = "#7D6EFF"
54+
static let tertiary = "#6D98FF"
55+
static let quaternary = "#A1ACFF"
56+
}
57+
}
58+
59+
private struct DemoBackgroundShapeStyle: HierarchicalPaletteShapeStyle {
60+
func resolve(in environment: EnvironmentValues, for level: HierarchicalPaletteLevel) -> Color {
61+
switch level {
62+
case .primary:
63+
return .demoHex(DemoPalette.background.primary)
64+
case .secondary:
65+
return .demoHex(DemoPalette.background.secondary)
66+
case .tertiary, .quaternary, .quinary:
67+
return .demoHex(DemoPalette.background.quaternary)
68+
}
69+
}
70+
}
71+
72+
private struct DemoLabelShapeStyle: HierarchicalPaletteShapeStyle {
73+
func resolve(in environment: EnvironmentValues, for level: HierarchicalPaletteLevel) -> Color {
74+
switch level {
75+
case .primary:
76+
return .demoHex(DemoPalette.label.primary)
77+
case .secondary:
78+
return .demoHex(DemoPalette.label.secondary)
79+
case .tertiary:
80+
return .demoHex(DemoPalette.label.tertiary)
81+
case .quaternary, .quinary:
82+
return .demoHex(DemoPalette.label.quaternary)
83+
}
84+
}
85+
}
86+
87+
private struct DemoAccentShapeStyle: HierarchicalPaletteShapeStyle {
88+
func resolve(in environment: EnvironmentValues, for level: HierarchicalPaletteLevel) -> Color {
89+
switch level {
90+
case .primary:
91+
return .demoHex(DemoPalette.accent.primary)
92+
case .secondary:
93+
return .demoHex(DemoPalette.accent.secondary)
94+
case .tertiary:
95+
return .demoHex(DemoPalette.accent.tertiary)
96+
case .quaternary, .quinary:
97+
return .demoHex(DemoPalette.accent.quaternary)
98+
}
99+
}
100+
}
101+
102+
private extension Color {
103+
static func demoHex(_ hex: String) -> Color {
104+
let normalized = hex.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "#", with: "")
105+
guard normalized.count == 6, let value = UInt64(normalized, radix: 16) else {
106+
return .clear
107+
}
108+
109+
let red = Double((value >> 16) & 0xFF) / 255.0
110+
let green = Double((value >> 8) & 0xFF) / 255.0
111+
let blue = Double(value & 0xFF) / 255.0
112+
113+
return Color(red: red, green: green, blue: blue)
114+
}
115+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import Foundation
2+
3+
/// Multipliers used to compensate SwiftUI hierarchical attenuation for non-primary levels.
4+
///
5+
/// Use this type with ``HierarchicalPaletteAlphaPolicy/visualExact(_:)`` when you need the
6+
/// final on-screen color to visually match canonical design tokens.
7+
public struct HierarchicalPaletteCompensation: Sendable, Equatable {
8+
/// Compensation multiplier for ``HierarchicalPaletteLevel/secondary``.
9+
public let secondary: Double
10+
11+
/// Compensation multiplier for ``HierarchicalPaletteLevel/tertiary``.
12+
public let tertiary: Double
13+
14+
/// Compensation multiplier for ``HierarchicalPaletteLevel/quaternary``.
15+
public let quaternary: Double
16+
17+
/// Compensation multiplier for ``HierarchicalPaletteLevel/quinary``.
18+
public let quinary: Double
19+
20+
/// Creates a compensation profile.
21+
/// - Parameters:
22+
/// - secondary: Multiplier for secondary level.
23+
/// - tertiary: Multiplier for tertiary level.
24+
/// - quaternary: Multiplier for quaternary level.
25+
/// - quinary: Multiplier for quinary level.
26+
public init(
27+
secondary: Double,
28+
tertiary: Double,
29+
quaternary: Double,
30+
quinary: Double
31+
) {
32+
self.secondary = secondary
33+
self.tertiary = tertiary
34+
self.quaternary = quaternary
35+
self.quinary = quinary
36+
}
37+
38+
/// Returns a multiplier for the provided level.
39+
///
40+
/// Primary uses `1.0` by design and should not be compensated.
41+
/// - Parameter level: Semantic hierarchy level.
42+
public func multiplier(for level: HierarchicalPaletteLevel) -> Double {
43+
switch level {
44+
case .primary:
45+
return 1.0
46+
case .secondary:
47+
return secondary
48+
case .tertiary:
49+
return tertiary
50+
case .quaternary:
51+
return quaternary
52+
case .quinary:
53+
return quinary
54+
}
55+
}
56+
}
57+
58+
public extension HierarchicalPaletteCompensation {
59+
/// Default compensation values measured against the current SwiftUI runtime.
60+
///
61+
/// Default values are:
62+
/// - secondary: `255 / 128` (`1.9921875`)
63+
/// - tertiary: `255 / 64` (`3.984375`)
64+
/// - quaternary: `255 / 46` (`5.5434782609`)
65+
/// - quinary: `255 / 46` (`5.5434782609`)
66+
///
67+
/// Portability note:
68+
/// SwiftUI internals can evolve between major OS versions, so these values may require
69+
/// re-calibration in future runtimes.
70+
static let `default` = HierarchicalPaletteCompensation(
71+
secondary: 255.0 / 128.0,
72+
tertiary: 255.0 / 64.0,
73+
quaternary: 255.0 / 46.0,
74+
quinary: 255.0 / 46.0
75+
)
76+
}

0 commit comments

Comments
 (0)