Skip to content

Instantly share code, notes, and snippets.

@slangley
Last active December 2, 2025 02:04
Show Gist options
  • Select an option

  • Save slangley/7175afc3aaafcdfd3878ce676f088190 to your computer and use it in GitHub Desktop.

Select an option

Save slangley/7175afc3aaafcdfd3878ce676f088190 to your computer and use it in GitHub Desktop.
Exploring how to make an "action" button on the TabView in SwiftUI with Liquid Glass. (Screenshot in comment)
//
// GlassTabBar.swift
// Soothe
//
// Created by Sean Langley on 2025-11-28.
//
import SwiftUI
struct TabItem: Identifiable {
let id = UUID()
let icon: String
let title: String
let tag: Int
}
struct GlassTabBar: View {
var namespace: Namespace.ID
@Binding var selectedTab: Int
let items: [TabItem]
@Namespace private var animation
@State private var touchedTab: Int? = nil
@State private var isTouching: Bool = false
var body: some View {
HStack(spacing: 0) {
ForEach(items) { item in
TabBarButton(
icon: item.icon,
title: item.title,
isSelected: selectedTab == item.tag,
isTouched: touchedTab == item.tag,
isTouching: isTouching,
namespace: animation,
itemTag: item.tag,
onTouchBegan: {
isTouching = true
withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) {
touchedTab = item.tag
}
},
onTouchEnded: {
if let touched = touchedTab {
withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) {
selectedTab = touched
}
}
isTouching = false
touchedTab = nil
}
)
.frame(maxWidth: .infinity)
.overlay {
// Indicator positioned absolutely behind each button
HStack {
if (isTouching && touchedTab == item.tag) || (!isTouching && selectedTab == item.tag) {
GlassTabIndicator(isPressed: isTouching)
}
}
.allowsHitTesting(false)
}
}
}
.padding(.vertical, 6)
.glassEffect(.regular)//.glassEffectUnion(id: "tabbar", namespace: namespace)
.padding(.horizontal, 24)
.padding(.bottom, 16)
}
}
struct TabBarButton: View {
let icon: String
let title: String
let isSelected: Bool
let isTouched: Bool
let isTouching: Bool
let namespace: Namespace.ID
let itemTag: Int
let onTouchBegan: () -> Void
let onTouchEnded: () -> Void
var body: some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 20, weight: (isSelected || isTouched) ? .semibold : .regular))
.foregroundStyle((isSelected || isTouched) ? .primary : .secondary)
.foregroundColor(isSelected ? .accentColor : .primary)
Text(title)
.font(.caption2)
.fontWeight((isSelected || isTouched) ? .semibold : .regular)
.foregroundStyle((isSelected || isTouched) ? .primary : .secondary)
.foregroundColor(isSelected ? .accentColor : .primary)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !isTouching || !isTouched {
onTouchBegan()
}
}
.onEnded { _ in
onTouchEnded()
}
)
}
}
struct GlassTabIndicator: View {
var isPressed: Bool
var body: some View {
RoundedRectangle(cornerRadius: 22)
.frame(width: 70, height: 44)
.scaleEffect(isPressed ? 1.5 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: isPressed)
.background(Color.black.opacity(0.1))
}
}
// MARK: - Example Usage
struct GlassTabBarExample: View {
@Namespace var namespace
@State private var selectedTab = 0
@State private var buttonPressed = false
let tabItems = [
TabItem(icon: "house.fill", title: "Home", tag: 0),
TabItem(icon: "heart.fill", title: "Favorites", tag: 1),
TabItem(icon: "chart.line.uptrend.xyaxis", title: "Stats", tag: 2),
TabItem(icon: "person.fill", title: "Profile", tag: 3)
]
var body: some View {
ZStack(alignment: .bottom) {
Rectangle().background(LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom)).frame(maxWidth: .infinity, maxHeight: .infinity)
// Tab content that extends behind the tab bar
TabView(selection: $selectedTab) {
HomeTabContent()
.tag(0)
FavoritesTabContent()
.tag(1)
StatsTabContent()
.tag(2)
ProfileTabContent()
.tag(3)
}
.tabViewStyle(.page(indexDisplayMode: .never))
// Floating glass tab bar overlaid on top
GlassEffectContainer(spacing: 40) {
VStack(spacing:-10) {
Button {
buttonPressed.toggle()
} label: {
Image(systemName: "plus").padding()
}.glassEffect(.regular).frame(width:50,height:50).glassEffectUnion(id: "tabbar", namespace: namespace)
GlassTabBar(namespace: namespace, selectedTab: $selectedTab, items: tabItems)
}
}
}
.ignoresSafeArea(.container, edges: .bottom)
.sheet(isPresented: $buttonPressed) {
Text("Alert presented. Swipe down to dismiss.")
}
}
}
// MARK: - Demo Tab Content Views
struct HomeTabContent: View {
var body: some View {
VStack {
Image(systemName: "house.fill")
.font(.system(size: 60))
.foregroundStyle(.tint)
Text("Home")
.font(.title)
.fontWeight(.bold)
}
}
}
struct FavoritesTabContent: View {
var body: some View {
VStack {
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundStyle(.red)
Text("Favorites")
.font(.title)
.fontWeight(.bold)
}
}
}
struct StatsTabContent: View {
var body: some View {
VStack {
Image(systemName: "chart.line.uptrend.xyaxis")
.font(.system(size: 60))
.foregroundStyle(.green)
Text("Statistics")
.font(.title)
.fontWeight(.bold)
}
}
}
struct ProfileTabContent: View {
var body: some View {
VStack {
Image(systemName: "person.fill")
.font(.system(size: 60))
.foregroundStyle(.orange)
Text("Profile")
.font(.title)
.fontWeight(.bold)
}
}
}
#Preview("Full Screen", traits: .sizeThatFitsLayout) {
GlassTabBarExample()
}
@slangley
Copy link
Author

slangley commented Dec 2, 2025

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment