nav bar v2
This commit is contained in:
parent
62632ad8d6
commit
a473e39169
4 changed files with 562 additions and 19 deletions
|
|
@ -8,28 +8,55 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var selectedIndex = 0
|
||||
private let tabs = TabItem.allTabs
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
HomeView()
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house.fill")
|
||||
}
|
||||
|
||||
SearchView()
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
|
||||
NotificationsView()
|
||||
.tabItem {
|
||||
Label("Notifications", systemImage: "bell.fill")
|
||||
}
|
||||
|
||||
ProfileView()
|
||||
.tabItem {
|
||||
Label("Profile", systemImage: "person.fill")
|
||||
ZStack {
|
||||
// Background View Container
|
||||
Group {
|
||||
switch selectedIndex {
|
||||
case 0:
|
||||
HomeView()
|
||||
case 1:
|
||||
SearchView()
|
||||
case 2:
|
||||
NotificationsView()
|
||||
case 3:
|
||||
ProfileView()
|
||||
case 4:
|
||||
AnalyticsView()
|
||||
case 5:
|
||||
MessagesView()
|
||||
case 6:
|
||||
FavoritesView()
|
||||
case 7:
|
||||
DownloadsView()
|
||||
case 8:
|
||||
CloudView()
|
||||
case 9:
|
||||
SettingsView()
|
||||
case 10:
|
||||
HelpView()
|
||||
default:
|
||||
HomeView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.id(selectedIndex) // Enables smooth transition animation per view change
|
||||
.transition(.asymmetric(
|
||||
insertion: .opacity.combined(with: .scale(scale: 0.98)),
|
||||
removal: .opacity
|
||||
))
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
// Spacer height corresponding to the WheelNavBar height
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
WheelNavBar(selectedIndex: $selectedIndex, tabs: tabs)
|
||||
}
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
276
tg_test/ExtraViews.swift
Normal file
276
tg_test/ExtraViews.swift
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
//
|
||||
// ExtraViews.swift
|
||||
// tg_test
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Analytics View
|
||||
struct AnalyticsView: View {
|
||||
private let metrics = [
|
||||
("Daily Active Users", "12.4K", "+18%", Color.green),
|
||||
("Avg. Session Time", "8m 42s", "+5%", Color.blue),
|
||||
("Retention Rate", "68.2%", "+2.4%", Color.purple),
|
||||
("Conversion Rate", "4.15%", "-0.8%", Color.orange)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Overview Highlights")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
|
||||
ForEach(metrics, id: \.0) { item in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(item.0)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(item.1)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
Text(item.2)
|
||||
.font(.caption)
|
||||
.bold()
|
||||
.foregroundStyle(item.3)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Performance Chart")
|
||||
.font(.headline)
|
||||
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(LinearGradient(colors: [.blue.opacity(0.6), .purple.opacity(0.6)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.frame(height: 180)
|
||||
.overlay(
|
||||
Image(systemName: "waveform.path.ecg")
|
||||
.font(.system(size: 70))
|
||||
.foregroundStyle(.white.opacity(0.8))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
.navigationTitle("Analytics")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Messages View
|
||||
struct MessagesView: View {
|
||||
struct Chat: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let message: String
|
||||
let time: String
|
||||
let unread: Bool
|
||||
}
|
||||
|
||||
private let chats = [
|
||||
Chat(name: "Sarah Connor", message: "Hey, did you review the latest wheel UI?", time: "10:42 AM", unread: true),
|
||||
Chat(name: "Tech Team", message: "Deployment complete for v1.0.4", time: "09:15 AM", unread: true),
|
||||
Chat(name: "Alex Rivera", message: "See you at the meeting tomorrow!", time: "Yesterday", unread: false),
|
||||
Chat(name: "Design Guild", message: "New Figma mockups are uploaded.", time: "Tuesday", unread: false)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List(chats) { chat in
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(chat.unread ? Color.teal : Color.gray.opacity(0.3))
|
||||
.frame(width: 44, height: 44)
|
||||
.overlay(
|
||||
Text(String(chat.name.prefix(1)))
|
||||
.font(.headline)
|
||||
.foregroundStyle(chat.unread ? .white : .primary)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(chat.name)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(chat.time)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text(chat.message)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(chat.unread ? .primary : .secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.navigationTitle("Messages")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Favorites View
|
||||
struct FavoritesView: View {
|
||||
private let favoriteItems = [
|
||||
("SwiftUI Wheel Navigation", "UI Component", "star.fill", Color.yellow),
|
||||
("Async/Await Best Practices", "Article", "doc.text.fill", Color.blue),
|
||||
("Color Schemes & Typography", "Guide", "paintpalette.fill", Color.purple),
|
||||
("Custom Canvas Rendering", "Tutorial", "paintbrush.fill", Color.pink)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List(favoriteItems, id: \.0) { item in
|
||||
HStack(spacing: 14) {
|
||||
Image(systemName: item.2)
|
||||
.font(.title2)
|
||||
.foregroundStyle(item.3)
|
||||
.frame(width: 36)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(item.0)
|
||||
.font(.headline)
|
||||
Text(item.1)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundStyle(.pink)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.navigationTitle("Favorites")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Downloads View
|
||||
struct DownloadsView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.down.circle.fill")
|
||||
.font(.system(size: 70))
|
||||
.foregroundStyle(.cyan)
|
||||
|
||||
Text("No Active Downloads")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
|
||||
Text("Files you download will show up here for offline access.")
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
Button(action: {}) {
|
||||
Label("Explore Content", systemImage: "sparkles")
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(Color.cyan)
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("Downloads")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cloud View
|
||||
struct CloudView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("Storage Usage") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Cloud Storage")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text("42.5 GB / 100 GB")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
ProgressView(value: 0.425)
|
||||
.tint(.blue)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
Section("Synced Repositories") {
|
||||
Label("iOS App Test Project", systemImage: "folder.fill")
|
||||
Label("Design Assets 2026", systemImage: "photo.on.rectangle.angled")
|
||||
Label("User Data Backups", systemImage: "internaldrive.fill")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Cloud")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings View
|
||||
struct SettingsView: View {
|
||||
@State private var autoSync = true
|
||||
@State private var hapticFeedback = true
|
||||
@State private var wheelSpeed = 1.0
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Navigation Wheel Settings") {
|
||||
Toggle("Haptic Feedback", isOn: $hapticFeedback)
|
||||
Toggle("Auto Sync Selection", isOn: $autoSync)
|
||||
}
|
||||
|
||||
Section("App Preferences") {
|
||||
HStack {
|
||||
Text("Theme Color")
|
||||
Spacer()
|
||||
Text("Dynamic")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Help View
|
||||
struct HelpView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("Frequent Questions") {
|
||||
DisclosureGroup("How do I use the wheel nav bar?") {
|
||||
Text("Swipe horizontally on the bottom dial or tap any icon to switch pages instantly. You can also tap the left/right arrows for step-by-step navigation.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
DisclosureGroup("How many pages can the wheel hold?") {
|
||||
Text("The wheel supports virtually unlimited pages! Drag left or right to scroll smoothly through 10, 11, or more pages.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Help & Support")
|
||||
}
|
||||
}
|
||||
}
|
||||
28
tg_test/TabItem.swift
Normal file
28
tg_test/TabItem.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// TabItem.swift
|
||||
// tg_test
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TabItem: Identifiable, Equatable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let icon: String
|
||||
let color: Color
|
||||
var badge: String? = nil
|
||||
|
||||
static let allTabs: [TabItem] = [
|
||||
TabItem(id: 0, title: "Home", icon: "house.fill", color: .blue),
|
||||
TabItem(id: 1, title: "Search", icon: "magnifyingglass", color: .indigo),
|
||||
TabItem(id: 2, title: "Notifications", icon: "bell.fill", color: .orange, badge: "3"),
|
||||
TabItem(id: 3, title: "Profile", icon: "person.fill", color: .purple),
|
||||
TabItem(id: 4, title: "Analytics", icon: "chart.bar.xaxis", color: .green),
|
||||
TabItem(id: 5, title: "Messages", icon: "bubble.left.and.bubble.right.fill", color: .teal, badge: "5"),
|
||||
TabItem(id: 6, title: "Favorites", icon: "heart.fill", color: .pink),
|
||||
TabItem(id: 7, title: "Downloads", icon: "arrow.down.circle.fill", color: .cyan),
|
||||
TabItem(id: 8, title: "Cloud", icon: "icloud.fill", color: .blue),
|
||||
TabItem(id: 9, title: "Settings", icon: "gearshape.fill", color: .gray),
|
||||
TabItem(id: 10, title: "Help", icon: "questionmark.circle.fill", color: .mint)
|
||||
]
|
||||
}
|
||||
212
tg_test/WheelNavBar.swift
Normal file
212
tg_test/WheelNavBar.swift
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
//
|
||||
// WheelNavBar.swift
|
||||
// tg_test
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WheelNavBar: View {
|
||||
@Binding var selectedIndex: Int
|
||||
let tabs: [TabItem]
|
||||
|
||||
@GestureState private var dragTranslation: CGFloat = 0
|
||||
@State private var activeDragIndex: Int = 0
|
||||
|
||||
// Haptic feedback generator
|
||||
private let feedback = UIImpactFeedbackGenerator(style: .medium)
|
||||
|
||||
private let itemSpacing: CGFloat = 62
|
||||
|
||||
var body: some View {
|
||||
let currentTab = tabs[min(max(selectedIndex, 0), tabs.count - 1)]
|
||||
|
||||
VStack(spacing: 8) {
|
||||
// MARK: - Header Pill (Active Tab Title & Indicator)
|
||||
HStack(spacing: 8) {
|
||||
// Previous button
|
||||
Button(action: {
|
||||
selectPrevious()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundStyle(selectedIndex > 0 ? currentTab.color : Color.gray.opacity(0.4))
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color.white.opacity(0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.disabled(selectedIndex == 0)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Active Title and Index Pill
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: currentTab.icon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(currentTab.color)
|
||||
|
||||
Text(currentTab.title)
|
||||
.font(.system(size: 15, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("\(selectedIndex + 1)/\(tabs.count)")
|
||||
.font(.system(size: 11, weight: .semibold, design: .monospaced))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(currentTab.color.opacity(0.2))
|
||||
.foregroundStyle(currentTab.color)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(.regularMaterial)
|
||||
.shadow(color: currentTab.color.opacity(0.25), radius: 8, x: 0, y: 3)
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Next button
|
||||
Button(action: {
|
||||
selectNext()
|
||||
}) {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundStyle(selectedIndex < tabs.count - 1 ? currentTab.color : Color.gray.opacity(0.4))
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color.white.opacity(0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.disabled(selectedIndex == tabs.count - 1)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// MARK: - Wheel Dial Track
|
||||
GeometryReader { geometry in
|
||||
let centerPoint = geometry.size.width / 2
|
||||
let effectiveIndex = Double(selectedIndex) - Double(dragTranslation / itemSpacing)
|
||||
|
||||
ZStack {
|
||||
// Central Selector Ring Glow
|
||||
Circle()
|
||||
.fill(currentTab.color.opacity(0.15))
|
||||
.frame(width: 56, height: 56)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(currentTab.color, lineWidth: 2.5)
|
||||
)
|
||||
.position(x: centerPoint, y: 32)
|
||||
.shadow(color: currentTab.color.opacity(0.4), radius: 10)
|
||||
|
||||
// Rotary Wheel Items
|
||||
ForEach(tabs) { tab in
|
||||
let distance = Double(tab.id) - effectiveIndex
|
||||
let xOffset = distance * itemSpacing
|
||||
// Arc wheel curvature (quadratic Y offset for wheel effect)
|
||||
let yOffset = pow(distance, 2) * 2.2
|
||||
// Scaling factor (center item is largest)
|
||||
let scale = max(0.55, 1.15 - min(abs(distance) * 0.18, 0.6))
|
||||
// Opacity falloff
|
||||
let opacity = max(0.25, 1.0 - min(abs(distance) * 0.22, 0.75))
|
||||
// 3D Y Rotation Angle for wheel perspective
|
||||
let rotationY = min(max(distance * -18.0, -60.0), 60.0)
|
||||
|
||||
let isSelected = tab.id == selectedIndex
|
||||
|
||||
VStack(spacing: 2) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(isSelected ? tab.color : Color(.tertiarySystemFill))
|
||||
.frame(width: 44, height: 44)
|
||||
.shadow(color: isSelected ? tab.color.opacity(0.5) : .clear, radius: 6)
|
||||
|
||||
Image(systemName: tab.icon)
|
||||
.font(.system(size: isSelected ? 20 : 16, weight: isSelected ? .bold : .medium))
|
||||
.foregroundStyle(isSelected ? .white : .primary)
|
||||
|
||||
if let badge = tab.badge {
|
||||
Text(badge)
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
.padding(4)
|
||||
.background(Color.red)
|
||||
.clipShape(Circle())
|
||||
.offset(x: 14, y: -14)
|
||||
}
|
||||
}
|
||||
}
|
||||
.scaleEffect(scale)
|
||||
.opacity(opacity)
|
||||
.rotation3DEffect(
|
||||
.degrees(rotationY),
|
||||
axis: (x: 0.0, y: 1.0, z: 0.0),
|
||||
perspective: 0.5
|
||||
)
|
||||
.position(x: centerPoint + xOffset, y: 32 + yOffset)
|
||||
.onTapGesture {
|
||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||
selectedIndex = tab.id
|
||||
feedback.impactOccurred()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 72)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.updating($dragTranslation) { value, state, _ in
|
||||
state = value.translation.width
|
||||
}
|
||||
.onChanged { value in
|
||||
let calcIndex = Double(selectedIndex) - Double(value.translation.width / itemSpacing)
|
||||
let rounded = min(max(Int(round(calcIndex)), 0), tabs.count - 1)
|
||||
if rounded != activeDragIndex {
|
||||
activeDragIndex = rounded
|
||||
feedback.impactOccurred()
|
||||
}
|
||||
}
|
||||
.onEnded { value in
|
||||
let velocity = value.predictedEndTranslation.width / itemSpacing
|
||||
let calcIndex = Double(selectedIndex) - Double(value.translation.width / itemSpacing) - (velocity * 0.2)
|
||||
let targetIndex = min(max(Int(round(calcIndex)), 0), tabs.count - 1)
|
||||
|
||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||
selectedIndex = targetIndex
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 28, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.shadow(color: Color.black.opacity(0.12), radius: 16, x: 0, y: 8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 28, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
private func selectPrevious() {
|
||||
if selectedIndex > 0 {
|
||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||
selectedIndex -= 1
|
||||
feedback.impactOccurred()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectNext() {
|
||||
if selectedIndex < tabs.count - 1 {
|
||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||
selectedIndex += 1
|
||||
feedback.impactOccurred()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue