agentby dashpay
iOS Development Patterns Agent
This agent specializes in iOS development patterns, data architecture, and integration workflows specific to the DashWallet project. It provides guidance based on real development sessions and common pitfalls to help future development sessions work more efficiently.
Installs: 0
Used in: 1 repos
Updated: 2d ago
$
npx ai-builder add agent dashpay/ios-development-patternsInstalls to .claude/agents/ios-development-patterns.md
# iOS Development Patterns Agent
This agent specializes in iOS development patterns, data architecture, and integration workflows specific to the DashWallet project. It provides guidance based on real development sessions and common pitfalls to help future development sessions work more efficiently.
## šÆ MANDATORY UI Development Policy
### SwiftUI-First Architecture
**CRITICAL**: All new UI components MUST be implemented using SwiftUI. Do NOT create new UIKit ViewControllers, Storyboards, or XIB files.
#### ā
Required Pattern for New UI
```swift
// 1. SwiftUI View (UI only)
struct NewFeatureView: View {
@StateObject private var viewModel: NewFeatureViewModel
var body: some View {
// SwiftUI declarative UI
}
}
// 2. ObservableObject ViewModel (Business Logic)
@MainActor
class NewFeatureViewModel: ObservableObject {
@Published var state: ViewState = .loading
private let repository: FeatureRepository
func loadData() {
// Business logic implementation
}
}
// 3. Thin UIKit wrapper ONLY if needed for navigation integration
class NewFeatureHostingController: UIHostingController<NewFeatureView> {
init() {
let viewModel = NewFeatureViewModel()
let swiftUIView = NewFeatureView(viewModel: viewModel)
super.init(rootView: swiftUIView)
}
}
```
#### ā Prohibited Patterns
- Creating new `.storyboard` files
- Creating new `.xib` files
- Subclassing `UIViewController` for UI logic
- Using Interface Builder for new components
- Implementing UI logic directly in UIKit ViewControllers
### Architecture Enforcement
When implementing any new feature:
1. **Start with SwiftUI View** - Define the UI declaratively
2. **Create ViewModel** - Handle business logic and state management
3. **Use Repository Pattern** - Keep data access separate
4. **Add UIKit wrapper** - ONLY if existing navigation requires it
## Database Architecture Understanding
### Key Database Concepts
#### Local vs Remote Database Confusion
**Critical Understanding**: There are two distinct database systems:
1. **Local Database (`explore.db`)**
- SQLite database stored locally on device
- Contains cached merchant data and user preferences
- Managed through `DatabaseConnection.swift`
- Used for offline functionality and quick lookups
2. **Remote Database (`gs://dash-wallet-firebase.appspot.com/explore/explore-v3.db`)**
- Firebase-hosted SQLite database
- Source of truth for merchant information
- Downloaded and synced periodically
- Contains the complete merchant directory
**Common Mistake**: Confusing which database contains what data. Always verify if you're looking at local cached data or remote authoritative data.
#### Table Structure and Evolution
**Merchants Table Structure**:
```sql
-- Current merchant table fields (as of recent development)
CREATE TABLE merchant (
id INTEGER PRIMARY KEY,
name TEXT,
logoLocation TEXT,
website TEXT,
-- Payment configuration
denominationsType TEXT, -- EVOLVED FROM: redeemType
acceptsDash BOOLEAN,
acceptsGiftCards BOOLEAN,
-- Location data
latitude REAL,
longitude REAL,
-- CTX integration
ctxMerchantId TEXT,
enabled BOOLEAN DEFAULT 1
);
```
**Field Name Evolution**:
- `redeemType` ā `denominationsType` (represents payment type flexibility)
- Always check for both old and new field names when debugging legacy issues
**Gift Card Providers vs Merchants**:
- `gift_card_providers`: Separate table for gift card vendors (GameStop, Target, etc.)
- `merchant`: Main merchant directory table
- **Relationship**: Some merchants may be linked to gift card providers, but they are separate entities
### Data Flow Architecture
#### Model ā Database ā API Integration Pattern
```swift
// 1. Model Definition (ExplorePointOfUse.Merchant)
struct Merchant {
let id: Int
let name: String
let denominationsType: DenominationsType?
let enabled: Bool? // CTX-specific field
// Initializer updates required when adding properties
init(id: Int, name: String, denominationsType: DenominationsType? = nil, enabled: Bool? = nil) {
self.id = id
self.name = name
self.denominationsType = denominationsType
self.enabled = enabled
}
}
// 2. Database Access Pattern
class MerchantDAO {
func fetchMerchant(id: Int) -> Merchant? {
// Always implement fallback patterns for new fields
let ctxEnabled = row["enabled"] as? Bool
let localEnabled = row["acceptsDash"] as? Bool
let finalEnabled = ctxEnabled ?? localEnabled ?? true
return Merchant(id: id, enabled: finalEnabled)
}
}
// 3. API Integration (CTX Service)
class CTXSpendService {
func refreshTokenAndMerchantInfo() {
// Network call to update merchant data
// Triggers updatingMerchant observable
}
}
```
## CTX API Integration Patterns
### Service Integration Architecture
#### CTXSpendService Integration
```swift
class CTXSpendService: ObservableObject {
@Published var updatingMerchant = false
// Core integration pattern
func refreshTokenAndMerchantInfo() {
updatingMerchant = true
// 1. Network request
performNetworkRequest { [weak self] result in
DispatchQueue.main.async {
// 2. Update local data
self?.processResponse(result)
// 3. Notify UI
self?.updatingMerchant = false
}
}
}
// Fallback pattern for API integration
func getMerchantInfo(id: Int) -> MerchantInfo {
let ctxInfo = ctxMerchantData[id]
let localInfo = localMerchantData[id]
// Always implement fallbacks
return MerchantInfo(
enabled: ctxInfo?.enabled ?? localInfo?.enabled ?? true,
denominationsType: ctxInfo?.denominationsType ?? localInfo?.denominationsType
)
}
}
```
#### Reactive UI Integration
```swift
// PointOfUseDetailsView.swift pattern
struct PointOfUseDetailsView: View {
@StateObject private var ctxService = CTXSpendService()
var body: some View {
VStack {
// UI elements
}
.onAppear {
// Trigger data refresh
ctxService.refreshTokenAndMerchantInfo()
}
.onReceive(ctxService.$updatingMerchant) { isUpdating in
// React to loading states
if !isUpdating {
// Refresh UI with new data
updateMerchantDisplay()
}
}
}
}
```
### Data Synchronization Patterns
#### CTX to Local Database Sync
```swift
class MerchantSyncManager {
func syncMerchantData() {
// 1. Fetch from CTX API
ctxService.fetchAllMerchants { merchants in
// 2. Update local database
self.databaseManager.updateMerchants(merchants)
// 3. Notify UI components
NotificationCenter.default.post(name: .merchantsUpdated, object: nil)
}
}
// Fallback resolution pattern
func resolveMerchantData(ctxData: CTXMerchant?, localData: LocalMerchant?) -> MerchantData {
return MerchantData(
enabled: ctxData?.enabled ?? localData?.enabled ?? true,
denominationsType: ctxData?.denominationsType ?? localData?.denominationsType ?? .fixed
)
}
}
```
## iOS/Swift Development Patterns
### Model Property Additions
#### Required Initializer Updates
**Rule**: When adding properties to Swift structs/classes, always update initializers.
```swift
// Before: Simple merchant model
struct Merchant {
let id: Int
let name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
// After: Adding enabled property
struct Merchant {
let id: Int
let name: String
let enabled: Bool? // New property
// REQUIRED: Update initializer
init(id: Int, name: String, enabled: Bool? = nil) {
self.id = id
self.name = name
self.enabled = enabled
}
}
```
**Common Mistake**: Adding properties without updating initializers leads to compilation errors.
### UIKit + Combine Reactive Patterns
#### Observable Data Flow
```swift
class MerchantViewModel: ObservableObject {
@Published var merchant: Merchant?
@Published var isLoading = false
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
func loadMerchant(id: Int) {
isLoading = true
merchantService.fetchMerchant(id: id)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] merchant in
self?.merchant = merchant
}
)
.store(in: &cancellables)
}
}
```
#### UIKit Integration with Combine
```swift
class MerchantViewController: UIViewController {
private let viewModel = MerchantViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
// Bind loading state
viewModel.$isLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] isLoading in
self?.loadingIndicator.isHidden = !isLoading
}
.store(in: &cancellables)
// Bind merchant data
viewModel.$merchant
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [weak self] merchant in
self?.updateUI(with: merchant)
}
.store(in: &cancellables)
}
}
```
### Fallback Pattern Implementation
#### Safe Property Access
```swift
extension Merchant {
var safeEnabled: Bool {
// Implement fallback hierarchy
return ctxEnabled ?? localEnabled ?? true
}
var safeDenominationsType: DenominationsType {
return denominationsType ?? .fixed
}
}
// Usage in UI
class MerchantDisplayService {
func shouldShowFlexibleAmounts(for merchant: Merchant) -> Bool {
return merchant.safeEnabled && merchant.safeDenominationsType == .flexible
}
}
```
## Common Pitfalls and Solutions
### Database-Related Issues
#### Pitfall 1: Confusing Local vs Remote Data
**Problem**: Querying wrong database or assuming data exists locally.
**Solution**:
```swift
// Always specify data source clearly
func fetchMerchant(id: Int, source: DataSource) -> Merchant? {
switch source {
case .local:
return localDatabase.fetchMerchant(id: id)
case .remote:
return remoteDatabase.fetchMerchant(id: id)
case .hybrid:
return remoteMerchant ?? localMerchant ?? defaultMerchant
}
}
```
#### Pitfall 2: Field Name Evolution Confusion
**Problem**: Using deprecated field names (`redeemType` instead of `denominationsType`).
**Solution**:
```swift
// Create migration-aware accessors
extension DatabaseRow {
var denominationsType: DenominationsType? {
// Try new field name first, fall back to old
if let newValue = self["denominationsType"] as? String {
return DenominationsType(rawValue: newValue)
}
if let oldValue = self["redeemType"] as? String {
return DenominationsType(rawValue: oldValue)
}
return nil
}
}
```
### CTX Integration Issues
#### Pitfall 3: Missing Fallback Values
**Problem**: Assuming CTX API data is always available.
**Solution**:
```swift
// Always implement fallback chains
func getMerchantStatus(merchant: Merchant) -> MerchantStatus {
let ctxValue = ctxService.getMerchantInfo(merchant.id)?.enabled
let localValue = merchant.acceptsDash
let defaultValue = true
let isEnabled = ctxValue ?? localValue ?? defaultValue
return MerchantStatus(
enabled: isEnabled,
source: ctxValue != nil ? .ctx : .local
)
}
```
### UI Update Issues
#### Pitfall 4: GameStop Flexible Amounts Display
**Problem**: Flexible amounts not displaying correctly due to data source confusion.
**Solution**:
```swift
// Proper data flow validation
class GameStopDisplayManager {
func shouldShowFlexibleAmounts(merchantId: Int) -> Bool {
// 1. Check CTX data first
if let ctxMerchant = ctxService.getMerchant(id: merchantId),
let ctxEnabled = ctxMerchant.enabled {
return ctxEnabled && ctxMerchant.denominationsType == .flexible
}
// 2. Fall back to local data
if let localMerchant = localDatabase.getMerchant(id: merchantId) {
return localMerchant.acceptsGiftCards && localMerchant.denominationsType == .flexible
}
// 3. Default behavior
return false
}
}
```
## Effective Debugging Strategies
### Targeted Debug Logging
#### Debug Markers System
```swift
// Use emoji markers for easy log filtering
class DebugLogger {
static func merchantInfo(_ message: String) {
print("šÆ MERCHANT: \(message)")
}
static func ctxAPI(_ message: String) {
print("š CTX_API: \(message)")
}
static func database(_ message: String) {
print("š¾ DATABASE: \(message)")
}
static func ui(_ message: String) {
print("šØ UI_UPDATE: \(message)")
}
}
// Usage throughout codebase
class MerchantService {
func updateMerchant(id: Int) {
DebugLogger.merchantInfo("Starting update for merchant \(id)")
ctxService.refreshMerchant(id: id) { result in
DebugLogger.ctxAPI("Received CTX response: \(result)")
self.updateLocalDatabase(result) { success in
DebugLogger.database("Local update result: \(success)")
DispatchQueue.main.async {
DebugLogger.ui("Triggering UI refresh")
self.notifyUIUpdate()
}
}
}
}
}
```
#### Conditional Debug Output
```swift
#if DEBUG
extension MerchantViewModel {
func debugDataFlow() {
print("šÆ CTX Data: \(ctxMerchant?.enabled ?? "nil")")
print("šÆ Local Data: \(localMerchant?.acceptsDash ?? "nil")")
print("šÆ Final Value: \(merchant?.safeEnabled ?? "nil")")
}
}
#endif
```
### Problem Isolation Techniques
#### Data Source Verification
```swift
class DataSourceDebugger {
static func verifyMerchantData(id: Int) {
let ctxData = ctxService.getMerchant(id: id)
let localData = localDatabase.getMerchant(id: id)
print("šÆ Merchant \(id) Data Sources:")
print(" CTX: enabled=\(ctxData?.enabled ?? "nil"), type=\(ctxData?.denominationsType ?? "nil")")
print(" Local: enabled=\(localData?.acceptsDash ?? "nil"), type=\(localData?.denominationsType ?? "nil")")
// Verify data consistency
if let ctxEnabled = ctxData?.enabled,
let localEnabled = localData?.acceptsDash,
ctxEnabled != localEnabled {
print("ā ļø WARNING: CTX and local enabled status mismatch!")
}
}
}
```
## Data Model Update Procedures
### Safe Model Evolution
#### 1. Property Addition Checklist
- [ ] Add property to model struct/class
- [ ] Update all initializers with default values
- [ ] Update database schema if needed
- [ ] Add migration script for existing data
- [ ] Update API serialization/deserialization
- [ ] Add unit tests for new property
- [ ] Update UI components that use the model
#### 2. Database Migration Pattern
```swift
// Migration for adding new fields
class DatabaseMigration_AddEnabledField: DatabaseMigration {
override func migrate() {
database.execute("ALTER TABLE merchant ADD COLUMN enabled BOOLEAN DEFAULT 1")
// Backfill existing data
database.execute("UPDATE merchant SET enabled = acceptsDash WHERE enabled IS NULL")
markMigrationComplete()
}
}
```
#### 3. API Integration Updates
```swift
// Backwards-compatible API handling
extension Merchant {
init(from apiResponse: [String: Any]) {
self.id = apiResponse["id"] as? Int ?? 0
self.name = apiResponse["name"] as? String ?? ""
// Handle field evolution gracefully
if let enabled = apiResponse["enabled"] as? Bool {
self.enabled = enabled
} else if let acceptsDash = apiResponse["acceptsDash"] as? Bool {
self.enabled = acceptsDash
} else {
self.enabled = true
}
}
}
```
## Project-Specific Integration Points
### CTXSpendService Architecture
- **Location**: `DashWallet/Sources/Models/Services/CTXSpendService.swift`
- **Purpose**: Handles communication with CTX API for merchant data
- **Key Methods**:
- `refreshTokenAndMerchantInfo()`: Updates merchant data from CTX
- `getMerchantInfo(id:)`: Retrieves specific merchant information
- **Integration Pattern**: Observable service with `@Published` properties
### PointOfUseDetailsView Architecture
- **Location**: `DashWallet/Sources/UI/Explore/PointOfUseDetailsView.swift`
- **Purpose**: Displays detailed merchant information with real-time updates
- **Data Flow**: CTXSpendService ā updatingMerchant ā UI refresh
- **Key Pattern**: Combine publishers trigger UI updates
### ExplorePointOfUse.Merchant Model
- **Location**: Model definitions for merchant data structures
- **Evolution**: Handles both legacy and current field names
- **Integration**: Works with both CTX API and local database
## Best Practices Summary
### Development Workflow
1. **Always verify data sources** before implementing features
2. **Implement fallback patterns** for all external data dependencies
3. **Use targeted debugging** with emoji markers for easy log filtering
4. **Test with both CTX and local data** scenarios
5. **Update models safely** following the property addition checklist
### Code Quality
1. **Prefer explicit over implicit** when dealing with optionals
2. **Document field evolution** in comments for future developers
3. **Use descriptive variable names** that indicate data source (`ctxEnabled`, `localEnabled`)
4. **Implement proper error handling** for all network operations
5. **Follow reactive programming patterns** with Combine for UI updates
## Location-Based Query Patterns (Critical Fix from Recent Session)
### Radius Filtering Architecture
#### The Two-Stage Filtering Pattern
Location-based queries in the app MUST implement a two-stage filtering approach:
1. **Rectangular Bounds Filtering** (SQL Query Optimization)
- Uses min/max latitude/longitude for initial database filtering
- Reduces the dataset size for performance
2. **Circular Distance Filtering** (Accuracy)
- Uses `CLLocation.distance(from:)` for great-circle calculations
- Ensures results are within the true circular radius
#### Critical Bug Pattern (Fixed)
**Issue**: "Show all locations" button showed locations outside the 20-mile radius filter
**Root Cause Analysis**:
```swift
// ā INCORRECT: Only rectangular filtering
func allLocations(by query: String, in bounds: ExploreMapBounds, userPoint: CLLocation?) -> [Merchant] {
// This only filtered by rectangular bounds (lines 512-519 in MerchantDAO.swift)
let sql = """
SELECT * FROM merchant
WHERE latitude BETWEEN \(bounds.minLat) AND \(bounds.maxLat)
AND longitude BETWEEN \(bounds.minLng) AND \(bounds.maxLng)
"""
return executeQuery(sql)
}
```
**Correct Implementation**:
```swift
// ā
CORRECT: Both rectangular AND circular filtering
func allLocations(by query: String, in bounds: ExploreMapBounds, userPoint: CLLocation?) -> [Merchant] {
// Stage 1: Rectangular bounds (SQL optimization)
let sql = """
SELECT * FROM merchant
WHERE latitude BETWEEN \(bounds.minLat) AND \(bounds.maxLat)
AND longitude BETWEEN \(bounds.minLng) AND \(bounds.maxLng)
"""
var merchants = executeQuery(sql)
// Stage 2: Circular distance filtering (accuracy)
if let userPoint = userPoint {
let radius = calculateRadiusFromBounds(bounds)
merchants = merchants.filter { merchant in
guard let lat = merchant.latitude, let lng = merchant.longitude else {
print("šÆ Filtering out merchant with nil coordinates: \(merchant.name)")
return false
}
let distance = CLLocation(latitude: lat, longitude: lng).distance(from: userPoint)
let isWithinRadius = distance <= radius
if !isWithinRadius {
print("šÆ Filtering out \(merchant.name): distance \(distance)m > radius \(radius)m")
}
return isWithinRadius
}
}
return merchants
}
```
#### Distance Calculation Best Practices
1. **Always Use CLLocation for Distance**:
```swift
// ā
CORRECT: Great-circle distance calculation
let distance = CLLocation(latitude: lat1, longitude: lng1)
.distance(from: CLLocation(latitude: lat2, longitude: lng2))
// ā AVOID: Mathematical approximations
let distance = sqrt(pow(lat2 - lat1, 2) + pow(lng2 - lng1, 2)) * someConstant
```
2. **Radius Conversion from Bounds**:
```swift
func calculateRadiusFromBounds(_ bounds: ExploreMapBounds, userPoint: CLLocation) -> Double {
// Calculate the actual circular radius from rectangular bounds
let neLoc = CLLocation(latitude: bounds.maxLat, longitude: bounds.maxLng)
let swLoc = CLLocation(latitude: bounds.minLat, longitude: bounds.minLng)
// Use the smaller of the two distances to ensure circular coverage
let neDistance = userPoint.distance(from: neLoc)
let swDistance = userPoint.distance(from: swLoc)
return min(neDistance, swDistance)
}
```
3. **Debug Logging Pattern**:
```swift
// Use emoji markers for easy filtering
print("šÆ Distance filtering: \(merchantName)")
print("šÆ User location: \(userPoint.coordinate)")
print("šÆ Merchant location: (\(lat), \(lng))")
print("šÆ Distance: \(distance)m, Radius: \(radius)m")
print("šÆ Include: \(distance <= radius)")
```
#### Architecture Components
**Data Flow Hierarchy**:
1. **AllMerchantLocationsDataProvider** ā Creates circular bounds from radius filters
2. **MerchantDAO.items** ā Applies proper circular filtering for main lists
3. **MerchantDAO.allLocations** ā Now also applies proper circular filtering
**Key Classes and Methods**:
- `MerchantDAO.swift` (lines 540-566): Fixed `allLocations` implementation
- `AllMerchantLocationsDataProvider.swift`: Creates bounds from radius
- `ExploreMapBounds`: Handles coordinate region calculations
#### Testing Distance Filtering
```swift
func testRadiusFiltering() {
// Test case from actual bug fix
let userLocation = CLLocation(latitude: 37.7749, longitude: -122.4194) // San Francisco
let radius = 32186.88 // 20 miles in meters
let testMerchants = [
("Within Radius", 37.7849, -122.4094, true), // ~1.4km away
("On Edge", 37.9649, -122.4194, true), // ~20 miles away
("Outside Radius", 38.0000, -122.4194, false) // ~25 miles away
]
for (name, lat, lng, shouldInclude) in testMerchants {
let distance = CLLocation(latitude: lat, longitude: lng).distance(from: userLocation)
let isIncluded = distance <= radius
XCTAssertEqual(isIncluded, shouldInclude,
"\(name) at distance \(distance)m should\(shouldInclude ? "" : " not") be included")
}
}
```
## Feature Flag Architecture (Updated from Current Session)
### Gift Card Provider System Architecture
The app implements a sophisticated gift card provider system with conditional compilation for feature hiding:
#### Provider Enum Structure
```swift
enum GiftCardProvider: CaseIterable {
case ctx
#if PIGGYCARDS_ENABLED
case piggyCards
#endif
var displayName: String {
switch self {
case .ctx: return "CTX"
#if PIGGYCARDS_ENABLED
case .piggyCards: return "PiggyCards"
#endif
}
}
}
```
#### Repository Factory Pattern
```swift
class DashSpendRepositoryFactory {
func create(provider: GiftCardProvider) -> any DashSpendRepository {
switch provider {
case .ctx:
return createCTXSpendRepository()
#if PIGGYCARDS_ENABLED
case .piggyCards:
return createPiggyCardsRepository()
#endif
}
}
#if PIGGYCARDS_ENABLED
private func createPiggyCardsRepository() -> PiggyCardsRepository {
return PiggyCardsRepository.shared
}
#endif
}
```
#### ViewModel Repository Management
Uses closure-based dictionary initialization for conditional compilation:
```swift
private let repositories: [GiftCardProvider: any DashSpendRepository] = {
var dict: [GiftCardProvider: any DashSpendRepository] = [
.ctx: CTXSpendRepository.shared
]
#if PIGGYCARDS_ENABLED
dict[.piggyCards] = PiggyCardsRepository.shared
#endif
return dict
}()
```
### Feature Flag Implementation Patterns
#### Database Conditional Filtering
```swift
// MerchantDAO filtering with conditional compilation
let hasCTX = methods.contains(.ctx)
#if PIGGYCARDS_ENABLED
let hasPiggy = methods.contains(.piggyCards)
#else
let hasPiggy = false
#endif
if hasCTX || hasPiggy {
// Gift card filtering logic
var providerList: [String] = []
if hasCTX { providerList.append("'CTX'") }
#if PIGGYCARDS_ENABLED
if hasPiggy { providerList.append("'PiggyCards'") }
#endif
}
```
#### UI Filter Implementation
Uses helper computed properties to avoid SwiftUI ViewBuilder conflicts:
```swift
struct FilterView: View {
// Helper computed properties for conditional UI logic
private var shouldShowPiggyCards: Bool {
#if PIGGYCARDS_ENABLED
return true
#else
return false
#endif
}
var body: some View {
VStack {
// CTX always available
FilterOption("CTX")
// PiggyCards conditionally available
if shouldShowPiggyCards {
FilterOption("PiggyCards")
}
}
}
}
```
### Boolean Expression Conditional Compilation
Avoid inline conditional compilation in complex boolean expressions:
#### ā Problematic Pattern
```swift
// Causes syntax errors when PIGGYCARDS_ENABLED is undefined
private var hasChanges: Bool {
return ctxChanged ||
#if PIGGYCARDS_ENABLED
piggyChanged ||
#endif
otherChanged
}
```
#### ā
Correct Pattern
```swift
private var hasChanges: Bool {
let baseChanges = ctxChanged || otherChanged
#if PIGGYCARDS_ENABLED
return baseChanges || piggyChanged
#else
return baseChanges
#endif
}
```
### Architecture Benefits
1. **Clean Separation**: Features can be completely hidden without runtime checks
2. **Easy Rollback**: Simply remove/add compilation flags to enable/disable features
3. **Build-Time Optimization**: Unused code is completely eliminated from binary
4. **Testing**: Can test both enabled and disabled states easily
5. **Deployment Flexibility**: Different builds can have different feature sets
### Common Pitfalls and Solutions
1. **SwiftUI ViewBuilder Issues**: Use computed properties instead of inline conditionals
2. **Dictionary Syntax Errors**: Use closure-based initialization
3. **Boolean Expression Errors**: Split complex expressions with conditional variables
4. **Repository Missing**: Ensure conditional compilation covers both declaration and usage
5. **Filter Logic Inconsistency**: Always provide fallback values for disabled features
## SwiftUI Development Patterns (MANDATORY for New UI)
### MVVM Architecture with SwiftUI
#### View-ViewModel Binding Patterns
```swift
// Views should be stateless and delegate all business logic to ViewModels
struct MerchantListView: View {
@StateObject private var viewModel = MerchantListViewModel()
@State private var searchText = ""
var body: some View {
NavigationStack {
List(viewModel.filteredMerchants) { merchant in
MerchantRowView(merchant: merchant)
.onTapGesture {
viewModel.selectMerchant(merchant)
}
}
.searchable(text: $searchText)
.onChange(of: searchText) { _, newValue in
viewModel.filterMerchants(by: newValue)
}
.refreshable {
await viewModel.refreshData()
}
.task {
await viewModel.loadInitialData()
}
.alert("Error", isPresented: $viewModel.hasError) {
Button("Retry") { viewModel.retryLastAction() }
Button("Cancel", role: .cancel) { }
} message: {
Text(viewModel.errorMessage)
}
}
}
}
```
#### ViewModel State Management
```swift
@MainActor
class MerchantListViewModel: ObservableObject {
@Published var merchants: [Merchant] = []
@Published var filteredMerchants: [Merchant] = []
@Published var isLoading = false
@Published var hasError = false
@Published var errorMessage = ""
private let merchantRepository: MerchantRepository
private let locationService: LocationService
init(merchantRepository: MerchantRepository = .shared) {
self.merchantRepository = merchantRepository
self.locationService = LocationService.shared
}
func loadInitialData() async {
isLoading = true
do {
merchants = try await merchantRepository.fetchMerchants()
filteredMerchants = merchants
} catch {
handleError(error)
}
isLoading = false
}
func filterMerchants(by searchText: String) {
filteredMerchants = searchText.isEmpty ?
merchants : merchants.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
private func handleError(_ error: Error) {
hasError = true
errorMessage = error.localizedDescription
}
}
```
### Navigation Patterns
#### SwiftUI Navigation (Preferred)
```swift
// Use NavigationStack for programmatic navigation
struct MainNavigationView: View {
var body: some View {
NavigationStack {
MerchantListView()
.navigationDestination(for: Merchant.self) { merchant in
MerchantDetailsView(merchant: merchant)
}
.navigationDestination(for: PaymentRequest.self) { request in
PaymentView(request: request)
}
}
}
}
```
#### UIKit Integration Bridge
```swift
// Extension for pushing SwiftUI views from UIKit contexts
extension UINavigationController {
func pushSwiftUIView<Content: View>(
_ view: Content,
title: String? = nil,
hidesBottomBar: Bool = false
) {
let hostingController = UIHostingController(rootView: view)
hostingController.title = title
hostingController.hidesBottomBarWhenPushed = hidesBottomBar
pushViewController(hostingController, animated: true)
}
func presentSwiftUIView<Content: View>(
_ view: Content,
style: UIModalPresentationStyle = .pageSheet
) {
let hostingController = UIHostingController(rootView: view)
hostingController.modalPresentationStyle = style
present(hostingController, animated: true)
}
}
```
### Data Flow Patterns
#### Repository Integration
```swift
// ViewModels should use repositories, not direct API calls
class PaymentViewModel: ObservableObject {
@Published var paymentState: PaymentState = .idle
private let paymentRepository: PaymentRepository
private let walletService: WalletService
init(
paymentRepository: PaymentRepository = .shared,
walletService: WalletService = .shared
) {
self.paymentRepository = paymentRepository
self.walletService = walletService
}
func processPayment(_ request: PaymentRequest) async {
paymentState = .processing
do {
let transaction = try await paymentRepository.createTransaction(request)
let result = try await walletService.broadcastTransaction(transaction)
paymentState = .completed(result)
} catch {
paymentState = .failed(error)
}
}
}
```
#### Combine Integration for Reactive Updates
```swift
class MerchantViewModel: ObservableObject {
@Published var merchant: Merchant?
@Published var isOnline = false
private var cancellables = Set<AnyCancellable>()
private let networkMonitor: NetworkMonitor
init(merchantId: String) {
self.networkMonitor = NetworkMonitor.shared
// React to network changes
networkMonitor.$isConnected
.assign(to: \.isOnline, on: self)
.store(in: &cancellables)
// Auto-refresh when coming back online
networkMonitor.$isConnected
.filter { $0 == true }
.sink { [weak self] _ in
Task { @MainActor in
await self?.refreshMerchantData()
}
}
.store(in: &cancellables)
}
}
```
### UI Component Patterns
#### Reusable SwiftUI Components
```swift
// Create reusable components instead of duplicating UI code
struct MerchantRowView: View {
let merchant: Merchant
var body: some View {
HStack {
AsyncImage(url: merchant.logoURL) { image in
image.resizable()
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
}
.frame(width: 50, height: 50)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 4) {
Text(merchant.name)
.font(.headline)
Text(merchant.category)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if merchant.hasDiscount {
DiscountBadge(percentage: merchant.discountPercentage)
}
}
.padding(.vertical, 4)
}
}
```
### State Management Best Practices
#### Loading States
```swift
enum ViewState<T> {
case idle
case loading
case loaded(T)
case error(Error)
}
struct ContentView<Data>: View where Data: Hashable {
let state: ViewState<Data>
let content: (Data) -> AnyView
let retry: () -> Void
var body: some View {
switch state {
case .idle:
Color.clear
.onAppear { retry() }
case .loading:
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .loaded(let data):
content(data)
case .error(let error):
ErrorView(error: error, retry: retry)
}
}
}
```
### Performance Patterns
#### Efficient List Updates
```swift
// Use proper identifiers for list performance
struct MerchantListView: View {
@StateObject private var viewModel: MerchantListViewModel
var body: some View {
List {
ForEach(viewModel.merchants) { merchant in
MerchantRowView(merchant: merchant)
.id(merchant.id) // Explicit ID for performance
}
}
.animation(.default, value: viewModel.merchants)
}
}
```
## Legacy Integration Patterns (From DEVELOPMENT-PATTERNS.md)
### Protocol-Oriented Programming (Legacy Codebase)
**Context**: Existing codebase uses protocols extensively for dependency injection
```objc
// Objective-C protocol example (Legacy)
@protocol DWHomeProtocol <NSObject>
- (void)updateBalance:(uint64_t)balance;
- (void)showTransactionDetail:(DSTransaction *)transaction;
@end
```
```swift
// Swift protocol example (Modern)
protocol CurrencyExchangerProtocol {
func exchangeRate(for currency: String) async throws -> Double
func convertToFiat(amount: UInt64, currency: String) -> String
}
```
### Base View Controller Pattern (Legacy)
**For existing UIKit code only - DO NOT create new UIKit ViewControllers**
```swift
class BaseViewController: UIViewController, ErrorPresentable, NetworkReachabilityObservable {
override func viewDidLoad() {
super.viewDidLoad()
setupBaseConfiguration()
observeNetworkReachability()
}
func setupBaseConfiguration() {
// Common setup for all view controllers
view.backgroundColor = UIColor.dw_background()
setupNavigationBar()
}
func showError(_ error: Error) {
// Standardized error presentation
let alert = errorAlert(for: error)
present(alert, animated: true)
}
}
```
### Custom View Pattern (Legacy)
**For existing components only - new components should use SwiftUI**
```swift
class CustomComponentView: UIView {
// MARK: - UI Components
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.dw_mediumFont(ofSize: 16)
label.textColor = UIColor.dw_primaryText()
return label
}()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
// MARK: - Setup
private func setupView() {
addSubview(titleLabel)
setupConstraints()
applyStyle()
}
private func setupConstraints() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
}
```
### Testing Patterns (Enhanced)
#### Mock Objects Pattern
```swift
class MockRatesProvider: RatesProvider {
var shouldReturnError = false
var mockRates: [String: Double] = [:]
func fetchRates() async throws -> [String: Double] {
if shouldReturnError {
throw RatesError.networkError
}
return mockRates
}
}
class CurrencyExchangerTests: XCTestCase {
var sut: CurrencyExchanger!
var mockProvider: MockRatesProvider!
override func setUp() {
super.setUp()
mockProvider = MockRatesProvider()
sut = CurrencyExchanger(provider: mockProvider)
}
func testSuccessfulRateFetch() async throws {
// Given
mockProvider.mockRates = ["USD": 25.50]
// When
let rate = try await sut.exchangeRate(for: "USD")
// Then
XCTAssertEqual(rate, 25.50)
}
}
```
### External Service Integration Patterns (Legacy)
```swift
protocol ExternalServiceProtocol {
associatedtype AuthType
associatedtype ResponseType
func authenticate(_ auth: AuthType) async throws
func performRequest<T: Codable>(_ endpoint: APIEndpoint) async throws -> T
}
class ExternalService: ExternalServiceProtocol {
private let httpClient: HTTPClient<APIResponse>
private var authToken: String?
func authenticate(_ credentials: Credentials) async throws {
let response = try await httpClient.post("/auth", body: credentials)
self.authToken = response.token
}
func performRequest<T: Codable>(_ endpoint: APIEndpoint) async throws -> T {
guard let token = authToken else {
throw ServiceError.notAuthenticated
}
return try await httpClient.request(endpoint, headers: ["Authorization": "Bearer \(token)"])
}
}
```
## Code Organization Patterns (Enhanced)
### Feature-Based Organization
```
UI/
āāā Home/
ā āāā HomeView.swift # SwiftUI View (NEW)
ā āāā HomeViewModel.swift # ViewModel (NEW)
ā āāā HomeViewController.swift # Legacy UIKit (EXISTING)
ā āāā Models/
ā āāā Views/
āāā Payments/
ā āāā PaymentView.swift # SwiftUI (NEW)
ā āāā PaymentViewModel.swift # ViewModel (NEW)
ā āāā Pay/
ā āāā Receive/
ā āāā ScanQR/
āāā DashPay/
āāā DashPayView.swift # SwiftUI (NEW)
āāā DashPayViewModel.swift # ViewModel (NEW)
āāā Contacts/
āāā Profile/
āāā Voting/
```
### Conditional Compilation (Updated)
**Enhanced with real-world patterns from PiggyCards feature hiding**
```swift
// Complex conditional compilation patterns
#if DASHPAY
// DashPay-specific functionality
func setupDashPayFeatures() {
// Implementation
}
#endif
// Feature flag with enum cases
enum Features: CaseIterable {
case dashPay
#if PIGGYCARDS_ENABLED
case piggyCards
#endif
}
// Safe repository initialization
private let repositories: [Provider: Repository] = {
var dict: [Provider: Repository] = [.ctx: CTXRepository.shared]
#if PIGGYCARDS_ENABLED
dict[.piggyCards] = PiggyCardsRepository.shared
#endif
return dict
}()
```
This documentation should significantly reduce common mistakes and improve development efficiency for future iOS sessions on the DashWallet project, combining modern SwiftUI-first development with comprehensive legacy integration patterns.Quick Install
$
npx ai-builder add agent dashpay/ios-development-patternsDetails
- Type
- agent
- Author
- dashpay
- Slug
- dashpay/ios-development-patterns
- Created
- 5d ago