234 lines
6.4 KiB
Markdown
234 lines
6.4 KiB
Markdown
---
|
|
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
|