--- 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`(_ 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