RProductCard
A flexible and powerful SwiftUI component for displaying products in different layouts. The RProductCard
automatically adapts to various use cases with built-in image loading, error handling, multiple visual variants, and sophisticated animations for enhanced user feedback.
Overview
The RProductCard
is designed to be the primary component for displaying products throughout your app. It supports multiple layout variants, handles multiple images with swipe navigation, and includes comprehensive error states.
Basic Usage
import SwiftUI
import ReachuUI
import ReachuCore
struct ProductGridView: View {
let products: [Product]
var body: some View {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
]) {
ForEach(products) { product in
RProductCard(
product: product,
variant: .grid,
onTap: {
// Navigate to product detail
},
onAddToCart: {
// Add to cart logic
}
)
}
}
}
}
Layout Variants
The RProductCard
supports four distinct layout variants, each optimized for different use cases:
Grid Variant
Perfect for product grids and catalog views.
RProductCard(
product: product,
variant: .grid,
showBrand: true,
onAddToCart: { addToCart(product) }
)
Features:
- ✅ Square image area (160pt height)
- ✅ Product title, brand, and price
- ✅ Add to Cart button
- ✅ Multiple image support with swipe navigation
- ✅ Optimized for 2-column grids
List Variant
Ideal for search results and compact product lists.
RProductCard(
product: product,
variant: .list,
showDescription: true,
onTap: { showProductDetail(product) }
)
Features:
- ✅ Horizontal layout with small image (80pt)
- ✅ Space-efficient design
- ✅ Optional description text
- ✅ Compact Add to Cart button
Hero Variant
Showcase products with large, prominent display.
RProductCard(
product: product,
variant: .hero,
showBrand: true,
showDescription: true,
onAddToCart: { addToCart(product) }
)
Features:
- ✅ Large image area (240pt height)
- ✅ Full product information display
- ✅ Prominent Add to Cart button
- ✅ Perfect for featured products
- ✅ Enhanced shadow and styling
Minimal Variant
Compact cards for carousels and space-constrained areas.
RProductCard(
product: product,
variant: .minimal,
onTap: { showProductDetail(product) }
)
Features:
- ✅ Fixed width (120pt) for horizontal scrolling
- ✅ Basic product info only
- ✅ No Add to Cart button (tap-only interaction)
- ✅ Perfect for product carousels
Advanced Features
Multiple Images with Swipe Navigation
The RProductCard
automatically handles multiple product images with smooth swipe navigation:
// Product with multiple images
let product = Product(
id: "123",
title: "Wireless Headphones",
images: [
ProductImage(id: "1", url: "https://example.com/image1.jpg", order: 0),
ProductImage(id: "2", url: "https://example.com/image2.jpg", order: 1),
ProductImage(id: "3", url: "https://example.com/image3.jpg", order: 2),
],
// ... other properties
)
// Grid and Hero variants automatically show image pagination
RProductCard(product: product, variant: .grid)
Image Features:
- ✅ Smart Ordering: Prioritizes images with
order
0 and 1 - ✅ Swipe Navigation: TabView with page indicators (iOS/tvOS/watchOS)
- ✅ Single Image Optimization: List and minimal variants show primary image only
- ✅ Loading States: Shimmer effect while images load
- ✅ Error Handling: Fallback icons for broken images
- ✅ Button Animations: Scale effects and loading spinners
- ✅ Success Feedback: Temporary "Added!" state with checkmarks
- ✅ Haptic Feedback: Tactile confirmation on iOS devices
Comprehensive Options
RProductCard(
product: product,
variant: .hero,
showBrand: true, // Show product brand
showDescription: true, // Show product description
onTap: { // Handle card tap
showProductDetail(product)
},
onAddToCart: { // Handle add to cart
cartManager.add(product)
}
)
Styling & Customization
Design System Integration
The RProductCard
automatically uses your app's design system:
// Colors from ReachuColors
.foregroundColor(ReachuColors.textPrimary)
.backgroundColor(ReachuColors.surface)
// Typography from ReachuTypography
.font(ReachuTypography.headline)
// Spacing from ReachuSpacing
.padding(ReachuSpacing.md)
// Border radius from ReachuBorderRadius
.cornerRadius(ReachuBorderRadius.large)
Stock Status Handling
Out-of-stock products are handled automatically:
// Product with zero quantity
let outOfStockProduct = Product(
id: "456",
title: "Sold Out Item",
quantity: 0, // highlight-line
// ... other properties
)
// Card automatically shows "Out of Stock" instead of Add to Cart
RProductCard(product: outOfStockProduct, variant: .grid)
Stock Features:
- ✅ Automatic Detection: Based on
product.quantity
- ✅ Visual Feedback: "Out of Stock" badge with error styling
- ✅ Disabled Interactions: Add to Cart button becomes non-interactive
- ✅ Consistent Styling: Maintains layout while showing unavailability
Real-World Examples
Product Catalog Grid
struct ProductCatalogView: View {
@StateObject private var viewModel = CatalogViewModel()
@State private var selectedVariant: LayoutVariant = .grid
var body: some View {
VStack {
// Layout variant picker
Picker("Layout", selection: $selectedVariant) {
Text("Grid").tag(LayoutVariant.grid)
Text("List").tag(LayoutVariant.list)
Text("Hero").tag(LayoutVariant.hero)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
ScrollView {
LazyVGrid(columns: gridColumns, spacing: 16) {
ForEach(viewModel.products) { product in
RProductCard(
product: product,
variant: selectedVariant.cardVariant,
showDescription: selectedVariant == .hero,
onTap: {
viewModel.showProductDetail(product)
},
onAddToCart: {
viewModel.addToCart(product)
}
)
}
}
.padding()
}
}
}
private var gridColumns: [GridItem] {
switch selectedVariant {
case .grid:
return Array(repeating: GridItem(.flexible()), count: 2)
case .list, .hero:
return [GridItem(.flexible())]
}
}
}
Featured Products Carousel
struct FeaturedProductsCarousel: View {
let featuredProducts: [Product]
var body: some View {
VStack(alignment: .leading) {
Text("Featured Products")
.font(ReachuTypography.headline)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: ReachuSpacing.sm) {
ForEach(featuredProducts) { product in
RProductCard(
product: product,
variant: .minimal,
onTap: { showProductDetail(product) }
)
}
}
.padding(.horizontal)
}
}
}
}
Mixed Layout Product Feed
struct ProductFeedView: View {
let products: [Product]
var body: some View {
LazyVStack(spacing: ReachuSpacing.lg) {
ForEach(Array(products.enumerated()), id: \.element.id) { index, product in
Group {
if index == 0 {
// First product as hero
RProductCard(
product: product,
variant: .hero,
showDescription: true
)
} else if index % 5 == 0 {
// Every 5th product as hero
RProductCard(product: product, variant: .hero)
} else {
// Regular products as list
RProductCard(product: product, variant: .list)
}
}
}
}
.padding()
}
}
✨ Animation & Feedback System
RProductCard
includes a sophisticated animation system that provides immediate visual and tactile feedback when users interact with the "Add to Cart" button.
Automatic Animation Sequence
When a user taps "Add to Cart", the following sequence occurs automatically:
1. Button Scale Animation (0.1s) // Button scales to 0.9x
2. Loading State (variable) // Shows spinner while processing
3. Success State (1.5s) // Shows "Added!" with checkmark
4. Return to Normal (0.3s) // Fades back to original state
Visual Feedback States
Normal State
RButton(
title: "Add to Cart",
style: .primary,
size: .medium
)
Loading State
RButton(
title: "Add to Cart",
style: .primary,
size: .medium,
isLoading: true // Shows spinner automatically
)
Success State
RButton(
title: "Added!", // Changes to success text
style: .primary,
size: .medium,
icon: "checkmark" // Shows checkmark icon
)
Haptic Feedback Integration
On iOS devices, the card provides tactile feedback:
// Light impact when button is pressed
UIImpactFeedbackGenerator(style: .light).impactOccurred()
// Success notification when item is added
UINotificationFeedbackGenerator().notificationOccurred(.success)
Haptic Timeline:
- Button Press → Light impact feedback
- Successful Add → Success notification pattern
- Error → Error notification pattern (if operation fails)
Cross-Platform Behavior
iOS
- Full haptic feedback support
- Smooth spring animations with proper timing
- Respects accessibility settings for reduced motion
macOS
- Visual animations without haptic feedback
- Proper hover states and cursor interaction
- Keyboard navigation support
tvOS
- Adapted for remote control interaction
- Focus management compatible
- Appropriate timing for TV interface
watchOS
- Simplified animations for performance
- Watch-specific haptic patterns when available
- Optimized for small screen interactions
Animation Customization
While animations are automatic, you can customize the experience:
// The animation system uses these built-in timings:
.animation(.easeInOut(duration: 0.1), value: buttonScale) // Press animation
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: showCheckmark) // Success animation
.animation(.easeInOut(duration: 0.3), value: isAddingToCart) // Reset animation
Integration with Global Systems
The card animations work seamlessly with other Reachu UX components:
// When user taps "Add to Cart":
1. RProductCard animates button // ✅ Local feedback
2. ToastManager shows notification // ✅ Global feedback
3. CartManager updates state // ✅ State management
4. RFloatingCartIndicator updates // ✅ Persistent indicator
5. Haptic feedback fires // ✅ Tactile confirmation
Animation Performance
The animation system is optimized for smooth performance:
- ✅ Hardware Acceleration - Uses Core Animation for smooth rendering
- ✅ State Management - Minimal re-renders with
@State
variables - ✅ Timer Management - Proper cleanup of animation timers
- ✅ Memory Efficiency - No memory leaks from animation callbacks
Accessibility & Animations
The animation system respects accessibility preferences:
// Respects reduced motion preferences
@Environment(\.accessibilityReduceMotion) var reduceMotion
// Shortened animations when reduce motion is enabled
.animation(reduceMotion ? .none : .spring(...), value: animationState)
// Alternative feedback for users who can't see animations
.accessibilityAnnouncement("Product added to cart")
Debug & Testing
To test animations during development:
#if DEBUG
// Slow down animations for testing
.animation(.spring(response: 2.0, dampingFraction: 0.6), value: showCheckmark)
// Add visual debugging
.background(showCheckmark ? Color.green.opacity(0.3) : Color.clear)
#endif
Accessibility
The RProductCard
includes comprehensive accessibility support:
// Automatic accessibility labels
.accessibilityLabel("\(product.title), \(product.price.displayAmount)")
// Add to cart button accessibility
.accessibilityHint("Add this product to your shopping cart")
// Stock status accessibility
.accessibilityValue(isInStock ? "In stock" : "Out of stock")
// Image accessibility
.accessibilityLabel("Product image for \(product.title)")
Performance Considerations
Image Loading Optimization
- ✅ Lazy Loading: Images load only when visible
- ✅ Automatic Caching: Built-in image caching via
AsyncImage
- ✅ Memory Management: Proper image disposal when cards scroll off-screen
- ✅ Progressive Loading: Placeholder → Image → Error state progression
Best Practices
- Use appropriate variants for your layout needs
- Implement proper data loading patterns with ViewModels
- Handle loading states at the collection level
- Optimize image URLs (use appropriate sizes from your CDN)
- Test with various screen sizes and accessibility settings
Component Usage Guidelines
When to Use Each Variant
The RProductCard
variants are designed for specific use cases. Choose the right variant based on your context:
Variant | Best For | Card Size | Information Density | Primary Use Cases |
---|---|---|---|---|
Grid | Product catalogs | Medium (flexible) | Balanced | Main product grids, category pages, search results |
List | Compact listings | Small (80pt image) | High | Search results, cart items, compact product lists |
Hero | Featured products | Large (240pt image) | Full | Homepage features, product spotlights, promotions |
Minimal | Quick browsing | Small (120pt wide) | Minimal | Carousels, recommendations, related products |
Design System Integration
// All variants automatically use design system tokens
RProductCard(product: product, variant: .grid)
// Uses ReachuSpacing.md for internal spacing
// Uses ReachuColors.surface for background
// Uses ReachuTypography.headline for title
// Uses ReachuBorderRadius.large for corners
Responsive Behavior
struct ResponsiveProductGrid: View {
let products: [Product]
@Environment(\.horizontalSizeClass) var sizeClass
var columns: [GridItem] {
switch sizeClass {
case .compact:
return Array(repeating: GridItem(.flexible()), count: 2)
default:
return Array(repeating: GridItem(.flexible()), count: 3)
}
}
var body: some View {
LazyVGrid(columns: columns) {
ForEach(products) { product in
RProductCard(
product: product,
variant: sizeClass == .compact ? .grid : .hero,
showDescription: sizeClass != .compact
)
}
}
}
}
Use the Hero variant sparingly for featured products, Grid variant for main catalogs, List variant for search results, and Minimal variant for recommendations. Consider screen size and information hierarchy when choosing variants.