# Character Avatar Image Loading Fix

**Date:** 2026-01-28
**Status:** Planning
**Priority:** High

## Problem Summary

Character avatar images are not displaying reliably across the iOS app. Images sometimes appear on the swiping page and character list, but often fail to load or appear blank.

## Root Cause Analysis

### Issue 1: Race Condition in SVGImageView

The custom `SVGImageView` component has a fundamental flaw:

```swift
func updateUIView(_ uiView: UIImageView, context: Context) {
    uiView.sd_setImage(with: url, completed: { ... })
}
```

**Problem:** `updateUIView` is called on EVERY SwiftUI re-render, triggering a new image load each time. This causes:
- Image loads → view re-renders → new load starts → previous load completes but view is stale
- Race conditions between multiple concurrent loads
- Images appearing briefly then disappearing

### Issue 2: No URL Change Detection

The component doesn't check if the URL actually changed before starting a new load. Every re-render = new network request.

### Issue 3: Silent Failures

No error handling in the completion closure:
```swift
if let validImage = image {
    // Only success handled
    // Errors silently ignored - view stays blank
}
```

### Issue 4: Resize Function Can Fail

`UIImage.resized(to:)` returns `UIImage?` - if it fails, the image becomes nil.

### Issue 5: Duplicate SVG Coder Registration

SVG coder is registered in multiple places:
- `lichunWebsocketApp.swift` (line 75-76)
- `MainCharacterView.swift` (line 23-24)

This is redundant and could cause timing issues.

## Solution: CharacterAvatar Component

Replace the problematic `SVGImageView` with a well-designed `CharacterAvatar` component that wraps `WebImage` from SDWebImageSwiftUI.

### Why WebImage?

1. **Battle-tested** - Maintained by SDWebImage team
2. **SwiftUI-native** - Proper lifecycle management
3. **Built-in features** - Caching, retries, cancellation, memory management
4. **Already a dependency** - No new dependencies needed

## Implementation Plan

### Phase 1: Create New Component

#### 1.1 Create CharacterAvatar.swift

**Location:** `ios/lichunWebsocket/Shared/Components/Images/CharacterAvatar.swift`

```swift
//
//  CharacterAvatar.swift
//  lichunWebsocket
//
//  Unified character avatar component with proper image loading
//

import SwiftUI
import SDWebImageSwiftUI

/// A reusable avatar component for displaying character images
/// Uses WebImage for proper SwiftUI lifecycle handling and caching
struct CharacterAvatar: View {
    // MARK: - Properties

    /// The URL string for the avatar image
    let imageURL: String

    /// The character's name (used for placeholder initial)
    let name: String

    /// Size of the avatar (width and height)
    var size: CGFloat = AppSpacing.avatarSizeMedium

    /// Whether to show the gradient border
    var showBorder: Bool = true

    /// Custom border gradient colors
    var borderGradient: [Color] = [AppColors.primary, AppColors.accent]

    /// Border line width (auto-calculated if nil)
    var borderWidth: CGFloat? = nil

    /// Whether to show the shadow glow effect
    var showGlow: Bool = true

    /// Whether to show loading indicator
    var showLoadingIndicator: Bool = true

    // MARK: - Computed Properties

    private var computedBorderWidth: CGFloat {
        borderWidth ?? (size > 80 ? 3 : 2)
    }

    private var url: URL? {
        guard !imageURL.isEmpty else { return nil }
        return URL(string: imageURL)
    }

    // MARK: - Body

    var body: some View {
        Group {
            if let url = url {
                WebImage(url: url)
                    .resizable()
                    .placeholder { placeholderView }
                    .indicator(showLoadingIndicator ? .activity(style: .medium) : .none)
                    .transition(.fade(duration: 0.25))
                    .scaledToFill()
            } else {
                placeholderView
            }
        }
        .frame(width: size, height: size)
        .clipShape(Circle())
        .overlay(borderOverlay)
        .modifier(GlowModifier(showGlow: showGlow, color: borderGradient.first ?? AppColors.primary, size: size))
    }

    // MARK: - Subviews

    @ViewBuilder
    private var placeholderView: some View {
        Circle()
            .fill(
                LinearGradient(
                    colors: [
                        AppColors.cardBackground.lighter(by: 0.15),
                        AppColors.cardBackground
                    ],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
            )
            .overlay(
                Text(String(name.prefix(1)).uppercased())
                    .font(.system(size: size * 0.4, weight: .bold, design: .rounded))
                    .foregroundStyle(
                        LinearGradient(
                            colors: borderGradient,
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
            )
    }

    @ViewBuilder
    private var borderOverlay: some View {
        if showBorder {
            Circle()
                .strokeBorder(
                    LinearGradient(
                        colors: borderGradient,
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    ),
                    lineWidth: computedBorderWidth
                )
        }
    }
}

// MARK: - Glow Modifier

private struct GlowModifier: ViewModifier {
    let showGlow: Bool
    let color: Color
    let size: CGFloat

    func body(content: Content) -> some View {
        if showGlow {
            content
                .shadow(
                    color: color.opacity(0.2),
                    radius: size > 80 ? 12 : 8,
                    x: 0,
                    y: 4
                )
        } else {
            content
        }
    }
}

// MARK: - Convenience Initializers

extension CharacterAvatar {
    /// Initialize with a Person object
    init(person: Person, size: CGFloat = AppSpacing.avatarSizeMedium, showBorder: Bool = true) {
        self.imageURL = person.image
        self.name = person.firstname
        self.size = size
        self.showBorder = showBorder
    }
}

// MARK: - Preview

#if DEBUG
struct CharacterAvatar_Previews: PreviewProvider {
    static var previews: some View {
        VStack(spacing: 20) {
            // With valid URL
            CharacterAvatar(
                imageURL: "https://api.dicebear.com/7.x/avataaars/svg?seed=test",
                name: "John"
            )

            // Empty URL (shows placeholder)
            CharacterAvatar(
                imageURL: "",
                name: "Jane",
                size: 80
            )

            // Large size
            CharacterAvatar(
                imageURL: "https://api.dicebear.com/7.x/avataaars/svg?seed=large",
                name: "Alex",
                size: 120,
                borderGradient: [.pink, .purple]
            )

            // No border
            CharacterAvatar(
                imageURL: "",
                name: "Sam",
                size: 60,
                showBorder: false,
                showGlow: false
            )
        }
        .padding()
        .background(AppColors.background)
    }
}
#endif
```

#### 1.2 Create Directory Structure

```
ios/lichunWebsocket/Shared/Components/Images/
└── CharacterAvatar.swift
```

### Phase 2: Migration

Replace all `SVGImageView` usages with `CharacterAvatar`. Each file needs to be updated:

#### 2.1 High Priority (Main Character Display)

| File | Line | Current | Replace With |
|------|------|---------|--------------|
| `Features/Home/Components/AvatarCard.swift` | 23 | `SVGImageView(url:desiredSize:)` | `CharacterAvatar(person:size:)` |
| `Features/Home/Components/MainCharacterView.swift` | 83 | `SVGImageView(url:desiredSize:)` | `CharacterAvatar(imageURL:name:size:)` |
| `Features/Character/Components/AvatarView.swift` | 24 | `SVGImageView(url:desiredSize:)` | `CharacterAvatar(person:size:showBorder:)` |

#### 2.2 Dating Features

| File | Line | Current | Replace With |
|------|------|---------|--------------|
| `Features/Dating/Components/EnhancedProfileCard.swift` | 90 | `SVGImageView` | `CharacterAvatar` |
| `Features/Dating/Components/MatchCard.swift` | 30 | `SVGImageView` | `CharacterAvatar` |
| `Features/Dating/Components/RomanticHeroCard.swift` | 54 | `SVGImageView` | `CharacterAvatar` |
| `Features/Dating/Views/RelationshipsView.swift` | 158 | `SVGImageView` | `CharacterAvatar` |
| `Features/Dating/Views/RelationshipDetailView.swift` | 164 | `SVGImageView` | `CharacterAvatar` |
| `Features/Dating/Views/DateMiniGameView.swift` | 258 | `SVGImageView` | `CharacterAvatar` |

#### 2.3 Messaging Features

| File | Line | Current | Replace With |
|------|------|---------|--------------|
| `Features/Messaging/Components/ConversationListItem.swift` | 20 | `SVGImageView` | `CharacterAvatar` |
| `Features/Messaging/Components/MessageRowCard.swift` | 125 | `SVGImageView` | `CharacterAvatar` |
| `Features/Messaging/Components/ChatHeaderCard.swift` | 113, 233 | `SVGImageView` | `CharacterAvatar` |

#### 2.4 Activities Features

| File | Line | Current | Replace With |
|------|------|---------|--------------|
| `Features/Activities/Views/ActivityView.swift` | 522 | `SVGImageView` | `CharacterAvatar` |

### Phase 3: Cleanup

#### 3.1 Files to Delete

- `Features/Character/Components/SVGImageView.swift` - No longer needed
- `Features/Character/Components/AvatarView.swift` - Replaced by CharacterAvatar

#### 3.2 Remove Duplicate SVG Coder Registration

**Keep only in `lichunWebsocketApp.swift`:**
```swift
// Configure SVG image support (line 74-76)
let SVGCoder = SDImageSVGCoder.shared
SDImageCodersManager.shared.addCoder(SVGCoder)
```

**Remove from `MainCharacterView.swift`:**
```swift
// DELETE lines 22-25:
init() {
    let SVGCoder = SDImageSVGCoder.shared
    SDImageCodersManager.shared.addCoder(SVGCoder)
}
```

#### 3.3 Update Imports

Remove unnecessary imports from files that no longer use SVGImageView:
- Remove `import SDWebImage` where only `SDWebImageSwiftUI` is needed
- Keep `import SDWebImageSwiftUI` for `WebImage` usage

### Phase 4: Testing Checklist

#### 4.1 Visual Testing

- [ ] Home screen avatar displays correctly
- [ ] Character list shows all avatars
- [ ] Dating swipe cards show avatars
- [ ] Chat headers show avatars
- [ ] Conversation list shows avatars
- [ ] Relationship detail view shows avatar
- [ ] Activity views show character avatars

#### 4.2 Edge Cases

- [ ] Empty image URL shows placeholder with initial
- [ ] Network error shows placeholder (not blank)
- [ ] Slow network shows loading indicator
- [ ] Scrolling through lists doesn't cause flickering
- [ ] Returning to a view doesn't reload images (cached)
- [ ] Memory pressure doesn't cause blank images

#### 4.3 Performance Testing

- [ ] Scroll through 20+ characters smoothly
- [ ] No memory leaks when navigating between views
- [ ] Images load quickly from cache on repeat views

## Migration Example

### Before (AvatarView.swift usage):
```swift
AvatarView(person: person, size: 60, showBorder: true)
```

### After (CharacterAvatar usage):
```swift
CharacterAvatar(person: person, size: 60, showBorder: true)
```

### Before (Direct SVGImageView usage):
```swift
if let url = URL(string: person.image) {
    SVGImageView(url: url, desiredSize: CGSize(width: 56, height: 56))
        .frame(width: 56, height: 56)
        .clipShape(Circle())
}
```

### After:
```swift
CharacterAvatar(
    imageURL: person.image,
    name: person.firstname,
    size: 56,
    showBorder: false
)
```

## Rollback Plan

If issues arise:
1. Keep `SVGImageView.swift` in a `Deprecated/` folder
2. Revert individual files that have problems
3. Both components can coexist during migration

## Timeline Estimate

- Phase 1 (Create component): 30 minutes
- Phase 2 (Migration): 1-2 hours
- Phase 3 (Cleanup): 15 minutes
- Phase 4 (Testing): 30 minutes

**Total: ~3 hours**

## Success Criteria

1. All character avatars load reliably (no blank images)
2. Placeholder shows immediately while loading
3. Loading indicator visible on slow connections
4. No flickering when scrolling or navigating
5. Images cached and load instantly on repeat views
6. Codebase has single source of truth for avatar display
