This commit is contained in:
Ho Ngoc Hai
2026-05-23 18:37:02 +07:00
parent f15d91ee29
commit 76d75c753b
3993 changed files with 403 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
---
name: swift-ui-components
description: Reusable SwiftUI components, Extensions, Validation patterns. Use for String/View extensions, custom UI components, hoặc khi cần reusable code.
compatibility: "Swift 5.9+, iOS 17+, SwiftUI"
metadata:
author: Velik Ho
version: "1.0"
---
# Swift UI Components & Extensions
Reusable components và Extensions cho SwiftUI Enterprise.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Creating string validation / Tạo validation chuỗi
- Building reusable views / Xây dựng views tái sử dụng
- View modifiers / Thêm view modifiers
- Localization patterns / Patterns đa ngôn ngữ
## String Extensions
```swift
// Core/Extensions/String+Extensions.swift
import Foundation
extension String {
// MARK: - Localization
/// Localized string
var localized: String {
NSLocalizedString(self, comment: "")
}
/// Localized with arguments
func localized(with arguments: CVarArg...) -> String {
String(format: localized, arguments: arguments)
}
// MARK: - Validation
/// Valid email check
var isValidEmail: Bool {
let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: self)
}
/// Valid Vietnamese phone
var isValidVietnamesePhone: Bool {
let regex = "^(0|\\+84)(3|5|7|8|9)[0-9]{8}$"
return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: self)
}
/// Valid password (8+ chars, upper, lower, digit)
var isValidPassword: Bool {
let regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{8,}$"
return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: self)
}
/// Trimmed string
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Is blank (empty or whitespace)
var isBlank: Bool {
trimmed.isEmpty
}
// MARK: - Formatting
/// Masked email (j***@example.com)
var maskedEmail: String {
guard isValidEmail else { return self }
let parts = split(separator: "@")
guard parts.count == 2 else { return self }
let local = String(parts[0])
let domain = String(parts[1])
if local.count <= 2 {
return "\(local.prefix(1))***@\(domain)"
}
return "\(local.prefix(1))***\(local.suffix(1))@\(domain)"
}
/// Format VND currency
static func formatVND(_ amount: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = "VND"
formatter.currencySymbol = ""
formatter.maximumFractionDigits = 0
return formatter.string(from: NSNumber(value: amount)) ?? "\(Int(amount))"
}
}
```
## View Extensions
```swift
// Core/Extensions/View+Extensions.swift
import SwiftUI
extension View {
/// Conditional modifier
@ViewBuilder
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
/// Hide keyboard
func hideKeyboard() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil
)
}
/// Corner radius with specific corners
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
/// Custom rounded corner shape
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
```
## Custom Components
### Primary Button
```swift
/// Primary action button
struct PrimaryButton: View {
let title: String
let action: () -> Void
var isLoading: Bool = false
var isDisabled: Bool = false
var body: some View {
Button(action: action) {
HStack(spacing: DesignSystem.spacingSM) {
if isLoading {
ProgressView()
.tint(.white)
}
Text(title)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(.vertical, DesignSystem.spacingMD)
.background(isDisabled ? Color.gray : Color.accentColor)
.foregroundColor(.white)
.cornerRadius(DesignSystem.cornerRadiusMD)
}
.disabled(isDisabled || isLoading)
}
}
```
### Text Field with Validation
```swift
/// Validated text field
struct ValidatedTextField: View {
let placeholder: String
@Binding var text: String
var isSecure: Bool = false
var isValid: Bool = true
var errorMessage: String?
var body: some View {
VStack(alignment: .leading, spacing: DesignSystem.spacingXS) {
Group {
if isSecure {
SecureField(placeholder, text: $text)
} else {
TextField(placeholder, text: $text)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(DesignSystem.cornerRadiusSM)
.overlay(
RoundedRectangle(cornerRadius: DesignSystem.cornerRadiusSM)
.stroke(isValid ? Color.clear : Color.red, lineWidth: 1)
)
if let error = errorMessage, !isValid {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
}
}
}
```
## Quick Reference / Tham Chiếu Nhanh
| Pattern | Usage |
|---------|-------|
| `string.localized` | NSLocalizedString wrapper |
| `string.isValidEmail` | Email regex validation |
| `string.trimmed` | Remove whitespace |
| `.if(condition) { }` | Conditional modifier |
| `PrimaryButton` | Loading + disabled support |
## Resources / Tài Nguyên
- [Swift Enterprise Architect](../swift-enterprise-architect/SKILL.md)
- [Swift Security](../swift-security/SKILL.md) - Validation patterns