Last active
August 11, 2025 20:28
-
-
Save anettodev/4fd022236a7db2c9d22cd60260d8f905 to your computer and use it in GitHub Desktop.
Test LiquidGlass Music Template
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ContentView.swift | |
| // tetetetee | |
| // | |
| // Created by Antonio Netto on 11/08/25. | |
| // | |
| import SwiftUI | |
| // MARK: - Data Models | |
| struct Artist: Identifiable { | |
| let id: Int | |
| let name: String | |
| let imageName: String | |
| let topSong: String | |
| let rank: Int | |
| } | |
| struct ListeningStats { | |
| let minutes: Int | |
| let period: String | |
| } | |
| // MARK: - Tab Items Enum | |
| enum TabItem: String, CaseIterable { | |
| case dashboard = "Dashboard" | |
| case profile = "Profile" | |
| case search = "Search" | |
| var iconName: String { | |
| switch self { | |
| case .dashboard: "music.note.list" | |
| case .profile: "person.fill" | |
| case .search: "magnifyingglass" | |
| } | |
| } | |
| var title: String { | |
| return self.rawValue | |
| } | |
| @ViewBuilder | |
| func view() -> some View { | |
| switch self { | |
| case .dashboard: DashboardView() | |
| case .profile: ProfileView() | |
| case .search: SearchView() | |
| } | |
| } | |
| } | |
| // MARK: - Action Items Enum | |
| enum ActionItem: CaseIterable { | |
| case like | |
| case play | |
| var iconName: String { | |
| switch self { | |
| case .like: | |
| "heart.fill" | |
| case .play: | |
| "play" | |
| } | |
| } | |
| var color: Color { | |
| switch self { | |
| case .like: .red | |
| case .play: .blue | |
| } | |
| } | |
| } | |
| // MARK: - Reusable Components | |
| struct GradientBackgroundView: View { | |
| var body: some View { | |
| LinearGradient( | |
| gradient: Gradient(colors: [ | |
| Color.orange.opacity(0.5), | |
| Color.red.opacity(0.5), | |
| Color.pink.opacity(0.4), | |
| Color.clear | |
| ]), | |
| startPoint: .topLeading, | |
| endPoint: .bottomTrailing | |
| ) | |
| .ignoresSafeArea(.all) | |
| } | |
| } | |
| struct FloatingActionButtonView: View { | |
| @State private var isExpanded = false | |
| var body: some View { | |
| GlassEffectContainer { | |
| HStack { | |
| Spacer() | |
| if isExpanded { | |
| ForEach(ActionItem.allCases, id: \.self) { item in | |
| Button(action: { | |
| withAnimation { | |
| self.isExpanded = false | |
| // TODO ACTIONS | |
| } | |
| }) { | |
| Label("", systemImage: item.iconName) | |
| .labelStyle(.iconOnly) | |
| .frame(width: 60, height: 60) | |
| .background(.clear, in: Circle()) | |
| .foregroundColor(item.color) | |
| } | |
| .glassEffect(.clear.interactive()) | |
| .padding([.bottom, .trailing], 20) | |
| } | |
| } | |
| Button { self.isExpanded.toggle() } label: { | |
| Label("Add", systemImage: "plus") | |
| .labelStyle(.iconOnly) | |
| .frame(width: 60, height: 60) | |
| .background(.ultraThinMaterial, in: Circle()) | |
| .foregroundColor(.primary) | |
| .rotationEffect(.degrees(isExpanded ? 45 : 0)) | |
| } | |
| .glassEffect( | |
| .clear.interactive().tint(.clear) | |
| ) | |
| .padding([.trailing, .bottom], 20) | |
| } | |
| .tint(.primary) | |
| .frame(height: 100) | |
| } | |
| .animation(.smooth(duration: 0.5), value: isExpanded) | |
| } | |
| } | |
| // MARK: - Dashboard Components | |
| struct StatsCardView: View { | |
| let stats: ListeningStats | |
| var body: some View { | |
| VStack(alignment: .leading, spacing: 8) { | |
| HStack { | |
| Text("You listened for") | |
| .font(.title3) | |
| .foregroundColor(.primary) | |
| Spacer() | |
| } | |
| HStack { | |
| Text("\(stats.minutes.formatted())") | |
| .font(.largeTitle) | |
| .fontWeight(.bold) | |
| .foregroundColor(.primary) | |
| Text("minutes") | |
| .font(.title3) | |
| .foregroundColor(.primary) | |
| Text("in \(stats.period).") | |
| .font(.title3) | |
| .foregroundColor(.secondary) | |
| Spacer() | |
| } | |
| } | |
| .padding() | |
| .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) | |
| } | |
| } | |
| struct SectionHeaderView: View { | |
| let title: String | |
| var body: some View { | |
| HStack { | |
| Text(title) | |
| .font(.title2) | |
| .fontWeight(.semibold) | |
| .foregroundColor(.primary) | |
| Spacer() | |
| Button("View All") { | |
| // TODO: View all action | |
| } | |
| .font(.subheadline) | |
| .foregroundColor(.secondary) | |
| } | |
| } | |
| } | |
| struct ArtistCardView: View { | |
| let artist: Artist | |
| var body: some View { | |
| VStack(alignment: .leading, spacing: 12) { | |
| // Artist image with rank overlay | |
| ZStack(alignment: .topLeading) { | |
| RoundedRectangle(cornerRadius: 12) | |
| .fill(LinearGradient( | |
| colors: [.blue, .purple, .pink], | |
| startPoint: .topLeading, | |
| endPoint: .bottomTrailing | |
| )) | |
| .aspectRatio(1, contentMode: .fit) | |
| // Rank number | |
| Text("\(artist.rank)") | |
| .font(.title2) | |
| .fontWeight(.bold) | |
| .foregroundColor(.white) | |
| .padding(8) | |
| } | |
| // Artist info | |
| VStack(alignment: .leading, spacing: 4) { | |
| Text(artist.name) | |
| .font(.headline) | |
| .fontWeight(.semibold) | |
| .foregroundColor(.primary) | |
| HStack { | |
| Image(systemName: "music.note") | |
| .font(.caption) | |
| .foregroundColor(.secondary) | |
| Text(artist.topSong) | |
| .font(.caption) | |
| .foregroundColor(.secondary) | |
| .lineLimit(1) | |
| } | |
| // Play controls | |
| HStack { | |
| Button(action: {}) { | |
| Image(systemName: "play.fill") | |
| .font(.caption) | |
| .foregroundColor(.primary) | |
| } | |
| Button(action: {}) { | |
| Image(systemName: "forward.fill") | |
| .font(.caption) | |
| .foregroundColor(.secondary) | |
| } | |
| Spacer() | |
| } | |
| } | |
| } | |
| .padding(12) | |
| .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) | |
| } | |
| } | |
| // MARK: - Tab Views | |
| struct DashboardView: View { | |
| // Sample data | |
| private let stats = ListeningStats(minutes: 8491, period: "May") | |
| private let topArtists = [ | |
| Artist(id: 1, name: "ZAYN", imageName: "zayn", topSong: "Let Me Tonight", rank: 1), | |
| Artist(id: 2, name: "Justin Bieber", imageName: "justin", topSong: "Sorry", rank: 2), | |
| Artist(id: 3, name: "The Weeknd", imageName: "weeknd", topSong: "Blinding Lights", rank: 3), | |
| Artist(id: 4, name: "Dua Lipa", imageName: "dua", topSong: "Levitating", rank: 4), | |
| Artist(id: 5, name: "Billie Eilish", imageName: "billie", topSong: "Bad Guy", rank: 5), | |
| Artist(id: 6, name: "Ed Sheeran", imageName: "ed", topSong: "Shape of You", rank: 6) | |
| ] | |
| var body: some View { | |
| NavigationStack { | |
| ZStack(alignment: .bottomTrailing) { | |
| GradientBackgroundView() | |
| ScrollView { | |
| VStack(spacing: 24) { | |
| // Stats card | |
| StatsCardView(stats: stats) | |
| // Top Artists section | |
| VStack(alignment: .leading, spacing: 16) { | |
| SectionHeaderView(title: "Your Top Artists") | |
| LazyVGrid(columns: [ | |
| GridItem(.flexible(), spacing: 16), | |
| GridItem(.flexible(), spacing: 16) | |
| ], spacing: 16) { | |
| ForEach(topArtists) { artist in | |
| ArtistCardView(artist: artist) | |
| } | |
| } | |
| } | |
| // Bottom padding for floating action button | |
| Spacer() | |
| .frame(height: 120) | |
| } | |
| .padding(.horizontal) | |
| .padding(.top) | |
| } | |
| FloatingActionButtonView() | |
| } | |
| .navigationTitle("Replay") | |
| .navigationBarTitleDisplayMode(.large) | |
| } | |
| } | |
| } | |
| struct ProfileView: View { | |
| var body: some View { | |
| NavigationStack { | |
| ZStack { | |
| GradientBackgroundView() | |
| } | |
| .navigationTitle("Profile") | |
| } | |
| } | |
| } | |
| struct SearchView: View { | |
| @State private var searchString: String = "" | |
| var body: some View { | |
| NavigationStack { | |
| ZStack { | |
| GradientBackgroundView() | |
| } | |
| .navigationTitle("Search") | |
| .searchable(text: $searchString) | |
| } | |
| } | |
| } | |
| // MARK: - Main Tab View | |
| struct MainTabView: View { | |
| @State private var selectedTab: TabItem = .dashboard | |
| var body: some View { | |
| if #available(iOS 26.0, *) { | |
| TabView(selection: $selectedTab) { | |
| ForEach(TabItem.allCases, id: \.self) { tab in | |
| Tab( | |
| tab.title, | |
| systemImage: tab.iconName, | |
| value: tab, | |
| role: tab == .search ? .search : nil | |
| ) { | |
| tab.view() | |
| } | |
| } | |
| } | |
| #if os(iOS) | |
| .tabBarMinimizeBehavior(.onScrollDown) | |
| #endif | |
| } else { | |
| // Fallback < iOS 26.0 | |
| TabView { | |
| DashboardView() | |
| .tabItem { | |
| Image(systemName: TabItem.dashboard.iconName) | |
| Text(TabItem.dashboard.title) | |
| } | |
| ProfileView() | |
| .tabItem { | |
| Image(systemName: TabItem.profile.iconName) | |
| Text(TabItem.profile.title) | |
| } | |
| SearchView() | |
| .tabItem { | |
| Image(systemName: TabItem.search.iconName) | |
| Text(TabItem.search.title) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - Content View | |
| struct ContentView: View { | |
| var body: some View { | |
| MainTabView() | |
| .tabViewStyle(.sidebarAdaptable) | |
| } | |
| } | |
| #Preview { | |
| ContentView() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment