|
import SwiftUI |
|
|
|
// `.labelStyle(S)` also writes the private `\.effectiveLabelStyle` (only for the three built-in styles), which toolbars, |
|
// menus and PasteButton read. A custom style writes nil, so we replay the writer SwiftUI builds for each built-in. |
|
|
|
private protocol ModifiedContentProtocol { |
|
var anyModifier: Any { get } |
|
} |
|
|
|
extension ModifiedContent: ModifiedContentProtocol { |
|
fileprivate var anyModifier: Any { modifier } |
|
} |
|
|
|
private protocol EffectiveLabelStyleWriter { |
|
func isApplied(in environment: EnvironmentValues) -> Bool |
|
func apply(to environment: inout EnvironmentValues) |
|
} |
|
|
|
extension _EnvironmentKeyWritingModifier: EffectiveLabelStyleWriter where Value: Equatable { |
|
fileprivate func isApplied(in environment: EnvironmentValues) -> Bool { environment[keyPath: keyPath] == value } |
|
fileprivate func apply(to environment: inout EnvironmentValues) { environment[keyPath: keyPath] = value } |
|
} |
|
|
|
/// The environment writer inside `LabelStyleWritingModifier.body`, or nil if SwiftUI's internals changed. |
|
@MainActor private func effectiveLabelStyleWriter(of view: some View) -> (any EffectiveLabelStyleWriter)? { |
|
func body<M: ViewModifier>(of modifier: M) -> Any? { |
|
// `_ViewModifier_Content` is an empty placeholder with no public initializer. |
|
guard MemoryLayout<_ViewModifier_Content<M>>.size == 0 else { return nil } |
|
return modifier.body(content: unsafeBitCast((), to: _ViewModifier_Content<M>.self)) |
|
} |
|
guard let modifier = (view as? any ModifiedContentProtocol)?.anyModifier as? any ViewModifier, |
|
let body = body(of: modifier) as? any ModifiedContentProtocol |
|
else { return nil } |
|
return body.anyModifier as? any EffectiveLabelStyleWriter |
|
} |
|
|
|
@MainActor private let effectiveLabelStyleWriters: [DynamicLabelStyle.Kind?: any EffectiveLabelStyleWriter] = { |
|
// Explicit types: `.iconOnly` etc. could resolve to our `labelStyle(_: Kind?)` overload. |
|
let pairs: [(DynamicLabelStyle.Kind?, (any EffectiveLabelStyleWriter)?)] = [ |
|
(.titleAndIcon, effectiveLabelStyleWriter(of: Color.clear.labelStyle(TitleAndIconLabelStyle()))), |
|
(.iconOnly, effectiveLabelStyleWriter(of: Color.clear.labelStyle(IconOnlyLabelStyle()))), |
|
(.titleOnly, effectiveLabelStyleWriter(of: Color.clear.labelStyle(TitleOnlyLabelStyle()))), |
|
(nil, effectiveLabelStyleWriter(of: Color.clear.labelStyle(DefaultLabelStyle()))), |
|
] |
|
return Dictionary(uniqueKeysWithValues: pairs.compactMap { kind, writer in writer.map { (kind, $0) } }) |
|
}() |
|
|
|
extension EnvironmentValues { |
|
/// The built-in label style in effect, or nil for automatic or custom styles. |
|
@MainActor var effectiveLabelStyle: DynamicLabelStyle.Kind? { |
|
get { DynamicLabelStyle.Kind.allCases.first { effectiveLabelStyleWriters[$0]?.isApplied(in: self) == true } } |
|
set { effectiveLabelStyleWriters[newValue]?.apply(to: &self) } |
|
} |
|
} |
|
|
|
/// One style type for every kind, so switching doesn't change the view's type. |
|
struct DynamicLabelStyle: LabelStyle { |
|
enum Kind: CaseIterable { |
|
case titleAndIcon, iconOnly, titleOnly |
|
} |
|
|
|
/// nil means `.automatic`. |
|
var kind: Kind? |
|
|
|
func makeBody(configuration: Configuration) -> some View { |
|
MatchedIconLabel(configuration: configuration, kind: kind) |
|
} |
|
|
|
private struct MatchedIconLabel: View { |
|
let configuration: Configuration |
|
let kind: Kind? |
|
@Namespace private var namespace |
|
@Environment(\.isCircularWhenIconOnly) private var isCircular |
|
|
|
// A ViewBuilder switch, not AnyView, so the title fades instead of vanishing. |
|
var body: some View { |
|
switch kind { |
|
case .titleAndIcon: label.labelStyle(TitleAndIconLabelStyle()) |
|
case .iconOnly: |
|
label.labelStyle(IconOnlyLabelStyle()) |
|
.frame(minWidth: isCircular ? 0 : nil, maxWidth: isCircular ? .infinity : nil) |
|
case .titleOnly: label.labelStyle(TitleOnlyLabelStyle()) |
|
case nil: label.labelStyle(DefaultLabelStyle()) |
|
} |
|
} |
|
|
|
/// Shared icon geometry, so the icon slides instead of crossfading with a copy of itself. |
|
private var label: some View { |
|
Label { |
|
configuration.title |
|
} icon: { |
|
configuration.icon.matchedGeometryEffect(id: "icon", in: namespace) |
|
} |
|
} |
|
} |
|
} |
|
|
|
extension View { |
|
func labelStyle(_ kind: DynamicLabelStyle.Kind?) -> some View { |
|
// Inside `.labelStyle(...)` so it overrides the nil a custom style writes. |
|
environment(\.effectiveLabelStyle, kind) |
|
.labelStyle(DynamicLabelStyle(kind: kind)) |
|
} |
|
} |