| name | liquid-glass |
| description | Implementa efeitos Liquid Glass (iOS 26+) com fallbacks corretos para versoes anteriores. Use APENAS quando o usuario pedir explicitamente para adotar Liquid Glass, glass effects, glassEffect modifier, ou o novo visual design do iOS 26. |
| argument-hint | card | toolbar | button | segmented | morphing | fallback |
Implemente efeitos Liquid Glass (iOS 26+) seguindo as diretrizes de design e as regras de API da Apple.
Regra obrigatoria: Adote Liquid Glass APENAS quando o usuario pedir explicitamente. Nunca converta UI existente para glass proativamente.
Argumentos
Tipo de componente: $ARGUMENTS
Se $ARGUMENTS estiver vazio, pergunte:
- "Qual componente precisa de Liquid Glass?"
card — superficie de card com glassEffect
toolbar — barra de botoes com glass
button — botao individual com glass
segmented — controle segmentado com glass + morphing
morphing — transicao entre dois elementos glass
fallback — helper reutilizavel com fallback automatico
Disponibilidade — regra absoluta
Todos os APIs de Liquid Glass requerem iOS 26+. Sempre forneca fallback.
if #available(iOS 26, *) {
} else {
}
Materiais de fallback (do mais proximo ao mais opaco):
| Material | Quando usar |
|---|
.ultraThinMaterial | Mais proximo ao glass — padrao para fallback |
.thinMaterial | Levemente mais opaco |
.regularMaterial | Opaco padrao |
.thickMaterial / .ultraThickMaterial | Fundo de alta cobertura |
Ordem de modificadores — regra critica
Text("Label")
.font(.headline)
.foregroundStyle(.primary)
.padding()
.glassEffect()
Text("Label")
.glassEffect()
.padding()
.font(.headline)
GlassEffectContainer — quando e obrigatorio
Glass nao pode amostrar outro glass. O material glass refrata luz de uma area maior que si mesmo. Dois elementos glass proximos sem container produziram resultados visuais inconsistentes.
GlassEffectContainer(spacing: 16) {
HStack(spacing: 16) {
Button("A") { }.glassEffect()
Button("B") { }.glassEffect()
}
}
HStack {
Button("A") { }.glassEffect()
Button("B") { }.glassEffect()
}
O parametro spacing: do container deve ser igual ao spacing do layout interno.
Opcao 1: card — Card com glass
struct GlassCard: View {
let title: String
let subtitle: String
var body: some View {
if #available(iOS 26, *) {
cardContent
.glassEffect(.regular, in: .rect(cornerRadius: 20))
} else {
cardContent
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
}
}
private var cardContent: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Variantes de estilo
.glassEffect(.regular, in: .rect(cornerRadius: 16))
.glassEffect(.prominent, in: .rect(cornerRadius: 16))
.glassEffect(.regular.tint(.blue.opacity(0.3)), in: .rect(cornerRadius: 16))
.glassEffect(in: .circle)
.glassEffect(in: .capsule)
.glassEffect(in: .rect(cornerRadius: 12))
Opcao 2: toolbar — Barra de botoes
struct GlassToolbar: View {
var body: some View {
if #available(iOS 26, *) {
GlassEffectContainer(spacing: 16) {
HStack(spacing: 16) {
ToolbarButton(icon: "pencil", action: { })
ToolbarButton(icon: "eraser", action: { })
ToolbarButton(icon: "scissors", action: { })
Spacer()
ToolbarButton(icon: "square.and.arrow.up", action: { })
}
.padding(.horizontal)
}
} else {
HStack(spacing: 16) {
ToolbarButton(icon: "pencil", action: { })
ToolbarButton(icon: "eraser", action: { })
ToolbarButton(icon: "scissors", action: { })
Spacer()
ToolbarButton(icon: "square.and.arrow.up", action: { })
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
}
}
}
struct ToolbarButton: View {
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title2)
.frame(width: 44, height: 44)
}
.glassEffect(.regular.interactive(), in: .circle)
}
}
Design note: Icons de toolbar usam rendering monocromatico por padrao no iOS 26. Use tint(_:) apenas para transmitir significado (ex: acao destrutiva), nunca por efeito visual.
Opcao 3: button — Botao com glass
Button("Acao") { }
.buttonStyle(.glass)
Button("Acao Principal") { }
.buttonStyle(.glassProminent)
Button(action: { }) {
Label("Configuracoes", systemImage: "gear")
.padding(.horizontal, 20)
.padding(.vertical, 12)
}
.glassEffect(.regular.interactive(), in: .capsule)
Button(action: { }) {
Label("Compartilhar", systemImage: "square.and.arrow.up")
.padding(.horizontal, 20)
.padding(.vertical, 12)
}
.modifier(GlassButtonModifier())
struct GlassButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content.glassEffect(.regular.interactive(), in: .capsule)
} else {
content
.background(.ultraThinMaterial, in: Capsule())
.overlay(Capsule().strokeBorder(.secondary.opacity(0.2)))
}
}
}
.interactive() apenas em elementos que respondem a input do usuario (botoes, views tapeaveis, elementos focaveis). Nunca em conteudo estatico.
Opcao 4: segmented — Controle segmentado com morphing
struct GlassSegmentedControl: View {
@Binding var selection: Int
let options: [String]
@Namespace private var animation
var body: some View {
if #available(iOS 26, *) {
GlassEffectContainer(spacing: 4) {
HStack(spacing: 4) {
ForEach(options.indices, id: \.self) { index in
Button(options[index]) {
withAnimation(.smooth) {
selection = index
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.glassEffect(
selection == index ? .prominent.interactive() : .regular.interactive(),
in: .capsule
)
.glassEffectID(
selection == index ? "selected" : "option\(index)",
in: animation
)
}
}
.padding(4)
}
} else {
Picker("", selection: $selection) {
ForEach(options.indices, id: \.self) { index in
Text(options[index]).tag(index)
}
}
.pickerStyle(.segmented)
}
}
}
Opcao 5: morphing — Transicao entre elementos glass
Use glassEffectID + @Namespace para animar a transicao entre dois estados de um elemento glass.
struct MorphingCard: View {
@Namespace private var glassNamespace
@State private var isExpanded = false
var body: some View {
GlassEffectContainer {
if isExpanded {
ExpandedView()
.glassEffect(.regular, in: .rect(cornerRadius: 20))
.glassEffectID("card", in: glassNamespace)
} else {
CompactView()
.glassEffect(.regular, in: .rect(cornerRadius: 12))
.glassEffectID("card", in: glassNamespace)
}
}
.animation(.smooth, value: isExpanded)
.onTapGesture {
isExpanded.toggle()
}
}
}
Requisitos obrigatorios para morphing
- Ambas as views devem ter o mesmo
glassEffectID
- O mesmo
@Namespace em ambas
- Envolvidas em
GlassEffectContainer
- Animacao aplicada no container ou view pai
Opcao 6: fallback — Helper reutilizavel
Para aplicar glass com fallback em qualquer view do projeto:
extension View {
@ViewBuilder
func glassEffectWithFallback(
cornerRadius: CGFloat = 16,
prominent: Bool = false,
interactive: Bool = false
) -> some View {
if #available(iOS 26, *) {
let style: GlassEffectStyle = prominent
? (interactive ? .prominent.interactive() : .prominent)
: (interactive ? .regular.interactive() : .regular)
self.glassEffect(style, in: .rect(cornerRadius: cornerRadius))
} else {
self.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius)
)
}
}
}
Text("Conteudo")
.padding()
.glassEffectWithFallback(cornerRadius: 20, interactive: true)
Notas de design do iOS 26
Sheets
Sheets de altura parcial usam background Liquid Glass por padrao. Remova presentationBackground(_:) customizado para deixar o material padrao aparecer:
.sheet(isPresented: $showSheet) {
SheetContent()
}
Scroll edge effect
O iOS 26 aplica automaticamente blur/fade sob toolbars do sistema. Remova qualquer background escurecido customizado atras de bar items — ele vai conflitar com o efeito automatico.
Checklist de Revisao