Skip to content

Instantly share code, notes, and snippets.

@samhenrigold
Last active October 16, 2026 22:44
Show Gist options
  • Select an option

  • Save samhenrigold/1f68e7c9c3339eb10c39eedf767c43d7 to your computer and use it in GitHub Desktop.

Select an option

Save samhenrigold/1f68e7c9c3339eb10c39eedf767c43d7 to your computer and use it in GitHub Desktop.
SwiftUI: .labelStyle(condition ? .titleAndIcon : .iconOnly) using Apple's built-in styles, stable view identity, and the private effectiveLabelStyle so toolbars/menus/PasteButton behave

.labelStyle(condition ? .titleAndIcon : .iconOnly)

Button("Add", systemImage: "plus") { … }
    .labelStyle(isCompact ? .iconOnly : .titleAndIcon)
    .circularWhenIconOnly(isCompact)   // optional: animated capsule ⇄ circle
  • Renders with Apple's built-in label styles; nothing reimplemented.
  • Stable view identity: toggling never recreates the button.
  • Toolbars, menus and PasteButton behave like the built-ins. A custom LabelStyle normally breaks them because it doesn't set SwiftUI's private effectiveLabelStyle; this file sets it.
  • Icon slides, title fades, shape morphs.

Tested on iOS 18+. Uses private API; if SwiftUI changes it, it falls back to plain custom-style behavior instead of crashing.

Caveats: PasteButton can't update after creation (.id(kind) if needed). The label's title and icon views are recreated on toggle.

import SwiftUI
extension EnvironmentValues {
/// Read by `DynamicLabelStyle` to let an icon-only label shrink to the square below.
@Entry var isCircularWhenIconOnly = false
}
extension View {
/// A capsule button that becomes a circle while icon-only. Unlike `.buttonBorderShape(.circle)`, it animates.
/// Needs `.labelStyle(_: DynamicLabelStyle.Kind?)` on the label.
func circularWhenIconOnly(_ isIconOnly: Bool) -> some View {
SquareWhenActive(isActive: isIconOnly) {
environment(\.isCircularWhenIconOnly, isIconOnly)
.buttonBorderShape(.capsule)
}
}
}
/// While active, sizes its child to the child's natural height in both directions.
private struct SquareWhenActive: Layout {
var isActive: Bool
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard let child = subviews.first else { return .zero }
guard isActive else { return child.sizeThatFits(proposal) }
let side = child.sizeThatFits(.unspecified).height
return CGSize(width: side, height: side)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
subviews.first?.place(at: CGPoint(x: bounds.midX, y: bounds.midY), anchor: .center,
proposal: isActive ? ProposedViewSize(bounds.size) : proposal)
}
}
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))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment