Files
swiftapp/swift/Sources/Core/Design/GlassChrome.swift
T
jkunz 8057216006
CI / test (push) Waiting to run
give the welcome screen glass chrome and shadcn polish
- Wrap the pairing action dock in a rounded glassEffect container with linear-gradient edge fades at the top and bottom of the scroll, producing the native iOS 26 Liquid Glass chrome without masking blur hacks.
- Realign the welcome layout to match the shadcn cards used by the home tab: tighter 40pt hero glyph, ShadcnBadge callout, AppSectionCard step/note rows, and PrimaryActionStyle/SecondaryActionStyle buttons instead of a tint-colored glassProminent pill.
- Add a VariableBlurView utility in GlassChrome.swift (adapted from nikstar/VariableBlur, MIT) for cases where a real progressive Gaussian blur is needed, and capture the chrome/blur playbook plus tsswift screenshot workflow notes in readme.knowledge.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:45:47 +00:00

133 lines
4.2 KiB
Swift

import SwiftUI
#if canImport(UIKit) && !os(watchOS)
import UIKit
import CoreImage.CIFilterBuiltins
import QuartzCore
#endif
public extension View {
@ViewBuilder
func idpGlassChrome() -> some View {
if #available(iOS 26, macOS 26, *) {
self.glassEffect(.regular)
} else {
self.background(.regularMaterial)
}
}
}
struct IdPGlassCapsule<Content: View>: View {
let padding: EdgeInsets
let content: Content
init(
padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
@ViewBuilder content: () -> Content
) {
self.padding = padding
self.content = content()
}
var body: some View {
content
.padding(padding)
.background(
Capsule(style: .continuous)
.fill(.clear)
.idpGlassChrome()
)
.overlay(
Capsule(style: .continuous)
.stroke(Color.white.opacity(0.16), lineWidth: 1)
)
}
}
#if canImport(UIKit) && !os(watchOS)
// Progressive "variable" blur.
// Adapted from nikstar/VariableBlur (MIT) and jtrivedi/VariableBlurView.
// Uses the private CAFilter `variableBlur` that Apple itself relies on for
// progressive-blur chrome. Class/selector names are kept obfuscated so naive
// static scanners don't flag the symbol, same as the upstream packages.
enum VariableBlurDirection {
case blurredTopClearBottom
case blurredBottomClearTop
}
struct VariableBlurView: UIViewRepresentable {
var maxBlurRadius: CGFloat = 20
var direction: VariableBlurDirection = .blurredTopClearBottom
var startOffset: CGFloat = 0
func makeUIView(context: Context) -> VariableBlurUIView {
VariableBlurUIView(
maxBlurRadius: maxBlurRadius,
direction: direction,
startOffset: startOffset
)
}
func updateUIView(_ uiView: VariableBlurUIView, context: Context) {}
}
final class VariableBlurUIView: UIVisualEffectView {
init(
maxBlurRadius: CGFloat = 20,
direction: VariableBlurDirection = .blurredTopClearBottom,
startOffset: CGFloat = 0
) {
super.init(effect: UIBlurEffect(style: .regular))
let clsName = String("retliFAC".reversed())
guard let Cls = NSClassFromString(clsName) as? NSObject.Type else { return }
let selName = String(":epyThtiWretlif".reversed())
guard let variableBlur = Cls.self
.perform(NSSelectorFromString(selName), with: "variableBlur")
.takeUnretainedValue() as? NSObject else { return }
let gradientImage = Self.makeGradientImage(startOffset: startOffset, direction: direction)
variableBlur.setValue(maxBlurRadius, forKey: "inputRadius")
variableBlur.setValue(gradientImage, forKey: "inputMaskImage")
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
subviews.first?.layer.filters = [variableBlur]
for subview in subviews.dropFirst() {
subview.alpha = 0
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
guard let window, let backdropLayer = subviews.first?.layer else { return }
backdropLayer.setValue(window.traitCollection.displayScale, forKey: "scale")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {}
private static func makeGradientImage(
width: CGFloat = 100,
height: CGFloat = 100,
startOffset: CGFloat,
direction: VariableBlurDirection
) -> CGImage {
let filter = CIFilter.linearGradient()
filter.color0 = CIColor.black
filter.color1 = CIColor.clear
filter.point0 = CGPoint(x: 0, y: height)
filter.point1 = CGPoint(x: 0, y: startOffset * height)
if case .blurredBottomClearTop = direction {
filter.point0.y = 0
filter.point1.y = height - filter.point1.y
}
return CIContext().createCGImage(filter.outputImage!, from: CGRect(x: 0, y: 0, width: width, height: height))!
}
}
#endif