Skip to content

Instantly share code, notes, and snippets.

@paulocoutinhox
Created June 4, 2026 07:32
Show Gist options
  • Select an option

  • Save paulocoutinhox/69d35e72232a560fe6fdacdd6c85b752 to your computer and use it in GitHub Desktop.

Select an option

Save paulocoutinhox/69d35e72232a560fe6fdacdd6c85b752 to your computer and use it in GitHub Desktop.
Product Details Test in SwiftUI
import SwiftUI
struct ProductDetailsScreen: View {
@State private var selectedImage = 0
@State private var selectedColor = "Black"
@State private var selectedSize = "M"
@State private var quantity = 1
@State private var isFavorite = false
@State private var selectedSection = "Details"
private let colors = ["Black", "Blue", "Green", "Orange"]
private let sizes = ["XS", "S", "M", "L", "XL"]
private let sections = ["Details", "Features", "Reviews"]
private var total: Double {
Double(quantity) * 89.90
}
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
ScrollView(showsIndicators: false) {
VStack(spacing: 16) {
headerGallery
mainInfoCard
optionCard
sectionSelector
dynamicSectionCard
deliveryCard
sellerCard
similarProductsSection
Spacer()
.frame(height: 100)
}
.padding(.bottom, 20)
}
}
.safeAreaInset(edge: .bottom) {
bottomBar
}
}
private var headerGallery: some View {
ZStack(alignment: .top) {
TabView(selection: $selectedImage) {
ForEach(0..<4, id: \.self) { index in
ZStack {
LinearGradient(
colors: [
Color(.systemGray5),
Color(.systemGray6)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
VStack(spacing: 18) {
Image(systemName: "backpack.fill")
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.foregroundStyle(.secondary)
Text("Product Preview \(index + 1)")
.font(.headline)
.foregroundStyle(.secondary)
}
}
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(height: 360)
VStack {
HStack {
Button {
// back action
} label: {
Image(systemName: "chevron.left")
.font(.headline)
.foregroundStyle(.primary)
.frame(width: 42, height: 42)
.background(.ultraThinMaterial)
.clipShape(Circle())
}
Spacer()
Button {
isFavorite.toggle()
} label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
.font(.headline)
.foregroundStyle(isFavorite ? .red : .primary)
.frame(width: 42, height: 42)
.background(.ultraThinMaterial)
.clipShape(Circle())
}
}
.padding(.horizontal, 20)
.padding(.top, 16)
Spacer()
HStack(spacing: 8) {
ForEach(0..<4, id: \.self) { index in
Capsule()
.fill(selectedImage == index ? Color.primary : Color.secondary.opacity(0.3))
.frame(width: selectedImage == index ? 24 : 8, height: 8)
}
}
.padding(.bottom, 18)
}
}
}
private var mainInfoCard: some View {
VStack(alignment: .leading, spacing: 18) {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 8) {
Text("Premium Urban Backpack")
.font(.title2)
.fontWeight(.bold)
Text("Waterproof backpack with laptop space, hidden pocket and modern design.")
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer()
VStack(alignment: .trailing, spacing: 5) {
HStack(spacing: 4) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
Text("4.8")
.fontWeight(.bold)
}
.font(.subheadline)
Text("2.4k reviews")
.font(.caption)
.foregroundStyle(.secondary)
}
}
HStack(alignment: .bottom, spacing: 8) {
Text("$89.90")
.font(.largeTitle)
.fontWeight(.heavy)
Text("$119.90")
.font(.headline)
.foregroundStyle(.secondary)
.strikethrough()
Spacer()
Text("-25%")
.font(.caption)
.fontWeight(.bold)
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(.red)
.clipShape(Capsule())
}
HStack(spacing: 12) {
productBadge(icon: "truck.box.fill", title: "Free", subtitle: "Shipping")
productBadge(icon: "arrow.uturn.backward", title: "30 days", subtitle: "Return")
productBadge(icon: "shield.checkered", title: "1 year", subtitle: "Warranty")
}
}
.cardStyle()
.padding(.horizontal, 16)
.padding(.top, -8)
}
private var optionCard: some View {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Color")
.font(.headline)
Spacer()
Text(selectedColor)
.font(.subheadline)
.foregroundStyle(.secondary)
}
HStack(spacing: 12) {
ForEach(colors, id: \.self) { color in
Button {
selectedColor = color
} label: {
Text(color)
.font(.caption)
.fontWeight(.bold)
.foregroundStyle(selectedColor == color ? .white : .primary)
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(selectedColor == color ? Color.primary : Color(.secondarySystemGroupedBackground))
.clipShape(Capsule())
}
}
}
}
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Size")
.font(.headline)
Spacer()
Button("Size guide") {
// size guide action
}
.font(.subheadline)
.fontWeight(.semibold)
}
HStack(spacing: 10) {
ForEach(sizes, id: \.self) { size in
Button {
selectedSize = size
} label: {
Text(size)
.font(.subheadline)
.fontWeight(.bold)
.foregroundStyle(selectedSize == size ? .white : .primary)
.frame(width: 48, height: 42)
.background(selectedSize == size ? Color.primary : Color(.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 14))
}
}
}
}
HStack {
Text("Quantity")
.font(.headline)
Spacer()
HStack(spacing: 16) {
Button {
quantity = max(1, quantity - 1)
} label: {
Image(systemName: "minus")
.frame(width: 38, height: 38)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(Circle())
}
Text("\(quantity)")
.font(.headline)
.frame(width: 24)
Button {
quantity += 1
} label: {
Image(systemName: "plus")
.frame(width: 38, height: 38)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(Circle())
}
}
}
}
.cardStyle()
.padding(.horizontal, 16)
}
private var sectionSelector: some View {
HStack(spacing: 8) {
ForEach(sections, id: \.self) { section in
Button {
selectedSection = section
} label: {
Text(section)
.font(.subheadline)
.fontWeight(.bold)
.foregroundStyle(selectedSection == section ? .white : .secondary)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(selectedSection == section ? Color.primary : Color.clear)
.clipShape(Capsule())
}
}
}
.padding(6)
.background(Color(.systemBackground))
.clipShape(Capsule())
.padding(.horizontal, 16)
}
@ViewBuilder
private var dynamicSectionCard: some View {
if selectedSection == "Details" {
detailsCard
} else if selectedSection == "Features" {
featuresCard
} else {
reviewsCard
}
}
private var detailsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Description")
.font(.headline)
Text("Designed for daily use, work and travel. It includes a waterproof layer, padded laptop compartment, anti-theft pocket and ergonomic back support.")
.font(.subheadline)
.foregroundStyle(.secondary)
.lineSpacing(4)
}
.cardStyle()
.padding(.horizontal, 16)
}
private var featuresCard: some View {
LazyVGrid(
columns: [
GridItem(.flexible()),
GridItem(.flexible())
],
spacing: 12
) {
featureItem(icon: "drop.fill", title: "Waterproof")
featureItem(icon: "laptopcomputer", title: "Laptop 16”")
featureItem(icon: "lock.fill", title: "Anti-theft")
featureItem(icon: "battery.100", title: "USB Port")
featureItem(icon: "leaf.fill", title: "Eco fabric")
featureItem(icon: "scalemass.fill", title: "Lightweight")
}
.cardStyle()
.padding(.horizontal, 16)
}
private var reviewsCard: some View {
VStack(spacing: 14) {
reviewItem(name: "Amanda Silva", text: "Excellent quality and beautiful finish.")
reviewItem(name: "Lucas Martins", text: "Very comfortable for daily use.")
}
.cardStyle()
.padding(.horizontal, 16)
}
private var deliveryCard: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Delivery Options")
.font(.headline)
deliveryRow(icon: "truck.box.fill", title: "Standard Delivery", subtitle: "2 to 5 business days", price: "Free")
deliveryRow(icon: "bolt.car.fill", title: "Express Delivery", subtitle: "1 to 2 business days", price: "$12.90")
deliveryRow(icon: "shippingbox.fill", title: "Store Pickup", subtitle: "Available today", price: "Free")
}
.cardStyle()
.padding(.horizontal, 16)
}
private var sellerCard: some View {
VStack(spacing: 16) {
HStack(spacing: 14) {
Image(systemName: "building.2.fill")
.font(.title2)
.foregroundStyle(.blue)
.frame(width: 54, height: 54)
.background(Color.blue.opacity(0.12))
.clipShape(Circle())
VStack(alignment: .leading, spacing: 4) {
Text("Urban Gear Store")
.font(.headline)
Text("Verified seller · 98% positive feedback")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.secondary)
}
Divider()
HStack {
sellerMetric(value: "12k+", label: "Sales")
sellerMetric(value: "4.9", label: "Rating")
sellerMetric(value: "24h", label: "Support")
}
}
.cardStyle()
.padding(.horizontal, 16)
}
private var similarProductsSection: some View {
VStack(alignment: .leading, spacing: 14) {
Text("You may also like")
.font(.headline)
.padding(.horizontal, 16)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 14) {
similarProduct(title: "Travel Organizer", price: "$24.90", icon: "rectangle.grid.1x2.fill")
similarProduct(title: "Laptop Sleeve", price: "$34.90", icon: "laptopcomputer")
similarProduct(title: "Bottle Holder", price: "$14.90", icon: "waterbottle.fill")
}
.padding(.horizontal, 16)
}
}
}
private var bottomBar: some View {
HStack(spacing: 14) {
VStack(alignment: .leading, spacing: 2) {
Text("Total")
.font(.caption)
.foregroundStyle(.secondary)
Text("$\(total, specifier: "%.2f")")
.font(.title3)
.fontWeight(.heavy)
}
Spacer()
Button {
// add to cart action
} label: {
Label("Add to Cart", systemImage: "cart.fill")
.font(.headline)
.fontWeight(.bold)
.foregroundStyle(.white)
.frame(height: 56)
.padding(.horizontal, 26)
.background(Color.primary)
.clipShape(RoundedRectangle(cornerRadius: 18))
}
}
.padding(.horizontal, 18)
.padding(.top, 12)
.padding(.bottom, 10)
.background(.ultraThinMaterial)
}
private func productBadge(icon: String, title: String, subtitle: String) -> some View {
VStack(spacing: 6) {
Image(systemName: icon)
.font(.headline)
Text(title)
.font(.caption)
.fontWeight(.bold)
Text(subtitle)
.font(.caption2)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
private func featureItem(icon: String, title: String) -> some View {
HStack(spacing: 10) {
Image(systemName: icon)
.foregroundStyle(.blue)
.frame(width: 34, height: 34)
.background(Color.blue.opacity(0.12))
.clipShape(Circle())
Text(title)
.font(.subheadline)
.fontWeight(.semibold)
Spacer()
}
.padding(12)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
private func deliveryRow(icon: String, title: String, subtitle: String, price: String) -> some View {
HStack(spacing: 12) {
Image(systemName: icon)
.foregroundStyle(.purple)
.frame(width: 44, height: 44)
.background(Color.purple.opacity(0.12))
.clipShape(Circle())
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.subheadline)
.fontWeight(.bold)
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(price)
.font(.subheadline)
.fontWeight(.bold)
}
}
private func reviewItem(name: String, text: String) -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Circle()
.fill(Color(.secondarySystemGroupedBackground))
.frame(width: 38, height: 38)
.overlay {
Text(String(name.prefix(1)))
.fontWeight(.bold)
}
VStack(alignment: .leading, spacing: 3) {
Text(name)
.font(.subheadline)
.fontWeight(.bold)
HStack(spacing: 2) {
ForEach(0..<5, id: \.self) { _ in
Image(systemName: "star.fill")
.font(.caption2)
.foregroundStyle(.yellow)
}
}
}
Spacer()
}
Text(text)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(14)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 18))
}
private func sellerMetric(value: String, label: String) -> some View {
VStack(spacing: 4) {
Text(value)
.font(.headline)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
}
private func similarProduct(title: String, price: String, icon: String) -> some View {
VStack(alignment: .leading, spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 18)
.fill(Color(.secondarySystemGroupedBackground))
.frame(width: 140, height: 120)
Image(systemName: icon)
.font(.largeTitle)
.foregroundStyle(.secondary)
}
Text(title)
.font(.subheadline)
.fontWeight(.bold)
.lineLimit(1)
Text(price)
.font(.headline)
.fontWeight(.heavy)
}
.frame(width: 140)
.padding(12)
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 22))
}
}
private extension View {
func cardStyle() -> some View {
self
.padding(20)
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 26))
}
}
#Preview {
ProductDetailsScreen()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment