ivanscorral / ivanscorral/KoWorkout

[Feature] Implement Shared TagView Component

Open
#19 0 comments 0 reactions 1 assignee View on GitHub

@ivanscorral is already working on this.

Since Oct 14, 2025.

enhancement
Dominant language
Swift
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

# Implement Shared TagView Component

---

## Summary
Unify the tag UI used across exercise and workout views by introducing a single reusable SwiftUI component and refactoring existing usages to adopt it.

- Create a shared `TagView` SwiftUI component to render tags consistently.
- Replace ad-hoc tag UI in:
- `KoWorkout/Views/ExerciseListView.swift`
- `KoWorkout/Views/WorkoutDetail/WorkoutDetailView.swift`
- Place the component under a shared views directory for reuse across the app.

---

## Motivation

- Eliminate duplicated tag rendering logic and styling drift between views.
- Improve maintainability and velocity by centralizing styling and behavior.
- Ensure consistent accessibility, dynamic type, and dark mode support.
- Enable future tag variants (size, style, icon) in one place.

---

## Scope

**Include:**
- Add `TagView` component with configurable text, optional SF Symbol, and style/size variants.
- Support dynamic type, dark mode, and accessibility labels.
- Refactor the two target views to use `TagView`.
- Provide previews for common variants.

**Exclude:**
- Changes to data models, repositories, or navigation.
- New tag semantics (selection state, filter behavior).
- Localization expansion beyond existing app languages.

---

## Implementation Strategy

- Implement `TagView: View` with a simple public API:
- Initializers for text-only and text + systemImage.
- Style enum (e.g., `.filled`, `.tinted`, `.outlined`) with semantic colors.
- Size enum (e.g., `.small`, `.medium`) for padding, font, and corner radius.
- Use `Label` when an icon is provided to maintain spacing and accessibility.
- Apply semantic colors that adapt to color scheme and contrast.
- Add `TagViewStyle` configuration defaults to keep usage terse.
- Provide SwiftUI previews to demonstrate variants and validate appearance.
- Refactor both views to replace any Text + capsule/rounded rect stacks with `TagView`.

---

## Proposed Coverage / Functionality

### **Tag rendering**
- Displays provided text with optional SF Symbol.
- Applies consistent padding, background, border, corner radius, and font.
- Adapts colors for light/dark mode and increased contrast.
- Respects Dynamic Type and VoiceOver (accessible label mirrors content).

### **Variants**
- `.filled` — background color, contrasting text.
- `.tinted` — tint background with opacity; text uses tint color.
- `.outlined` — transparent fill, stroked border.

### **Sizes**
- `.small` and `.medium` with sensible defaults.

### **Integrations**
- `KoWorkout/Views/ExerciseListView.swift` uses `TagView` wherever a tag appears (e.g., muscle group, difficulty).
- `KoWorkout/Views/WorkoutDetail/WorkoutDetailView.swift` uses `TagView` for tag chips (e.g., duration, intensity, equipment).

### **Previews**
- Showcase each style/size with and without icons.

---

## Example Skeleton

**File:** `KoWorkout/Views/Shared/TagView.swift`

```swift
import SwiftUI

public enum TagStyle {
case filled(Color)
case tinted(Color)
case outlined(Color)
}

public enum TagSize {
case small
case medium

var font: Font {
switch self {
case .small: return .caption
case .medium: return .callout
}
}

var padding: EdgeInsets {
switch self {
case .small: return EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)
case .medium: return EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)
}
}

var cornerRadius: CGFloat { 10 }
var lineWidth: CGFloat { 1 }
}

public struct TagView: View {
private let text: String
private let systemImage: String?
private let style: TagStyle
private let size: TagSize

public init(_ text: String,
systemImage: String? = nil,
style: TagStyle = .tinted(.accentColor),
size: TagSize = .small) {
self.text = text
self.systemImage = systemImage
self.style = style
self.size = size
}

public var body: some View {
content
.font(size.font)
.padding(size.padding)
.background(background)
.overlay(overlay)
.clipShape(RoundedRectangle(cornerRadius: size.cornerRadius, style: .continuous))
.accessibilityLabel(Text(text))
}

@ViewBuilder
private var content: some View {
if let systemImage {
Label(text, systemImage: systemImage)
.labelStyle(.titleAndIcon)
.foregroundStyle(foregroundColor)
} else {
Text(text)
.foregroundStyle(foregroundColor)
}
}

private var foregroundColor: Color {
switch style {
case .filled(let color):
return color.isBright ? .black : .white
case .tinted(let color):
return color
case .outlined(let color):
return color
}
}

@ViewBuilder
private var background: some View {
switch style {
case .filled(let color):
color
case .tinted(let color):
color.opacity(0.15)
case .outlined:
Color.clear
}
}

@ViewBuilder
private var overlay: some View {
switch style {
case .outlined(let color):
RoundedRectangle(cornerRadius: size.cornerRadius, style: .continuous)
.stroke(color, lineWidth: size.lineWidth)
default:
EmptyView()
}
}
}

private extension Color {
var isBright: Bool {
#if canImport(UIKit)
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0
UIColor(self).getRed(&r, green: &g, blue: &b, alpha: nil)
return (0.299*r + 0.587*g + 0.114*b) > 0.6
#else
false
#endif
}
}
```

**Usage examples in views:**
- **ExerciseListView.swift**
`HStack { TagView("Strength", systemImage: "bolt.fill", style: .tinted(.orange)) }`

- **WorkoutDetailView.swift**
`HStack { TagView("45 min", systemImage: "clock", style: .filled(.accentColor), size: .medium) }`

---

## Acceptance Criteria

- [ ] Shared `TagView` compiles and previews render across variants.
- [ ] `ExerciseListView` and `WorkoutDetailView` use `TagView` for all tag UI.
- [ ] Visual styling is consistent between screens in light/dark modes.
- [ ] Dynamic Type and accessibility labels function as expected.
- [ ] Duplicate tag-related view code is removed from refactored files.
- [ ] Code compiles and runs on CI with no regressions.

---

## Files To Touch

- `KoWorkout/Views/Shared/TagView.swift` (new)
- `KoWorkout/Views/ExerciseListView.swift`
- `KoWorkout/Views/WorkoutDetail/WorkoutDetailView.swift`
- `KoWorkout.xcodeproj` project file updates if a new group is added for Shared views (if needed)

---

## Notes

- Align colors with existing design tokens or theme helpers if present; otherwise use `.accentColor` and semantic system colors.
- Prefer SF Symbols that exist for the minimum iOS target.
- If a local style system exists (e.g., custom Color/Font extensions), map `TagView` to those instead of hard-coded values.
- Consider future states (selected/disabled) when defining the style API, but do not implement them now unless already needed.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.