Offer Banner Implementation Example
This example shows how to implement the ROfferBanner component in a complete iOS app, including integration with shopping cart functionality.
Complete App Example
import SwiftUI
import ReachuUI
import ReachuCore
struct ContentView: View {
@StateObject private var cartManager = CartManager()
@StateObject private var checkoutDraft = CheckoutDraft()
var body: some View {
NavigationView {
ZStack {
// Background
Color(.systemGroupedBackground)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
// Header
headerView
// Dynamic offer banner
offerBannerSection
// Products section
productsSection
// Categories section
categoriesSection
}
.padding()
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
cartButton
}
}
}
.environmentObject(cartManager)
.environmentObject(checkoutDraft)
.sheet(isPresented: $cartManager.isCheckoutPresented) {
RCheckoutOverlay()
.environmentObject(cartManager)
.environmentObject(checkoutDraft)
}
}
// MARK: - Header View
private var headerView: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Welcome to Our Store")
.font(.largeTitle)
.fontWeight(.bold)
Text("Discover amazing deals and offers")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
// MARK: - Offer Banner Section
private var offerBannerSection: some View {
VStack(spacing: 12) {
Text("Special Offers")
.font(.headline)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
// Dynamic offer banner from backend (campaign ID from config)
ROfferBannerDynamic()
.frame(height: 160)
}
}
// MARK: - Products Section
private var productsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Featured Products")
.font(.headline)
.fontWeight(.semibold)
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 16) {
ForEach(sampleProducts) { product in
ProductCard(product: product)
.environmentObject(cartManager)
}
}
}
}
// MARK: - Categories Section
private var categoriesSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Categories")
.font(.headline)
.fontWeight(.semibold)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(sampleCategories) { category in
CategoryCard(category: category)
}
}
.padding(.horizontal, 4)
}
}
}
// MARK: - Cart Button
private var cartButton: some View {
Button(action: {
cartManager.isCheckoutPresented = true
}) {
ZStack {
Image(systemName: "cart.fill")
.font(.system(size: 20))
.foregroundColor(.primary)
if cartManager.items.count > 0 {
Text("\(cartManager.items.count)")
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(width: 18, height: 18)
.background(Color.red)
.clipShape(Circle())
.offset(x: 12, y: -12)
}
}
}
}
}
// MARK: - Sample Data
extension ContentView {
private var sampleProducts: [Product] {
[
Product(
id: "1",
title: "Premium Headphones",
description: "High-quality wireless headphones",
price: ProductPrice(amount: 199.99, currency: "USD"),
imageUrl: "https://example.com/headphones.jpg"
),
Product(
id: "2",
title: "Smart Watch",
description: "Advanced fitness tracking watch",
price: ProductPrice(amount: 299.99, currency: "USD"),
imageUrl: "https://example.com/watch.jpg"
),
Product(
id: "3",
title: "Wireless Speaker",
description: "Portable Bluetooth speaker",
price: ProductPrice(amount: 149.99, currency: "USD"),
imageUrl: "https://example.com/speaker.jpg"
),
Product(
id: "4",
title: "Gaming Mouse",
description: "High-precision gaming mouse",
price: ProductPrice(amount: 79.99, currency: "USD"),
imageUrl: "https://example.com/mouse.jpg"
)
]
}
private var sampleCategories: [Category] {
[
Category(id: "1", name: "Electronics", imageUrl: "https://example.com/electronics.jpg"),
Category(id: "2", name: "Clothing", imageUrl: "https://example.com/clothing.jpg"),
Category(id: "3", name: "Home", imageUrl: "https://example.com/home.jpg"),
Category(id: "4", name: "Sports", imageUrl: "https://example.com/sports.jpg")
]
}
}
// MARK: - Supporting Views
struct ProductCard: View {
let product: Product
@EnvironmentObject private var cartManager: CartManager
var body: some View {
VStack(alignment: .leading, spacing: 8) {
AsyncImage(url: URL(string: product.imageUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
}
.frame(height: 120)
.clipped()
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text(product.title)
.font(.headline)
.lineLimit(2)
Text(product.description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
Text("$\(product.price.amount, specifier: "%.2f")")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.primary)
}
Spacer()
Button("Add to Cart") {
cartManager.addProduct(product)
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
}
}
struct CategoryCard: View {
let category: Category
var body: some View {
VStack(spacing: 8) {
AsyncImage(url: URL(string: category.imageUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
}
.frame(width: 80, height: 80)
.clipped()
.cornerRadius(8)
Text(category.name)
.font(.caption)
.fontWeight(.medium)
.multilineTextAlignment(.center)
}
.frame(width: 80)
}
}
// MARK: - Sample Models
struct Product: Identifiable {
let id: String
let title: String
let description: String
let price: ProductPrice
let imageUrl: String
}
struct ProductPrice {
let amount: Double
let currency: String
}
struct Category: Identifiable {
let id: String
let name: String
let imageUrl: String
}
#Preview {
ContentView()
}
Key Implementation Points
1. Simple Integration
// Just one line to add the dynamic banner - no parameters needed!
ROfferBannerDynamic()
2. Automatic Lifecycle ManagementThe container automatically handles:
- WebSocket connection/disconnection
- API calls for initial data
- Real-time updates
- Error handling
3. Environment Objects
.environmentObject(cartManager)
.environmentObject(checkoutDraft)
4. Sheet Presentation
.sheet(isPresented: $cartManager.isCheckoutPresented) {
RCheckoutOverlay()
.environmentObject(cartManager)
.environmentObject(checkoutDraft)
}
Configuration
Global Singleton Configuration
The example uses the global singleton pattern where the ComponentManager.shared automatically reads the campaignId from your reachu-config.json file:
{
"liveShow": {
"campaignId": 3,
"tipio": {
"apiKey": "your-api-key",
"baseUrl": "https://your-backend.com"
}
}
}
This means you can use ROfferBannerDynamic() without passing any parameters - the campaign ID is automatically retrieved from the configuration and the ComponentManager.shared singleton handles everything automatically.
Backend Configuration
To make this example work, you need to set up your backend with:
1. Active Components API
GET /api/campaigns/3/active-components
2. WebSocket Connection
wss://your-server.com/ws/3
3. Sample Banner Configuration
{
"id": "banner-1",
"type": "offer_banner",
"config": {
"logoUrl": "https://example.com/logo.png",
"title": "Black Friday Sale",
"subtitle": "Up to 50% off everything",
"backgroundImageUrl": "https://example.com/background.jpg",
"countdownEndDate": "2025-12-31T23:59:59Z",
"discountBadgeText": "50% OFF",
"ctaText": "Shop Now →",
"ctaLink": "https://example.com/sale",
"overlayOpacity": 0.4,
"buttonColor": "#FF6B6B"
}
}
Testing the Implementation
- Run the appwith the code above
- Check the banner appearswhen you have active components
- Test WebSocket updatesby changing the backend configuration
- Verify countdown timerworks correctly
- Test CTA buttonopens the correct link
This example provides a complete foundation for integrating the Offer Banner component into your iOS app with shopping cart functionality.