Skip to content

Instantly share code, notes, and snippets.

@mireabot
Last active February 13, 2025 20:51
Show Gist options
  • Save mireabot/63fac78a4f73d1ebd280864772231cc7 to your computer and use it in GitHub Desktop.
Save mireabot/63fac78a4f73d1ebd280864772231cc7 to your computer and use it in GitHub Desktop.
Eye Coins Menu UI
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