feat(radial): rebuild glass bubble menu on native Liquid Glass
글래스 버블 메뉴를 시스템 리퀴드 글라스(glassEffect) 기반으로 재구축. 컨셉(FAB → 부채꼴 유리 구슬, 엄지 반경 ~150pt, 바깥 탭 닫기, 순서 설정)은 그대로 두고 유리 질감·연출·성능을 올렸다. 유리: material 흉내(ultraThinMaterial + 그라데이션 + 테두리) 대신 .glassEffect(.regular.interactive()) — 배경 굴절·빛 반사·눌림 시머까지 시스템이 그린다. 현재 탭은 초록 틴트 유리. 연출: 2단계 — 삽입 순간 FAB 유리에서 모핑으로 부풀어 나온 뒤(bloomed) 스태거 바운스 스프링으로 아치에 흩어진다. GlassEffectContainer 근접 블렌딩으로 FAB를 벗어날 때 유리가 늘어나다 물방울처럼 분리. 닫힘은 지연 없이 흡수. 그라데이션 딤 + 메뉴 뒤 초록 빛 무리 + 햅틱. 성능(버벅임): 예전엔 블러 14장(버블 7 + 라벨 알약 7) + 그림자 14개가 각각 애니메이션됐다. 라벨을 구슬 안에 넣어 유리 요소를 8개로 줄이고 GlassEffectContainer가 한 패스로 일괄 렌더링, 커스텀 그림자 제거. 주의점 기록: 컨테이너 안 유리 요소는 glassEffect 뒤 opacity/scale이 콘텐츠에 온전히 적용되지 않아(접힌 상태 내용물 비침) 숨김은 계층 제거(if expanded), 이동은 offset으로만 처리. 검증: 접힘·펼침(라이트/다크) 스크린샷으로 유리 굴절·라벨 가독성· 콘텐츠 누출 없음 확인. Debug·Store 스킴 빌드 성공. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
a9473be937
commit
1b36607ba1
@ -140,7 +140,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
|
||||
- **스플래시**: 앱 로고(AppLogo) + 이름 + 멘트, 1.2초 후 페이드아웃 (`ContentView`)
|
||||
- **탭 정의** (`AppTab`): 모음(main) · 행동 · 꼬리표 · 목표 · 기록 · 통계 · 일기(iPad 전용) · 설정
|
||||
- **iPhone 기본**: 하단 탭바에 **노출 탭 1~3개**(기본 모음·행동·기록) + "더보기" 탭(나머지 목록). 설정에서 구성
|
||||
- **iPhone 실험 옵션**: "글래스 버블 메뉴"(`RadialNavigationView`) — 하단 중앙 FAB를 누르면 유리 구슬 탭 버튼들이 부채꼴로 펼쳐짐. 라벨은 글라스 알약 배경. 순서는 설정에서 편집(기기 로컬)
|
||||
- **iPhone 실험 옵션**: "글래스 버블 메뉴"(`RadialNavigationView`) — 하단 중앙 FAB를 누르면 리퀴드 글라스(`glassEffect(.regular.interactive())`) 유리 구슬 탭 버튼들이 부채꼴로 펼쳐짐. 아이콘+이름이 구슬 안에 함께. 모든 유리 요소는 하나의 `GlassEffectContainer`에서 한 패스로 렌더링(성능)되고 FAB 근처에서 액체처럼 분리·합체 모핑. 2단계 연출: 삽입(FAB에서 모핑) → bloomed(스태거 스프링으로 아치 확산). ⚠️ 컨테이너 안 유리 요소에는 glassEffect 뒤 opacity/scale이 콘텐츠에 온전히 적용되지 않음 — 숨김은 계층 제거(`if`), 이동은 `offset`으로만. 순서는 설정에서 편집(기기 로컬)
|
||||
- **iPad·Mac**: 항상 `NavigationSplitView` 사이드바 (`SidebarRootView`, `DeviceLayout.isPad`로 판정)
|
||||
- `AppRouter`: 탭 전환 + 탭 간 전달값(모음 탭 롱프레스 → 기록/통계 탭 행동 필터 예약, 더보기 내부 경로 진입 포함)
|
||||
|
||||
|
||||
@ -10,12 +10,17 @@
|
||||
// - 그 위에 항상 최상단(ZStack 마지막 자식)으로 떠 있는 메뉴 오버레이를 얹는다.
|
||||
// → 모음 탭의 행동 그리드가 아무리 길게 스크롤돼도 FAB/메뉴는 스크롤·터치에 간섭받지 않는다.
|
||||
//
|
||||
// 메뉴 UI: '글래스 버블(Glass Bubbles)'.
|
||||
// - FAB를 누르면 리퀴드 글라스 질감의 유리 구슬(버블) 탭 버튼들이 FAB에서 부채꼴 아치로
|
||||
// 회전하며 퍼져 나온다. 스태거(순차) 스프링으로 물방울이 튀듯 경쾌하게 등장.
|
||||
// 메뉴 UI: '글래스 버블(Glass Bubbles)' — 시스템 리퀴드 글라스(glassEffect) 기반.
|
||||
// - FAB를 누르면 진짜 유리 재질(.regular.interactive — 배경 굴절·빛 반사·눌림 시머까지
|
||||
// 시스템이 렌더링)의 구슬 버튼들이 FAB에서 부채꼴 아치로 회전하며 퍼져 나온다.
|
||||
// - 모든 유리 요소를 하나의 GlassEffectContainer에 담아 한 패스로 그린다: 예전의
|
||||
// material 블러 14장(버블 7 + 라벨 알약 7) + 그림자 14개 개별 애니메이션이 버벅임의
|
||||
// 원인이었다. 라벨을 구슬 안에 넣어 유리 요소를 8개(버블 7 + FAB)로 줄였고, 그림자는
|
||||
// 유리 자체 깊이감에 맡긴다. 컨테이너 덕분에 버블이 FAB 근처를 지날 때 유리가
|
||||
// 액체처럼 늘어나며 분리·합체되는 모핑이 함께 생긴다.
|
||||
// - 등장: 스태거(순차) 바운스 스프링 + 스월 회전. 닫힘: 지연 없이 FAB로 스냅 흡수.
|
||||
// 펼치면 메뉴 뒤로 은은한 초록 빛 무리(정적 그라데이션이라 렌더 비용 없음)가 퍼진다.
|
||||
// - 아치 반경이 엄지 반경(~150pt) 안이라 세로 스택과 달리 모든 탭이 한 손에 닿는다.
|
||||
// - 각 버블: ultraThinMaterial + 상단 화이트 그라데이션(빛 반사) + 얇은 흰 테두리 + 그림자.
|
||||
// 애니메이션은 offset·scale·opacity·rotation 조합(트랜스폼 전용)이라 블러 재계산이 없어 가볍다.
|
||||
//
|
||||
// iPhone(Compact) 전용 — iPad·Mac은 SidebarRootView, watch는 별도 타깃이라 이 파일과 무관하다.
|
||||
//
|
||||
@ -75,15 +80,26 @@ struct GlassBubbleMenu: View {
|
||||
/// 사용자가 설정에서 지정한 버블 배치 순서 (기기 로컬, 비동기화)
|
||||
@AppStorage(SettingsKeys.radialTabOrder) private var radialOrderRaw = ""
|
||||
|
||||
/// 리퀴드 글라스 모핑 네임스페이스 — FAB와 버블이 같은 유리 덩어리로 이어지게 한다
|
||||
@Namespace private var glassSpace
|
||||
|
||||
/// 2단계 연출의 두 번째 상태: 버블이 FAB에서 아치 위 자기 자리로 퍼져 나갔는지.
|
||||
/// expanded(삽입·모핑)와 분리한 이유 — GlassEffectContainer 안의 유리 요소는
|
||||
/// glassEffect 뒤의 opacity/scale이 콘텐츠에 온전히 적용되지 않아(접힌 상태에서
|
||||
/// 내용물이 비쳐 보이는 문제), 숨김은 계층 제거(if expanded)로, 이동은 검증된
|
||||
/// offset 애니메이션(bloomed)으로만 처리한다.
|
||||
@State private var bloomed = false
|
||||
|
||||
// 레이아웃 상수
|
||||
private let fabSize: CGFloat = 62
|
||||
private let bubbleSize: CGFloat = 56
|
||||
private let fabSize: CGFloat = 64
|
||||
private let bubbleSize: CGFloat = 58
|
||||
/// FAB 중심 → 버블 중심 거리. 엄지가 자연스럽게 닿는 반경 안으로 유지한다.
|
||||
private let desiredRadius: CGFloat = 152
|
||||
private let bottomInset: CGFloat = 42
|
||||
private let edgePadding: CGFloat = 16
|
||||
/// 버블 중심에서 탭 이름 라벨 중심까지의 거리(버블 아래로)
|
||||
private let labelOffset: CGFloat = 42
|
||||
private let edgePadding: CGFloat = 14
|
||||
/// 유리가 액체처럼 합쳐지기 시작하는 거리. 정지 상태의 이웃 버블 간격(~20pt)보다
|
||||
/// 살짝 작게 잡아, 아치에 자리 잡으면 또렷이 분리되고 FAB 근처를 지날 때만 모핑한다.
|
||||
private let glassBlendSpacing: CGFloat = 18
|
||||
|
||||
/// 왼쪽 끝 → 위 → 오른쪽 끝 순으로 배치할 탭 (설정의 커스텀 순서를 따른다)
|
||||
private var tabs: [AppTab] { AppTab.radialOrder(from: radialOrderRaw) }
|
||||
@ -98,19 +114,22 @@ struct GlassBubbleMenu: View {
|
||||
let radius = min(desiredRadius, geo.size.width / 2 - bubbleSize / 2 - edgePadding)
|
||||
|
||||
ZStack {
|
||||
// 바깥 영역 탭 → 닫기. 펼쳐졌을 때만 화면을 덮어 히트테스트를 받는다.
|
||||
Color.black
|
||||
.opacity(expanded ? 0.22 : 0)
|
||||
.ignoresSafeArea()
|
||||
.allowsHitTesting(expanded)
|
||||
.onTapGesture { close() }
|
||||
dimLayer
|
||||
|
||||
// FAB(하단 중앙)를 기준점으로 버블들이 부채꼴로 퍼진다.
|
||||
// FAB(하단 중앙)를 기준점으로 빛 무리와 버블들이 부채꼴로 퍼진다.
|
||||
ZStack {
|
||||
ForEach(Array(tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
bubble(tab, index: index, count: tabs.count, radius: radius)
|
||||
glow(radius: radius)
|
||||
// 모든 유리 요소를 한 컨테이너에 담아 한 패스로 렌더링 + 근접 시 액체 모핑
|
||||
GlassEffectContainer(spacing: glassBlendSpacing) {
|
||||
ZStack {
|
||||
if expanded {
|
||||
ForEach(Array(tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
bubble(tab, index: index, count: tabs.count, radius: radius)
|
||||
}
|
||||
}
|
||||
fab
|
||||
}
|
||||
}
|
||||
fab
|
||||
}
|
||||
.frame(width: fabSize, height: fabSize)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
@ -118,12 +137,50 @@ struct GlassBubbleMenu: View {
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.sensoryFeedback(.impact(weight: .light), trigger: expanded)
|
||||
.onAppear {
|
||||
// -radialExpanded 등으로 펼쳐진 채 시작하면 버블도 자리 잡은 상태로
|
||||
if expanded { bloomed = true }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: 배경 레이어
|
||||
|
||||
/// 바깥 영역 탭 → 닫기. 아래쪽(메뉴 주변)이 더 어두운 그라데이션 딤으로
|
||||
/// 유리 구슬의 굴절이 도드라지게 한다. 펼쳐졌을 때만 히트테스트를 받는다.
|
||||
private var dimLayer: some View {
|
||||
LinearGradient(
|
||||
colors: [.black.opacity(0.06), .black.opacity(0.32)],
|
||||
startPoint: .top, endPoint: .bottom
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
.opacity(expanded ? 1 : 0)
|
||||
.animation(.easeOut(duration: 0.22), value: expanded)
|
||||
.allowsHitTesting(expanded)
|
||||
.onTapGesture { close() }
|
||||
}
|
||||
|
||||
/// 펼쳤을 때 메뉴 뒤로 은은히 퍼지는 초록 빛 무리 — 유리가 빛을 머금은 느낌.
|
||||
/// 정적 그라데이션 한 장이라 렌더 비용이 사실상 없다.
|
||||
private func glow(radius: CGFloat) -> some View {
|
||||
RadialGradient(
|
||||
colors: [AppTheme.green.opacity(0.22), AppTheme.green.opacity(0.05), .clear],
|
||||
center: .center, startRadius: 0, endRadius: radius * 1.3
|
||||
)
|
||||
.frame(width: radius * 2.6, height: radius * 2.6)
|
||||
.scaleEffect(expanded ? 1 : 0.15)
|
||||
.opacity(expanded ? 1 : 0)
|
||||
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: expanded)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
// MARK: 유리 구슬 버튼
|
||||
|
||||
/// 탭 하나를 리퀴드 글라스 구슬로 렌더링. FAB 위치에서 아치 위 자기 자리로
|
||||
/// 회전하며 튀어 나가고, 스태거 딜레이로 물방울이 연달아 튀는 느낌을 준다.
|
||||
/// 탭 하나를 리퀴드 글라스 구슬로 렌더링 — 아이콘과 이름이 한 구슬 안에 있어
|
||||
/// 라벨 알약이 따로 필요 없고 탭 타깃도 하나로 명확하다.
|
||||
/// 연출: 삽입 순간 FAB 유리에서 모핑으로 부풀어 나온 뒤(offset 0 = FAB와 한 덩어리),
|
||||
/// bloomed가 켜지면 스태거 스프링으로 자기 자리로 퍼진다. 블렌드 거리를 벗어나는
|
||||
/// 순간 유리가 늘어나다 물방울처럼 똑 떨어져 나온다.
|
||||
private func bubble(_ tab: AppTab, index: Int, count: Int, radius: CGFloat) -> some View {
|
||||
// 왼쪽(180°) → 위(90°) → 오른쪽(0°)으로 이어지는 위쪽 반원에 균등 배치
|
||||
let t = count > 1 ? Double(index) / Double(count - 1) : 0.5
|
||||
@ -135,64 +192,42 @@ struct GlassBubbleMenu: View {
|
||||
return Button {
|
||||
select(tab)
|
||||
} label: {
|
||||
ZStack {
|
||||
// 유리 구슬 본체
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
// 위에서 빛을 받는 유리의 반사광
|
||||
Circle().fill(
|
||||
LinearGradient(
|
||||
colors: [.white.opacity(0.35), .white.opacity(0.03)],
|
||||
startPoint: .top, endPoint: .bottom
|
||||
)
|
||||
)
|
||||
)
|
||||
.overlay(
|
||||
// 현재 탭은 초록 유리로 강조
|
||||
Circle().fill(AppTheme.green.opacity(isCurrent ? 0.82 : 0))
|
||||
)
|
||||
.overlay(Circle().strokeBorder(.white.opacity(0.4), lineWidth: 1))
|
||||
Image(systemName: tab.symbol)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundStyle(isCurrent ? AnyShapeStyle(.white) : AnyShapeStyle(.primary))
|
||||
}
|
||||
.frame(width: bubbleSize, height: bubbleSize)
|
||||
.shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4)
|
||||
|
||||
// 탭 이름 라벨 — 구슬과 같은 유리 소재의 알약 배경으로 어떤 배경 위에서도
|
||||
// 읽히게 한다 (맨 글자는 라이트 모드 밝은 화면에서 안 보였음)
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: tab.symbol)
|
||||
.font(.system(size: 19, weight: .semibold))
|
||||
Text(tab.label)
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.font(.system(size: 9.5, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.fixedSize()
|
||||
.foregroundStyle(isCurrent ? AppTheme.green : .primary)
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 3.5)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
.overlay(Capsule().strokeBorder(.white.opacity(0.35), lineWidth: 0.8))
|
||||
.shadow(color: .black.opacity(0.12), radius: 4, y: 2)
|
||||
.offset(y: labelOffset)
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
.foregroundStyle(isCurrent ? AnyShapeStyle(.white) : AnyShapeStyle(.primary))
|
||||
// FAB와 겹쳐 있는 동안은 내용물을 숨겨 유리 덩어리만 보이게 한다
|
||||
// (glassEffect '앞'이라 콘텐츠에만 적용되고 유리 모양에는 영향 없음)
|
||||
.opacity(bloomed ? 1 : 0)
|
||||
.frame(width: bubbleSize, height: bubbleSize)
|
||||
.contentShape(Circle())
|
||||
}
|
||||
.buttonStyle(RadialPressStyle())
|
||||
.accessibilityLabel(Text(tab.label))
|
||||
// 펼침: FAB에서 자기 자리로 회전(-40°→0°)하며 날아간다. 접힘: FAB로 모여든다.
|
||||
// 트랜스폼 전용이라 프레임당 블러 재계산이 없다.
|
||||
.rotationEffect(.degrees(expanded ? 0 : -40))
|
||||
.offset(x: expanded ? dx : 0, y: expanded ? dy : 0)
|
||||
.scaleEffect(expanded ? 1 : 0.1)
|
||||
.opacity(expanded ? 1 : 0)
|
||||
// 펼칠 때만 안쪽부터 차례로 튀어나오는 스태거. 닫을 땐 지연 없이 한 번에 스냅.
|
||||
.animation(
|
||||
expanded
|
||||
? .spring(response: 0.32, dampingFraction: 0.72).delay(Double(index) * 0.03)
|
||||
: .spring(response: 0.24, dampingFraction: 0.9),
|
||||
value: expanded
|
||||
.buttonStyle(.plain)
|
||||
// 진짜 유리: 배경 굴절·빛 반사에 더해 .interactive가 눌림 시머·바운스까지 그려 준다.
|
||||
// 현재 탭은 초록 틴트 유리로 강조.
|
||||
.glassEffect(
|
||||
isCurrent
|
||||
? .regular.tint(AppTheme.green.opacity(0.85)).interactive()
|
||||
: .regular.interactive(),
|
||||
in: .circle
|
||||
)
|
||||
// 접혀 있을 땐 FAB 아래 숨어 있으므로 터치를 받지 않게 한다.
|
||||
.allowsHitTesting(expanded)
|
||||
.glassEffectID(tab.rawValue, in: glassSpace)
|
||||
.offset(x: bloomed ? dx : 0, y: bloomed ? dy : 0)
|
||||
// 펼칠 때만 안쪽부터 차례로 맺히는 스태거 + 감쇠 낮은 스프링(오버슈트로 액체 출렁임).
|
||||
// 되돌아갈 땐 지연 없이 한 번에 FAB로 흡수된다.
|
||||
.animation(
|
||||
bloomed
|
||||
? .spring(response: 0.45, dampingFraction: 0.6).delay(Double(index) * 0.04)
|
||||
: .spring(response: 0.26, dampingFraction: 0.9),
|
||||
value: bloomed
|
||||
)
|
||||
.allowsHitTesting(expanded && bloomed)
|
||||
.accessibilityLabel(Text(tab.label))
|
||||
}
|
||||
|
||||
// MARK: 중앙 플로팅 버튼
|
||||
@ -201,42 +236,45 @@ struct GlassBubbleMenu: View {
|
||||
Button {
|
||||
toggle()
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
Circle().fill(
|
||||
LinearGradient(
|
||||
colors: [.white.opacity(0.28), .white.opacity(0.02)],
|
||||
startPoint: .top, endPoint: .bottom
|
||||
)
|
||||
)
|
||||
)
|
||||
.overlay(Circle().fill(AppTheme.green.opacity(expanded ? 0.0 : 0.14)))
|
||||
.overlay(Circle().strokeBorder(.white.opacity(0.4), lineWidth: 1))
|
||||
Image(systemName: expanded ? "xmark" : currentTab.symbol)
|
||||
.font(.system(size: expanded ? 22 : 24, weight: .semibold))
|
||||
.foregroundStyle(AppTheme.green)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
.frame(width: fabSize, height: fabSize)
|
||||
.shadow(color: .black.opacity(0.22), radius: 10, y: 4)
|
||||
Image(systemName: expanded ? "xmark" : currentTab.symbol)
|
||||
.font(.system(size: expanded ? 21 : 23, weight: .semibold))
|
||||
.foregroundStyle(AppTheme.green)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.frame(width: fabSize, height: fabSize)
|
||||
.contentShape(Circle())
|
||||
}
|
||||
.buttonStyle(RadialPressStyle())
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive(), in: .circle)
|
||||
.glassEffectID("fab", in: glassSpace)
|
||||
// 펼치면 살짝 커져 '유리가 부풀며 방울을 내보내는' 기준점이 된다
|
||||
.scaleEffect(expanded ? 1.07 : 1)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: expanded)
|
||||
.accessibilityLabel(expanded ? Text("메뉴 닫기") : Text("탭 메뉴 열기"))
|
||||
}
|
||||
|
||||
// MARK: 동작
|
||||
|
||||
private func toggle() {
|
||||
// 버블은 자체 per-item 애니메이션을 갖고, 여기서는 배경 딤/FAB 아이콘 전환만 함께 움직인다.
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
expanded.toggle()
|
||||
if expanded { close() } else { open() }
|
||||
}
|
||||
|
||||
/// 1단계: 버블 삽입 — FAB 유리에서 모핑으로 부풀어 나온다 (offset 0, FAB와 한 덩어리).
|
||||
/// 2단계: 다음 런루프에서 bloomed → 스태거 스프링으로 아치에 흩어진다.
|
||||
/// (같은 트랜잭션에서 두 상태를 함께 바꾸면 삽입 애니메이션이 이동을 삼켜 버려 분리한다)
|
||||
private func open() {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.75)) {
|
||||
expanded = true
|
||||
}
|
||||
Task { @MainActor in
|
||||
guard expanded else { return }
|
||||
bloomed = true
|
||||
}
|
||||
}
|
||||
|
||||
/// 닫기: 지연 없이 흡수 — 제거 모핑과 복귀 스프링이 함께 FAB로 빨려 들어간다.
|
||||
private func close() {
|
||||
withAnimation(.easeOut(duration: 0.18)) {
|
||||
withAnimation(.spring(response: 0.26, dampingFraction: 0.9)) {
|
||||
bloomed = false
|
||||
expanded = false
|
||||
}
|
||||
}
|
||||
@ -247,12 +285,3 @@ struct GlassBubbleMenu: View {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
/// FAB·버블 공통 눌림 효과 (살짝 줄어들며 반응). 짧은 easeOut으로 가볍게.
|
||||
private struct RadialPressStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.9 : 1)
|
||||
.animation(.easeOut(duration: 0.14), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user