Instantly share code, notes, and snippets.
Last active
February 13, 2025 20:51
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save mireabot/63fac78a4f73d1ebd280864772231cc7 to your computer and use it in GitHub Desktop.
Eye Coins Menu UI
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
import SwiftUI | |
struct CoinsSummaryView: View { | |
@State private var scrollPosition: Int? = nil | |
let actions = [ | |
ActionsData(icon: Image(systemName: "arrow.triangle.2.circlepath"), iconColor: .blue, title: "Trade eyecoin", info: "Trade eyecoin to convert it to dollars"), | |
ActionsData(icon: Image(systemName: "trophy.fill"), iconColor: .yellow, title: "Complete available quests", info: "Complete quests to earn more eyecoins"), | |
ActionsData(icon: Image(systemName: "dollarsign.circle.fill"), iconColor: .green, title: "Earn points", info: "Earn eye coins which you can trade"), | |
] | |
let quests = [ | |
QuestData(points: 50, title: "Record driving to work"), | |
QuestData(points: 75, title: "Capture a 30-minute walk"), | |
QuestData(points: 40, title: "Record a visit to local café"), | |
QuestData(points: 30, title: "Record your studying session"), | |
QuestData(points: 50, title: "Capture a workut routine"), | |
QuestData(points: 90, title: "Record singing any song you like"), | |
QuestData(points: 20, title: "Take a photo of your pet"), | |
QuestData(points: 60, title: "Log your meditation session"), | |
QuestData(points: 80, title: "Capture a cycling trip"), | |
QuestData(points: 45, title: "Record a home-cooked meal"), | |
QuestData(points: 55, title: "Take a photo of a sunset"), | |
QuestData(points: 35, title: "Record your daily journaling"), | |
QuestData(points: 70, title: "Capture a nature walk"), | |
QuestData(points: 25, title: "Log your water intake for the day"), | |
QuestData(points: 85, title: "Record a dance session"), | |
QuestData(points: 50, title: "Take a picture of your favorite book"), | |
QuestData(points: 95, title: "Record a personal achievement today") | |
] | |
@State private var currentTab: Int = 0 | |
@State private var currentFilter: String = "Available quests" | |
var body: some View { | |
ZStack(alignment: .bottom) { | |
ScrollView(showsIndicators: false) { | |
// Header | |
VStack(alignment: .leading, spacing: 8, content: { | |
Text("Total eyecoins value") | |
.font(.system(.callout, weight: .medium)) | |
.foregroundColor(.secondary) | |
Text("$\(258.32, specifier: "%.2f")") | |
.font(.system(.title, weight: .semibold)) | |
}) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding(.horizontal, 16) | |
.padding(.top, 16) | |
// Scrollable actions | |
ScrollView(.horizontal, showsIndicators: false) { | |
LazyHStack(spacing: 10) { | |
ForEach(actions) { index in | |
ActionCard(icon: index.icon, iconColor: index.iconColor, title: index.title, info: index.info) | |
.id(index.id) | |
.onChange(of: scrollPosition ?? 0) { oldValue, newValue in | |
currentTab = newValue | |
} | |
} | |
} | |
.scrollTargetLayout() | |
.padding(.vertical, 16) | |
.padding(.horizontal, 16) | |
} | |
.scrollTargetBehavior(.viewAligned) | |
.scrollPosition(id: $scrollPosition, anchor: .center) | |
.fixedSize(horizontal: false, vertical: true) | |
tabsCounter() | |
.padding(.bottom, 16) | |
Divider() | |
// Filter tabs | |
HStack(spacing: 16) { | |
filterTab(title: "Available quests", count: 6) | |
filterTab(title: "Completed", count: 4) | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding(.horizontal, 16) | |
.padding(.vertical, 10) | |
Divider() | |
// Quests | |
LazyVStack { | |
ForEach(quests, id: \.title) { quest in | |
QuestCard(points: quest.points, title: quest.title) | |
.scrollTransition(.animated(.smooth(duration: 0.8))) { content, phase in | |
content | |
.opacity(opacityAmount(for: phase)) | |
.scaleEffect(scaleAmount(for: phase)) | |
.blur(radius: blurAmount(for: phase)) | |
} | |
} | |
} | |
} | |
// Controls | |
HStack { | |
Button(action: {}, label: { | |
Image(systemName: "rectangle.stack.fill") | |
}) | |
.buttonStyle(CircleButtonStyle()) | |
Button(action: {}, label: { | |
HStack { | |
Image(systemName: "record.circle") | |
Text("Start recording") | |
} | |
.frame(maxWidth: .infinity, alignment: .center) | |
}) | |
.buttonStyle(CapsuleButtonStyle()) | |
Button(action: {}, label: { | |
Image(systemName: "gearshape") | |
}) | |
.buttonStyle(CircleButtonStyle()) | |
} | |
.padding(.horizontal, 16) | |
.padding(.top, 8) | |
.background(.white) | |
} | |
} | |
// MARK: - Supporting Scroll Methods | |
private func blurAmount(for phase: ScrollTransitionPhase) -> CGFloat { | |
5 * abs(phase.value) | |
} | |
private func opacityAmount(for phase: ScrollTransitionPhase) -> CGFloat { | |
1 - abs(phase.value) | |
} | |
private func scaleAmount(for phase: ScrollTransitionPhase) -> CGFloat { | |
1 - (abs(phase.value) * 0.1) | |
} | |
// MARK: Supporting Views | |
@ViewBuilder | |
func tabsCounter() -> some View { | |
HStack(spacing: 4) { | |
ForEach(actions) { index in | |
Rectangle() | |
.fill(currentTab == index.id ? Color.black : Color(hex: "1018181B")) | |
.frame(width: 30, height: 5) | |
.cornerRadius(2) | |
.animation(.smooth(duration: 0.7, extraBounce: 0.3), value: currentTab) | |
} | |
} | |
} | |
@ViewBuilder | |
func filterTab(title: String, count: Int) -> some View { | |
Button(action: { | |
withAnimation(.smooth(duration: 0.7, extraBounce: 0.3)) { | |
currentFilter = title | |
} | |
}, label: { | |
HStack(alignment: .center, spacing: 5) { | |
Text(title) | |
.font(.system(.headline, weight: .medium)) | |
.foregroundColor(currentFilter == title ? .primary : .secondary) | |
Text("\(count)") | |
.font(.system(.caption)) | |
.foregroundColor(.secondary) | |
.padding(5) | |
.background(Color(hex: "#1018181B")) | |
.clipShape(Circle()) | |
} | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
} | |
// MARK: - Action cards data and view | |
struct ActionsData: Identifiable, Hashable { | |
let id: Int | |
var icon: Image | |
var iconColor: Color | |
var title: String | |
var info: String | |
private static var currentId = 0 | |
init(icon: Image, iconColor: Color, title: String, info: String) { | |
self.id = ActionsData.currentId | |
ActionsData.currentId += 1 | |
self.icon = icon | |
self.iconColor = iconColor | |
self.title = title | |
self.info = info | |
} | |
static func == (lhs: ActionsData, rhs: ActionsData) -> Bool { | |
lhs.id == rhs.id | |
} | |
func hash(into hasher: inout Hasher) { | |
hasher.combine(id) | |
} | |
} | |
struct ActionCard: View { | |
let icon: Image | |
let iconColor: Color | |
let title: String | |
let info: String | |
var body: some View { | |
HStack(alignment: .top) { | |
// Leading icon | |
icon | |
.font(.system(.footnote)) | |
.foregroundColor(iconColor) | |
// Text content | |
VStack(alignment: .leading, spacing: 4) { | |
Text(title) | |
.font(.system(.callout, weight: .medium)) | |
Text(info) | |
.font(.system(.footnote)) | |
.foregroundColor(.secondary) | |
} | |
Spacer() | |
} | |
.padding(12) | |
.background(.white) | |
.cornerRadius(10) | |
.overlay( | |
RoundedRectangle(cornerRadius: 10) | |
.strokeBorder(Color(hex: "#1218181B"), lineWidth: 1) | |
) | |
} | |
} | |
// MARK: - Quest cards data and view | |
struct QuestData { | |
var id: UUID = UUID() | |
var points: Int | |
var title: String | |
} | |
struct QuestCard: View { | |
private var points: Int | |
private var title: String | |
init(points: Int, title: String) { | |
self.points = points | |
self.title = title | |
} | |
var body: some View { | |
VStack(spacing: 0) { | |
HStack { | |
VStack(alignment: .leading, spacing: 8) { | |
HStack(spacing: 4) { | |
Image(systemName: "plus.circle") | |
.font(.system(.headline)) | |
.foregroundColor(.green) | |
Text("\(points) points") | |
.font(.system(.headline, weight: .semibold)) | |
} | |
Text(title) | |
.font(.system(.headline, weight: .regular)) | |
.foregroundColor(.secondary) | |
} | |
Spacer() | |
Button(action: {}, label: { | |
Text("Record") | |
.foregroundColor(.primary) | |
.font(.system(.headline, weight: .medium)) | |
.padding(.vertical, 10) | |
.padding(.horizontal, 14) | |
.background( | |
Capsule() | |
.strokeBorder(Color(hex: "#1218181B"), lineWidth: 1) | |
.background(Color.white) | |
.clipped() | |
) | |
.clipShape(Capsule()) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
.padding(16) | |
Divider() | |
} | |
} | |
} | |
// MARK: - Button Styles | |
struct CircleButtonStyle: ButtonStyle { | |
func makeBody(configuration: Configuration) -> some View { | |
configuration.label | |
.font(.system(.title3)) | |
.foregroundColor(Color(hex: "#52525B")) | |
.padding(16) | |
.background( | |
Circle() | |
.strokeBorder(Color(hex: "#1218181B"), lineWidth: 1) | |
.background(.white) | |
.clipped() | |
) | |
.clipShape(Circle()) | |
} | |
} | |
struct CapsuleButtonStyle: ButtonStyle { | |
func makeBody(configuration: Configuration) -> some View { | |
configuration.label | |
.font(.system(.title3)) | |
.foregroundColor(.white) | |
.padding(16) | |
.background( | |
Capsule() | |
.background(.black) | |
) | |
.clipShape(Capsule()) | |
} | |
} | |
// MARK: - Extensions | |
extension Color { | |
init(hex: String) { | |
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) | |
var int: UInt64 = 0 | |
Scanner(string: hex).scanHexInt64(&int) | |
let a, r, g, b: UInt64 | |
switch hex.count { | |
case 3: // RGB (12-bit) | |
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) | |
case 6: // RGB (24-bit) | |
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) | |
case 8: // ARGB (32-bit) | |
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) | |
default: | |
(a, r, g, b) = (255, 0, 0, 0) | |
} | |
self.init( | |
.sRGB, | |
red: Double(r) / 255, | |
green: Double(g) / 255, | |
blue: Double(b) / 255, | |
opacity: Double(a) / 255 | |
) | |
} | |
} | |
// MARK: - Preview | |
#Preview { | |
CoinsSummaryView() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment