Skip to main content

Complete iOS Shopping App

Learn how to build a full-featured iOS shopping app using the Reachu Swift SDK. This comprehensive example covers product browsing, cart management, and checkout flows with real-world best practices.

App Overview

We'll build a complete shopping app with the following features:

  • Product Catalog with search and filtering
  • Shopping Cart with quantity management
  • Checkout Flow with address and payment
  • User Profile and order history
  • Real-time Updates and error handling

Project Setup

1. Create New iOS Project

Create a new iOS project in Xcode and add the Reachu Swift SDK:

Package.swift
dependencies: [
.package(url: "https://github.com/ReachuDevteam/ReachuSwiftSDK.git", from: "1.0.0")
],
targets: [
.target(
name: "ShoppingApp",
dependencies: [
.product(name: "ReachuComplete", package: "ReachuSwiftSDK"),
]
),
]

2. App Configuration

ShoppingApp.swift
import SwiftUI
import ReachuCore

@main
struct ShoppingApp: App {

init() {
configureReachuSDK()
}

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(AppState())
}
}

private func configureReachuSDK() {
#if DEBUG
ReachuSDK.configure(
baseURL: "https://graph-ql-dev.reachu.io",
apiKey: "dev-api-key",
environment: .development
)
#else
ReachuSDK.configure(
baseURL: "https://graph-ql.reachu.io",
apiKey: "prod-api-key",
environment: .production
)
#endif
}
}

App State Management

Global App State

AppState.swift
import Foundation
import ReachuCore
import Combine

@MainActor
class AppState: ObservableObject {
// Navigation
@Published var selectedTab: Tab = .home
@Published var navigationPath = NavigationPath()

// Cart state
@Published var cart: Cart?
@Published var cartItemCount: Int = 0

// User state
@Published var isLoggedIn: Bool = false
@Published var userProfile: UserProfile?

// Global loading and error states
@Published var isLoading: Bool = false
@Published var errorMessage: String?

private let sdk = ReachuSDK.shared
private var cancellables = Set<AnyCancellable>()

init() {
setupObservers()
loadInitialData()
}

private func setupObservers() {
// Update cart item count when cart changes
$cart
.map { cart in
cart?.lineItems.reduce(0) { $0 + $1.quantity } ?? 0
}
.assign(to: &$cartItemCount)
}

private func loadInitialData() {
Task {
await loadCart()
}
}

func loadCart() async {
// Load existing cart or create new one
do {
// Try to load from stored cart ID
if let cartId = UserDefaults.standard.string(forKey: "cartId") {
self.cart = try await sdk.cart.getById(cartId)
} else {
// Create new cart
self.cart = try await sdk.cart.create(
customerSessionId: UUID().uuidString,
currency: "USD"
)
UserDefaults.standard.set(cart?.id, forKey: "cartId")
}
} catch {
print("Failed to load cart: \(error)")
}
}
}

enum Tab {
case home, search, cart, profile
}

Product Catalog

Product List View

ProductListView.swift
import SwiftUI
import ReachuUI
import ReachuCore

struct ProductListView: View {
@StateObject private var viewModel = ProductListViewModel()
@EnvironmentObject private var appState: AppState
@State private var selectedLayout: LayoutVariant = .grid

var body: some View {
NavigationStack(path: $appState.navigationPath) {
VStack(spacing: 0) {
// Search and filter bar
SearchFilterBar(
searchText: $viewModel.searchText,
selectedCategory: $viewModel.selectedCategory,
selectedLayout: $selectedLayout,
categories: viewModel.categories
)

// Product grid/list
ScrollView {
if viewModel.isLoading && viewModel.products.isEmpty {
LoadingView()
} else {
ProductGrid(
products: viewModel.products,
layout: selectedLayout,
onProductTap: { product in
appState.navigationPath.append(ProductDetailRoute(product: product))
},
onAddToCart: { product in
await viewModel.addToCart(product, appState: appState)
}
)
}

// Load more indicator
if viewModel.hasMoreProducts {
LoadMoreView()
.onAppear {
Task { await viewModel.loadMoreProducts() }
}
}
}
.refreshable {
await viewModel.refresh()
}
}
.navigationTitle("Products")
.navigationDestination(for: ProductDetailRoute.self) { route in
ProductDetailView(product: route.product)
}
.task {
await viewModel.loadInitialProducts()
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") { viewModel.errorMessage = nil }
} message: {
if let error = viewModel.errorMessage {
Text(error)
}
}
}
}
}

Product Grid Component

ProductGrid.swift
import SwiftUI
import ReachuUI

struct ProductGrid: View {
let products: [Product]
let layout: LayoutVariant
let onProductTap: (Product) -> Void
let onAddToCart: (Product) async -> Void

var body: some View {
LazyVGrid(columns: gridColumns, spacing: 16) {
ForEach(products) { product in
RProductCard(
product: product,
variant: layout.cardVariant,
showDescription: layout == .hero,
onTap: { onProductTap(product) },
onAddToCart: {
Task { await onAddToCart(product) }
}
)
.animation(.easeInOut, value: layout)
}
}
.padding()
}

private var gridColumns: [GridItem] {
switch layout {
case .grid:
return Array(repeating: GridItem(.flexible()), count: 2)
case .list, .hero:
return [GridItem(.flexible())]
}
}
}

enum LayoutVariant: CaseIterable {
case grid, list, hero

var cardVariant: RProductCard.Variant {
switch self {
case .grid: return .grid
case .list: return .list
case .hero: return .hero
}
}

var icon: String {
switch self {
case .grid: return "square.grid.2x2"
case .list: return "list.bullet"
case .hero: return "rectangle.stack"
}
}
}

Product ViewModel

ProductListViewModel.swift
import Foundation
import ReachuCore
import Combine

@MainActor
class ProductListViewModel: ObservableObject {
@Published var products: [Product] = []
@Published var filteredProducts: [Product] = []
@Published var categories: [Category] = []
@Published var searchText: String = ""
@Published var selectedCategory: Category?
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var hasMoreProducts: Bool = true

private let sdk = ReachuSDK.shared
private var currentPage = 0
private let pageSize = 20
private var cancellables = Set<AnyCancellable>()

init() {
setupSearchObserver()
}

private func setupSearchObserver() {
// Debounce search input
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] searchText in
Task { await self?.performSearch(searchText) }
}
.store(in: &cancellables)
}

func loadInitialProducts() async {
guard products.isEmpty else { return }

isLoading = true
errorMessage = nil

do {
// Load categories
async let categoriesTask = sdk.channel.category.getAll()

// Load initial products
async let productsTask = sdk.channel.product.getAll(
currency: "USD",
limit: pageSize,
offset: 0
)

let (loadedCategories, loadedProducts) = try await (categoriesTask, productsTask)

self.categories = loadedCategories
self.products = loadedProducts
self.hasMoreProducts = loadedProducts.count == pageSize
self.currentPage = 1

} catch {
self.errorMessage = error.localizedDescription
}

isLoading = false
}

func loadMoreProducts() async {
guard hasMoreProducts && !isLoading else { return }

isLoading = true

do {
let newProducts = try await sdk.channel.product.getAll(
currency: "USD",
limit: pageSize,
offset: currentPage * pageSize,
categoryId: selectedCategory?.id
)

self.products.append(contentsOf: newProducts)
self.hasMoreProducts = newProducts.count == pageSize
self.currentPage += 1

} catch {
self.errorMessage = error.localizedDescription
}

isLoading = false
}

func refresh() async {
currentPage = 0
products.removeAll()
hasMoreProducts = true
await loadInitialProducts()
}

private func performSearch(_ query: String) async {
guard !query.isEmpty else {
await refresh()
return
}

isLoading = true

do {
let searchResults = try await sdk.channel.product.search(
query: query,
currency: "USD",
limit: 50
)

self.products = searchResults
self.hasMoreProducts = false // Search doesn't support pagination

} catch {
self.errorMessage = error.localizedDescription
}

isLoading = false
}

func addToCart(_ product: Product, appState: AppState) async {
guard let cart = appState.cart else { return }

do {
let updatedCart = try await sdk.cart.addItem(
cartId: cart.id,
productId: product.id,
quantity: 1
)

appState.cart = updatedCart

// Show success feedback
HapticFeedback.success()

} catch {
self.errorMessage = "Failed to add item to cart: \(error.localizedDescription)"
HapticFeedback.error()
}
}
}

Shopping Cart

Cart View

CartView.swift
import SwiftUI
import ReachuUI
import ReachuCore

struct CartView: View {
@EnvironmentObject private var appState: AppState
@StateObject private var viewModel = CartViewModel()

var body: some View {
NavigationView {
VStack {
if let cart = appState.cart, !cart.lineItems.isEmpty {
// Cart items list
List {
ForEach(cart.lineItems) { item in
CartItemRow(
item: item,
onQuantityChange: { newQuantity in
await viewModel.updateQuantity(
lineItemId: item.id,
quantity: newQuantity,
appState: appState
)
},
onRemove: {
await viewModel.removeItem(
lineItemId: item.id,
appState: appState
)
}
)
}
.onDelete { indexSet in
Task {
for index in indexSet {
await viewModel.removeItem(
lineItemId: cart.lineItems[index].id,
appState: appState
)
}
}
}
}

// Cart summary and checkout
VStack(spacing: ReachuSpacing.md) {
CartSummary(totals: cart.totals)

RButton(
title: "Proceed to Checkout",
style: .primary,
size: .large,
isLoading: viewModel.isLoading
) {
await viewModel.proceedToCheckout(appState: appState)
}
.disabled(cart.lineItems.isEmpty)
}
.padding()
.background(ReachuColors.surface)

} else {
// Empty cart state
EmptyCartView()
}
}
.navigationTitle("Shopping Cart")
.toolbar {
if let cart = appState.cart, !cart.lineItems.isEmpty {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Clear") {
Task { await viewModel.clearCart(appState: appState) }
}
.foregroundColor(.red)
}
}
}
}
}
}

Cart Item Row

CartItemRow.swift
import SwiftUI
import ReachuUI
import ReachuCore

struct CartItemRow: View {
let item: LineItem
let onQuantityChange: (Int) async -> Void
let onRemove: () async -> Void

@State private var isUpdating = false

var body: some View {
HStack(spacing: ReachuSpacing.md) {
// Product image
AsyncImage(url: URL(string: item.product.images.first?.url ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(ReachuColors.background)
.overlay(
Image(systemName: "photo")
.foregroundColor(ReachuColors.textSecondary)
)
}
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: ReachuBorderRadius.medium))

// Product details
VStack(alignment: .leading, spacing: ReachuSpacing.xs) {
Text(item.product.title)
.font(ReachuTypography.body)
.foregroundColor(ReachuColors.textPrimary)
.lineLimit(2)

if let brand = item.product.brand {
Text(brand)
.font(ReachuTypography.caption1)
.foregroundColor(ReachuColors.textSecondary)
}

Text(item.price.displayAmount)
.font(ReachuTypography.bodyBold)
.foregroundColor(ReachuColors.primary)
}

Spacer()

// Quantity controls
VStack(spacing: ReachuSpacing.sm) {
HStack(spacing: ReachuSpacing.xs) {
Button {
Task {
isUpdating = true
await onQuantityChange(max(1, item.quantity - 1))
isUpdating = false
}
} label: {
Image(systemName: "minus.circle.fill")
.foregroundColor(ReachuColors.textSecondary)
}
.disabled(item.quantity <= 1 || isUpdating)

Text("\(item.quantity)")
.font(ReachuTypography.bodyBold)
.frame(minWidth: 30)

Button {
Task {
isUpdating = true
await onQuantityChange(item.quantity + 1)
isUpdating = false
}
} label: {
Image(systemName: "plus.circle.fill")
.foregroundColor(ReachuColors.primary)
}
.disabled(isUpdating)
}

Button("Remove") {
Task { await onRemove() }
}
.font(ReachuTypography.caption1)
.foregroundColor(.red)
}
}
.opacity(isUpdating ? 0.6 : 1.0)
.animation(.easeInOut(duration: 0.2), value: isUpdating)
}
}

Global Checkout System

Modern Approach: RCheckoutOverlay + CartManager

The new Reachu Swift SDK includes a complete global checkout system that works across your entire app. This modern approach provides better user experience and easier implementation.

App.swift - Global Setup
import SwiftUI
import ReachuCore
import ReachuUI

@main
struct ShoppingApp: App {

init() {
configureReachuSDK()
}

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(CartManager()) // Global cart state
}
}

private func configureReachuSDK() {
ReachuSDK.configure(
baseURL: "https://graph-ql.reachu.io",
apiKey: "your-api-key",
environment: .production
)
}
}

Main Content View with Global Checkout

ContentView.swift - Global Checkout Integration
import SwiftUI
import ReachuCore
import ReachuUI

struct ContentView: View {
@EnvironmentObject var cartManager: CartManager

var body: some View {
TabView {
ProductCatalogView()
.tabItem {
Image(systemName: "bag.fill")
Text("Products")
}

ShoppingCartView()
.tabItem {
Image(systemName: "cart.fill")
Text("Cart")
}
.badge(cartManager.itemCount > 0 ? cartManager.itemCount : nil)

ProfileView()
.tabItem {
Image(systemName: "person.fill")
Text("Profile")
}
}
// Global checkout overlay - works from anywhere in the app
.sheet(isPresented: $cartManager.isCheckoutPresented) {
RCheckoutOverlay()
.environmentObject(cartManager)
}
}
}

Product Catalog with Global Cart Integration

ProductCatalogView.swift - One-Click Add to Cart
import SwiftUI
import ReachuUI
import ReachuCore

struct ProductCatalogView: View {
@EnvironmentObject var cartManager: CartManager
@StateObject private var viewModel = ProductCatalogViewModel()

var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: ReachuSpacing.md) {
ForEach(viewModel.products) { product in
RProductCard(
product: product,
variant: .grid,
onTap: {
viewModel.showProductDetail(product)
},
onAddToCart: {
Task {
await cartManager.addProduct(product)
}
}
)
}
}
.padding(ReachuSpacing.lg)
}
.navigationTitle("Products")
.task {
await viewModel.loadProducts()
}
}
}
}

Shopping Cart View with Modern Controls

ShoppingCartView.swift - Full Cart Management
import SwiftUI
import ReachuUI
import ReachuDesignSystem

struct ShoppingCartView: View {
@EnvironmentObject var cartManager: CartManager

var body: some View {
NavigationView {
VStack(spacing: 0) {
if cartManager.items.isEmpty {
// Empty cart state
VStack(spacing: ReachuSpacing.lg) {
Spacer()

Image(systemName: "cart")
.font(.system(size: 48))
.foregroundColor(ReachuColors.textSecondary)

Text("Your cart is empty")
.font(ReachuTypography.headline)
.foregroundColor(ReachuColors.textSecondary)

Text("Add some products to get started")
.font(ReachuTypography.body)
.foregroundColor(ReachuColors.textTertiary)

Spacer()
}
} else {
// Cart items list
ScrollView {
LazyVStack(spacing: ReachuSpacing.md) {
ForEach(cartManager.items) { item in
CartItemRowView(item: item)
.environmentObject(cartManager)
}
}
.padding(ReachuSpacing.lg)
}

Spacer()

// Checkout section with total
VStack(spacing: 0) {
Divider()

VStack(spacing: ReachuSpacing.md) {
// Cart total
HStack {
Text("Total")
.font(ReachuTypography.headline)

Spacer()

Text("\(cartManager.currency) \(String(format: "%.2f", cartManager.cartTotal))")
.font(ReachuTypography.headline)
.foregroundColor(ReachuColors.primary)
}

// Global checkout button
RButton(
title: "Proceed to Checkout",
style: .primary,
size: .large,
isLoading: cartManager.isLoading
) {
cartManager.showCheckout() // Opens global overlay
}
}
.padding(ReachuSpacing.lg)
}
.background(ReachuColors.surface)
}
}
.navigationTitle("Cart (\(cartManager.itemCount))")
.toolbar {
if !cartManager.items.isEmpty {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Clear") {
Task {
await cartManager.clearCart()
}
}
.foregroundColor(ReachuColors.error)
}
}
}
}
}
}

Cart Item Row with Real-time Updates

CartItemRowView.swift - Interactive Cart Items
import SwiftUI
import ReachuUI
import ReachuDesignSystem

struct CartItemRowView: View {
let item: CartManager.CartItem
@EnvironmentObject var cartManager: CartManager

var body: some View {
HStack(spacing: ReachuSpacing.md) {
// Product image with AsyncImage
AsyncImage(url: URL(string: item.imageUrl ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(ReachuColors.background)
.overlay {
Image(systemName: "photo")
.foregroundColor(ReachuColors.textSecondary)
}
}
.frame(width: 60, height: 60)
.cornerRadius(ReachuBorderRadius.medium)

// Product information
VStack(alignment: .leading, spacing: ReachuSpacing.xs) {
Text(item.title)
.font(ReachuTypography.bodyBold)
.lineLimit(2)

if let brand = item.brand {
Text(brand)
.font(ReachuTypography.caption1)
.foregroundColor(ReachuColors.textSecondary)
}

Text("\(item.currency) \(String(format: "%.2f", item.price))")
.font(ReachuTypography.body)
.foregroundColor(ReachuColors.primary)
}

Spacer()

// Quantity controls with real-time updates
VStack(spacing: ReachuSpacing.xs) {
HStack(spacing: ReachuSpacing.xs) {
// Decrease quantity
Button("-") {
if item.quantity > 1 {
Task {
await cartManager.updateQuantity(for: item, to: item.quantity - 1)
}
}
}
.frame(width: 32, height: 32)
.background(ReachuColors.background)
.cornerRadius(ReachuBorderRadius.small)

// Current quantity
Text("\(item.quantity)")
.font(ReachuTypography.bodyBold)
.frame(minWidth: 24)

// Increase quantity
Button("+") {
Task {
await cartManager.updateQuantity(for: item, to: item.quantity + 1)
}
}
.frame(width: 32, height: 32)
.background(ReachuColors.background)
.cornerRadius(ReachuBorderRadius.small)
}

// Remove item
Button("Remove") {
Task {
await cartManager.removeItem(item)
}
}
.font(ReachuTypography.caption1)
.foregroundColor(ReachuColors.error)
}
}
.padding(ReachuSpacing.md)
.background(ReachuColors.surface)
.cornerRadius(ReachuBorderRadius.medium)
.shadow(color: ReachuColors.textPrimary.opacity(0.05), radius: 2, x: 0, y: 1)
}
}

Floating Cart Button (Optional)

FloatingCartButton.swift - Quick Checkout Access
import SwiftUI
import ReachuUI
import ReachuDesignSystem

struct FloatingCartButton: View {
@EnvironmentObject var cartManager: CartManager

var body: some View {
if cartManager.itemCount > 0 {
VStack {
Spacer()
HStack {
Spacer()

Button(action: {
cartManager.showCheckout() // Opens checkout from anywhere
}) {
HStack {
Image(systemName: "cart.fill")
Text("\(cartManager.itemCount)")
Text("•")
Text("\(cartManager.currency) \(String(format: "%.0f", cartManager.cartTotal))")
}
.padding(.horizontal, ReachuSpacing.lg)
.padding(.vertical, ReachuSpacing.md)
.background(ReachuColors.primary)
.foregroundColor(.white)
.cornerRadius(ReachuBorderRadius.circle)
.shadow(radius: 4)
}

Spacer()
}
.padding(.bottom, ReachuSpacing.xl)
}
}
}
}

Benefits of the Global Checkout System

  1. ✅ Works Everywhere: Checkout can be triggered from any screen in your app
  2. ✅ Real-time Updates: Cart state updates instantly across all views
  3. ✅ Modal Experience: Checkout appears as overlay without disrupting navigation
  4. ✅ Production Ready: Handles errors, loading states, and edge cases
  5. ✅ Cross-Platform: Works on iOS, macOS, tvOS, and watchOS
  6. ✅ Easy Integration: Just add CartManager as environment object

Key Features Comparison

FeatureTraditional CheckoutGlobal Checkout System
Trigger LocationOnly from cart screenFrom anywhere in app
NavigationPush/present new screenModal overlay
State ManagementLocal to checkout flowGlobal reactive state
User ExperienceInterrupts navigationMaintains context
Integration EffortHigh (custom implementation)Low (built-in components)
Real-time UpdatesManual implementationAutomatic via @Published
Error HandlingCustom implementationBuilt-in comprehensive handling
NavigationRoutes.swift
import Foundation
import ReachuCore

// Route definitions for type-safe navigation
struct ProductDetailRoute: Hashable {
let product: Product
}

struct CategoryRoute: Hashable {
let category: Category
}

struct CheckoutRoute: Hashable {
let cartId: String
}

struct OrderDetailRoute: Hashable {
let orderId: String
}

Main Tab View

MainTabView.swift
import SwiftUI
import ReachuUI

struct MainTabView: View {
@EnvironmentObject private var appState: AppState

var body: some View {
TabView(selection: $appState.selectedTab) {
ProductListView()
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(Tab.home)

SearchView()
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
.tag(Tab.search)

CartView()
.tabItem {
Image(systemName: "cart.fill")
Text("Cart")
}
.badge(appState.cartItemCount > 0 ? appState.cartItemCount : nil)
.tag(Tab.cart)

ProfileView()
.tabItem {
Image(systemName: "person.fill")
Text("Profile")
}
.tag(Tab.profile)
}
.accentColor(ReachuColors.primary)
}
}

Performance Optimizations

Image Caching

ImageCache.swift
import Foundation
import UIKit

actor ImageCache {
static let shared = ImageCache()

private var cache: [URL: UIImage] = [:]
private let maxCacheSize = 100

private init() {}

func image(for url: URL) -> UIImage? {
return cache[url]
}

func setImage(_ image: UIImage, for url: URL) {
if cache.count >= maxCacheSize {
// Remove oldest entries
let keysToRemove = Array(cache.keys.prefix(10))
keysToRemove.forEach { cache.removeValue(forKey: $0) }
}

cache[url] = image
}
}

Data Loading Strategy

DataLoader.swift
import Foundation
import ReachuCore

@MainActor
class DataLoader: ObservableObject {
@Published var isLoading = false
private var loadingTasks: [String: Task<Void, Never>] = [:]

func loadData<T>(
id: String,
operation: @escaping () async throws -> T,
completion: @escaping (Result<T, Error>) -> Void
) {
// Cancel existing task with same ID
loadingTasks[id]?.cancel()

loadingTasks[id] = Task {
isLoading = true
defer { isLoading = false }

do {
let result = try await operation()
completion(.success(result))
} catch {
completion(.failure(error))
}

loadingTasks.removeValue(forKey: id)
}
}

func cancelAll() {
loadingTasks.values.forEach { $0.cancel() }
loadingTasks.removeAll()
}
}

Testing

Unit Tests

ProductViewModelTests.swift
import XCTest
@testable import ShoppingApp
import ReachuTesting

final class ProductViewModelTests: XCTestCase {
var viewModel: ProductListViewModel!

override func setUp() {
super.setUp()
// Configure SDK for testing
ReachuSDK.configure(
baseURL: "https://mock.reachu.io",
apiKey: "test-key",
environment: .development
)
viewModel = ProductListViewModel()
}

func testLoadInitialProducts() async {
// Given
XCTAssertTrue(viewModel.products.isEmpty)

// When
await viewModel.loadInitialProducts()

// Then
XCTAssertFalse(viewModel.products.isEmpty)
XCTAssertFalse(viewModel.isLoading)
}

func testSearchProducts() async {
// Given
await viewModel.loadInitialProducts()
let initialCount = viewModel.products.count

// When
viewModel.searchText = "headphones"

// Wait for debounced search
try? await Task.sleep(nanoseconds: 600_000_000)

// Then
XCTAssertNotEqual(viewModel.products.count, initialCount)
}
}

SwiftUI Previews

ProductListView+Preview.swift
#if DEBUG
import SwiftUI
import ReachuTesting

struct ProductListView_Previews: PreviewProvider {
static var previews: some View {
ProductListView()
.environmentObject(AppState())
.previewDisplayName("Product List")
}
}
#endif

This complete example demonstrates how to build a production-ready iOS shopping app using the Reachu Swift SDK. The modular architecture, proper state management, and comprehensive error handling ensure a robust and maintainable application.


Production Ready

This example includes production-ready patterns like proper error handling, loading states, image caching, and comprehensive testing. Use it as a foundation for your own ecommerce applications.