feat: Thêm các kỹ năng Swift mới về kiến trúc doanh nghiệp, mạng, bảo mật, mẫu kiểm thử và thành phần UI.

This commit is contained in:
Ho Ngoc Hai
2026-01-16 10:46:44 +07:00
parent 334d66c91f
commit f62464bc36
6 changed files with 1401 additions and 0 deletions

View File

@@ -0,0 +1,449 @@
---
name: swift-enterprise-architect
description: Kiến trúc và patterns cho ứng dụng SwiftUI Enterprise (MVVM, DI, Navigation, Project Structure). Use for iOS/macOS apps, SwiftUI development, hoặc khi cần structured Swift architecture.
compatibility: "Swift 5.9+, iOS 17+, macOS 14+, SwiftUI"
metadata:
author: Velik Ho
version: "1.0"
references: "Apple SwiftUI Documentation, Swift Concurrency"
---
# Swift Enterprise Development Workflow
Quy trình 4 giai đoạn để phát triển ứng dụng SwiftUI theo chuẩn Enterprise.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Building iOS/macOS/visionOS apps / Xây dựng app Apple platforms
- Creating enterprise SwiftUI applications / Tạo ứng dụng SwiftUI enterprise
- Need MVVM + DI architecture / Cần kiến trúc MVVM + DI
- Implementing navigation patterns / Triển khai điều hướng
**DO NOT use when:**
- Simple single-screen apps / App đơn giản 1 màn hình
- UIKit-only projects / Dự án chỉ UIKit
- Backend Swift (use Vapor patterns) / Swift backend
## Overview / Tổng Quan
```
┌──────────────────────────────────────────────────────────────────┐
│ WORKFLOW 4 GIAI ĐOẠN (Swift Enterprise) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PHASE 1 │────►│ PHASE 2 │ │
│ │ PROJECT │ │ ARCHITECTURE │ │
│ │ STRUCTURE │ │ │ │
│ │ - Folders │ │ - MVVM Pattern │ │
│ │ - Resources │ │ - DI Setup │ │
│ │ - Config │ │ - Services │ │
│ └─────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PHASE 4 │◄────│ PHASE 3 │ │
│ │ PLATFORM │ │ UI & NAV │ │
│ │ │ │ │ │
│ │ - Extensions│ │ - TabView │ │
│ │ - Platform │ │ - NavStack │ │
│ │ - Native │ │ - Sheets │ │
│ └─────────────┘ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
---
## Phase 1: Project Structure / Cấu Trúc Dự Án
**Goal**: Thiết lập cấu trúc thư mục chuẩn Enterprise
### Project Structure
```
MyApp/
├── MyAppApp.swift # App entry point
├── ContentView.swift # Root view
├── Core/ # Core utilities
│ ├── Constants/
│ │ └── Constants.swift # App-wide constants
│ └── Extensions/
│ ├── String+Extensions.swift
│ └── View+Extensions.swift
├── Models/ # Data models
│ └── User.swift
├── Services/ # Business services
│ ├── APIService.swift
│ └── AuthManager.swift
├── ViewModels/ # MVVM ViewModels
│ ├── AuthViewModel.swift
│ └── HomeViewModel.swift
├── Views/ # SwiftUI Views
│ ├── Auth/
│ │ ├── LoginView.swift
│ │ └── RegisterView.swift
│ ├── Home/
│ │ └── HomeView.swift
│ └── Screens/
│ ├── ProfileView.swift
│ └── SettingsView.swift
└── Resources/
├── Assets.xcassets/
└── Localizable.strings
```
### Constants Pattern
```swift
// Core/Constants/Constants.swift
// MARK: - API Configuration
enum APIConfig {
static let baseURL = "https://api.example.com"
static let apiVersion = "/api/v1"
static let timeout: TimeInterval = 30.0
}
// MARK: - Storage Keys
enum StorageKeys {
static let accessToken = "access_token"
static let refreshToken = "refresh_token"
static let userData = "user_data"
}
// MARK: - Design System
enum DesignSystem {
// Spacing
static let spacingXS: CGFloat = 4
static let spacingSM: CGFloat = 8
static let spacingMD: CGFloat = 16
static let spacingLG: CGFloat = 24
// Corner Radius
static let cornerRadiusSM: CGFloat = 8
static let cornerRadiusMD: CGFloat = 12
static let cornerRadiusLG: CGFloat = 16
}
```
---
## Phase 2: Architecture (MVVM + DI) / Kiến Trúc
**Goal**: Thiết lập MVVM pattern với Swift Concurrency
### ViewModel Pattern (BẮT BUỘC)
```swift
// ViewModels/SomeViewModel.swift
import SwiftUI
import Combine
/// ViewModel for SomeView
/// ViewModel cho SomeView
@MainActor
final class SomeViewModel: ObservableObject {
// MARK: - Published Properties
/// Current items
/// Các items hin ti
@Published private(set) var items: [Item] = []
/// Loading state
/// Trng thái đang ti
@Published private(set) var isLoading = false
/// Error message
/// Thông báo li
@Published var errorMessage: String?
// MARK: - Dependencies
private let service: SomeServiceProtocol
// MARK: - Init
/// Initialize with dependencies
/// Khi to vi dependencies
init(service: SomeServiceProtocol = SomeService.shared) {
self.service = service
}
// MARK: - Public Methods
/// Load items from service
/// Ti items t service
func loadItems() async {
isLoading = true
errorMessage = nil
do {
items = try await service.fetchItems()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
```
### Service Pattern
```swift
// Services/SomeService.swift
/// Service protocol for DI
/// Protocol service cho DI
protocol SomeServiceProtocol {
func fetchItems() async throws -> [Item]
}
/// Main service implementation
/// Implementation service chính
final class SomeService: SomeServiceProtocol {
/// Shared singleton
/// Singleton dùng chung
static let shared = SomeService()
private init() {}
func fetchItems() async throws -> [Item] {
// Implementation
}
}
```
### Dependency Injection via Init
```swift
// GOOD: Protocol-based DI
final class HomeViewModel: ObservableObject {
private let authManager: AuthManagerProtocol
private let apiService: APIServiceProtocol
init(
authManager: AuthManagerProtocol = AuthManager.shared,
apiService: APIServiceProtocol = APIService.shared
) {
self.authManager = authManager
self.apiService = apiService
}
}
// Testing
let mockAuth = MockAuthManager()
let viewModel = HomeViewModel(authManager: mockAuth)
```
---
## Phase 3: UI & Navigation / Giao Diện & Điều Hướng
**Goal**: Xây dựng UI với TabView và NavigationStack
### TabView Navigation
```swift
// ContentView.swift
struct ContentView: View {
/// Selected tab
@State private var selectedTab: Tab = .home
/// Tab enumeration
enum Tab: String, CaseIterable {
case home, explore, profile
var title: String {
switch self {
case .home: return "Home"
case .explore: return "Explore"
case .profile: return "Profile"
}
}
var icon: String {
switch self {
case .home: return "house"
case .explore: return "magnifyingglass"
case .profile: return "person"
}
}
}
var body: some View {
TabView(selection: $selectedTab) {
HomeView()
.tabItem { Label(Tab.home.title, systemImage: Tab.home.icon) }
.tag(Tab.home)
ExploreView()
.tabItem { Label(Tab.explore.title, systemImage: Tab.explore.icon) }
.tag(Tab.explore)
ProfileView()
.tabItem { Label(Tab.profile.title, systemImage: Tab.profile.icon) }
.tag(Tab.profile)
}
}
}
```
### NavigationStack with Path
```swift
// Views/Home/HomeView.swift
struct HomeView: View {
@StateObject private var viewModel = HomeViewModel()
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
List(viewModel.items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationTitle("Home")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
.task {
await viewModel.loadItems()
}
}
}
}
```
### Auth State Conditional UI
```swift
// ContentView.swift with Auth State
struct ContentView: View {
@StateObject private var authManager = AuthManager.shared
var body: some View {
Group {
switch authManager.authState {
case .unknown:
ProgressView()
case .unauthenticated:
AuthContainerView()
case .authenticated:
MainTabView()
}
}
.task {
await authManager.initialize()
}
}
}
```
---
## Phase 4: Platform & Extensions / Nền Tảng & Extensions
### String Extensions
```swift
// Core/Extensions/String+Extensions.swift
extension String {
/// Localized string
/// Chui đã bn đa hóa
var localized: String {
NSLocalizedString(self, comment: "")
}
/// Email validation
/// Kim tra email hp l
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)
}
/// Trimmed string
/// Chui đã trim
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
}
```
### View Extensions
```swift
// Core/Extensions/View+Extensions.swift
extension View {
/// Apply modifier conditionally
/// Áp dng modifier có điu kin
@ViewBuilder
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
/// Hide keyboard
/// n bàn phím
func hideKeyboard() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil
)
}
}
```
---
## Common Mistakes / Lỗi Thường Gặp
| Mistake | Problem | Solution |
|---------|---------|----------|
| Logic in View | Hard to test | Move to ViewModel |
| Missing `@MainActor` | Thread issues | Add to ViewModel class |
| Force unwrap `!` | Crashes | Use `if let`, `guard let` |
| Singleton abuse | Hard to test | Protocol-based DI |
| No loading states | Bad UX | Add `isLoading` property |
| Hardcoded strings | No i18n | Use `String.localized` |
## Quick Reference / Tham Chiếu Nhanh
| Category | Standard |
|----------|----------|
| Architecture | MVVM + Protocol DI |
| ViewModel | `@MainActor final class` + `ObservableObject` |
| State | `@Published`, `@State`, `@StateObject` |
| Navigation | `NavigationStack` + `NavigationPath` |
| Tabs | `TabView` with enum-based `Tab` |
| Async | `async/await`, `.task {}` modifier |
| Constants | Enum-based (no instance) |
| Comments | Bilingual EN/VI |
## Resources / Tài Nguyên
- [Swift Networking](../swift-networking/SKILL.md) - HTTP client patterns
- [Swift Security](../swift-security/SKILL.md) - Keychain & Auth
- [Swift UI Components](../swift-ui-components/SKILL.md) - Reusable components
- [Apple SwiftUI Docs](https://developer.apple.com/documentation/swiftui/)
- [Swift Concurrency](https://developer.apple.com/documentation/swift/concurrency)
- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards

View File

@@ -0,0 +1,254 @@
---
name: swift-networking
description: HTTP client, API handling, Error management cho Swift Enterprise. Use for REST APIs, URLSession, async networking, hoặc khi cần structured API layer.
compatibility: "Swift 5.9+, iOS 17+, Foundation"
metadata:
author: Velik Ho
version: "1.0"
references: "URLSession, Swift Concurrency"
---
# Swift Networking Patterns
HTTP client và API handling patterns cho Swift Enterprise applications.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Building REST API client / Xây dựng REST API client
- Need URLSession wrapper / Cần wrapper cho URLSession
- Implementing token-based auth / Triển khai auth token-based
- Error handling for APIs / Xử lý lỗi API
## Core Patterns / Mẫu Chính
### API Error Enum
```swift
// Services/APIError.swift
/// API error types
/// Các loi li API
enum APIError: Error, LocalizedError {
case invalidURL
case noData
case decodingError(Error)
case networkError(Error)
case serverError(statusCode: Int, message: String?)
case unauthorized
case forbidden
case notFound
case rateLimited
case unknown
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL / URL không hợp lệ"
case .noData:
return "No data received / Không nhận được dữ liệu"
case .decodingError(let error):
return "Decoding error: \(error.localizedDescription)"
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
case .serverError(let code, let message):
return "Server error (\(code)): \(message ?? "Unknown")"
case .unauthorized:
return "Unauthorized / Chưa xác thực"
case .forbidden:
return "Access forbidden / Truy cập bị từ chối"
case .notFound:
return "Resource not found / Không tìm thấy"
case .rateLimited:
return "Rate limited / Giới hạn request"
case .unknown:
return "Unknown error / Lỗi không xác định"
}
}
}
```
### HTTP Method Enum
```swift
/// HTTP request methods
/// Các phương thc HTTP
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
```
### API Service Protocol
```swift
/// API service protocol for DI
/// Protocol API service cho DI
protocol APIServiceProtocol {
func request<T: Decodable>(
endpoint: String,
method: HTTPMethod,
body: Encodable?,
headers: [String: String]?
) async throws -> T
}
```
### API Service Implementation
```swift
// Services/APIService.swift
/// Main API service
/// Dch v API chính
final class APIService: APIServiceProtocol {
// MARK: - Singleton
static let shared = APIService()
// MARK: - Properties
private let session: URLSession
private let encoder: JSONEncoder
private let decoder: JSONDecoder
// MARK: - Init
init(session: URLSession = .shared) {
self.session = session
self.encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
}
// MARK: - Request
/// Perform network request
/// Thc hin request network
func request<T: Decodable>(
endpoint: String,
method: HTTPMethod = .get,
body: Encodable? = nil,
headers: [String: String]? = nil
) async throws -> T {
// Build URL
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + endpoint) else {
throw APIError.invalidURL
}
// Create request
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.timeoutInterval = APIConfig.timeout
// Set headers
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// Add auth token
if let token = await AuthManager.shared.accessToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
// Add custom headers
headers?.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
// Set body
if let body = body {
request.httpBody = try encoder.encode(body)
}
// Perform request
let (data, response) = try await session.data(for: request)
// Handle response
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.unknown
}
// Check status code
switch httpResponse.statusCode {
case 200...299:
do {
return try decoder.decode(T.self, from: data)
} catch {
throw APIError.decodingError(error)
}
case 401:
await AuthManager.shared.handleUnauthorized()
throw APIError.unauthorized
case 403:
throw APIError.forbidden
case 404:
throw APIError.notFound
case 429:
throw APIError.rateLimited
default:
let message = String(data: data, encoding: .utf8)
throw APIError.serverError(statusCode: httpResponse.statusCode, message: message)
}
}
// MARK: - Convenience Methods
/// GET request
func get<T: Decodable>(endpoint: String) async throws -> T {
try await request(endpoint: endpoint, method: .get, body: nil as String?, headers: nil)
}
/// POST request
func post<T: Decodable, B: Encodable>(endpoint: String, body: B) async throws -> T {
try await request(endpoint: endpoint, method: .post, body: body, headers: nil)
}
/// PUT request
func put<T: Decodable, B: Encodable>(endpoint: String, body: B) async throws -> T {
try await request(endpoint: endpoint, method: .put, body: body, headers: nil)
}
/// DELETE request
func delete<T: Decodable>(endpoint: String) async throws -> T {
try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil)
}
}
```
---
## Common Mistakes / Lỗi Thường Gặp
| Mistake | Problem | Solution |
|---------|---------|----------|
| No error handling | Crashes | Use `do-catch` with custom errors |
| Force unwrap URLs | Crashes | Guard with `APIError.invalidURL` |
| Blocking main thread | Frozen UI | Use `async/await` |
| No timeout | Hanging requests | Set `timeoutInterval` |
| Hardcoded URLs | Hard to maintain | Use `APIConfig` enum |
## Quick Reference / Tham Chiếu Nhanh
| Category | Standard |
|----------|----------|
| HTTP Client | URLSession with async/await |
| Errors | Custom `APIError` enum |
| Encoding | snake_case JSON |
| Auth | Bearer token injection |
| Timeout | 30 seconds default |
## Resources / Tài Nguyên
- [Swift Enterprise Architect](../swift-enterprise-architect/SKILL.md) - Architecture
- [Swift Security](../swift-security/SKILL.md) - Token management
- [Apple URLSession](https://developer.apple.com/documentation/foundation/urlsession)

View File

@@ -0,0 +1,294 @@
---
name: swift-security
description: Security patterns cho Swift - Keychain, Token management, Auth State Machine, Biometric. Use for secure storage, authentication flows, hoặc khi cần security best practices.
compatibility: "Swift 5.9+, iOS 17+, Security Framework"
metadata:
author: Velik Ho
version: "1.0"
references: "Apple Security Framework, Keychain Services"
---
# Swift Security Patterns
Keychain, Token management, và Auth patterns cho Swift Enterprise.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Storing sensitive data / Lưu trữ dữ liệu nhạy cảm
- Implementing auth flows / Triển khai luồng xác thực
- Token management / Quản lý tokens
- Biometric authentication / Xác thực sinh trắc học
## Core Patterns / Mẫu Chính
### Keychain Helper
```swift
// Services/KeychainHelper.swift
import Security
/// Keychain operations helper
/// Helper cho các thao tác Keychain
enum KeychainHelper {
/// Save value to Keychain
/// Lưu giá tr vào Keychain
static func save(key: String, value: String) {
guard let data = value.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: AppConstants.keychainService,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
// Delete existing
SecItemDelete(query as CFDictionary)
// Add new
SecItemAdd(query as CFDictionary, nil)
}
/// Read value from Keychain
/// Đc giá tr t Keychain
static func read(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: AppConstants.keychainService,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
guard status == errSecSuccess,
let data = dataTypeRef as? Data,
let value = String(data: data, encoding: .utf8)
else {
return nil
}
return value
}
/// Delete value from Keychain
/// Xóa giá tr khi Keychain
static func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: AppConstants.keychainService,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}
```
### Auth State Machine
```swift
// Services/AuthState.swift
/// Authentication state enumeration
/// Enum trng thái xác thc
enum AuthState: Equatable {
case unknown
case unauthenticated
case authenticated(User)
var isAuthenticated: Bool {
if case .authenticated = self {
return true
}
return false
}
var user: User? {
if case .authenticated(let user) = self {
return user
}
return nil
}
}
```
### Auth Manager
```swift
// Services/AuthManager.swift
/// Authentication manager protocol
protocol AuthManagerProtocol {
var authState: AuthState { get }
var accessToken: String? { get }
func initialize() async
func login(email: String, password: String) async throws
func logout()
}
/// Main authentication manager
/// Qun lý xác thc chính
@MainActor
final class AuthManager: ObservableObject, AuthManagerProtocol {
// MARK: - Singleton
static let shared = AuthManager()
// MARK: - Published
@Published private(set) var authState: AuthState = .unknown
// MARK: - Properties
var accessToken: String? {
KeychainHelper.read(key: StorageKeys.accessToken)
}
var refreshToken: String? {
KeychainHelper.read(key: StorageKeys.refreshToken)
}
// MARK: - Init
private init() {}
// MARK: - Public Methods
/// Initialize auth on app launch
/// Khi to auth khi app khi đng
func initialize() async {
guard accessToken != nil else {
authState = .unauthenticated
return
}
// Try cached user
if let userData = UserDefaults.standard.data(forKey: StorageKeys.userData),
let user = try? JSONDecoder().decode(User.self, from: userData) {
authState = .authenticated(user)
} else {
await refreshCurrentUser()
}
}
/// Login with credentials
/// Đăng nhp vi credentials
func login(email: String, password: String) async throws {
struct LoginRequest: Encodable {
let email: String
let password: String
}
struct LoginResponse: Decodable {
let accessToken: String
let refreshToken: String
let user: User
}
let request = LoginRequest(email: email, password: password)
let response: LoginResponse = try await APIService.shared.post(
endpoint: "/auth/login",
body: request
)
// Save tokens
KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken)
KeychainHelper.save(key: StorageKeys.refreshToken, value: response.refreshToken)
// Cache user
if let userData = try? JSONEncoder().encode(response.user) {
UserDefaults.standard.set(userData, forKey: StorageKeys.userData)
}
authState = .authenticated(response.user)
}
/// Logout current user
/// Đăng xut
func logout() {
KeychainHelper.delete(key: StorageKeys.accessToken)
KeychainHelper.delete(key: StorageKeys.refreshToken)
UserDefaults.standard.removeObject(forKey: StorageKeys.userData)
authState = .unauthenticated
}
/// Handle 401 unauthorized
/// X lý 401 unauthorized
func handleUnauthorized() {
Task {
let success = await refreshTokens()
if !success {
logout()
}
}
}
// MARK: - Private
private func refreshCurrentUser() async {
do {
let user: User = try await APIService.shared.get(endpoint: "/auth/me")
if let userData = try? JSONEncoder().encode(user) {
UserDefaults.standard.set(userData, forKey: StorageKeys.userData)
}
authState = .authenticated(user)
} catch {
authState = .unauthenticated
}
}
private func refreshTokens() async -> Bool {
guard let refreshToken = refreshToken else { return false }
struct RefreshRequest: Encodable { let refreshToken: String }
struct RefreshResponse: Decodable { let accessToken: String; let refreshToken: String }
do {
let response: RefreshResponse = try await APIService.shared.post(
endpoint: "/auth/refresh",
body: RefreshRequest(refreshToken: refreshToken)
)
KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken)
KeychainHelper.save(key: StorageKeys.refreshToken, value: response.refreshToken)
return true
} catch {
return false
}
}
}
```
---
## Common Mistakes / Lỗi Thường Gặp
| Mistake | Problem | Solution |
|---------|---------|----------|
| Store tokens in UserDefaults | Insecure | Use Keychain |
| No token refresh | User logged out | Implement refresh flow |
| Missing `@MainActor` | Thread issues | Add to AuthManager |
| Hardcoded service name | Conflicts | Use `AppConstants` |
## Quick Reference / Tham Chiếu Nhanh
| Category | Standard |
|----------|----------|
| Token Storage | Keychain only |
| Auth State | Enum-based state machine |
| Manager | `@MainActor` + `ObservableObject` |
| User Cache | UserDefaults (non-sensitive) |
| 401 Handling | Refresh token, then logout |
## Resources / Tài Nguyên
- [Swift Enterprise Architect](../swift-enterprise-architect/SKILL.md) - Architecture
- [Swift Networking](../swift-networking/SKILL.md) - API layer
- [Apple Keychain Services](https://developer.apple.com/documentation/security/keychain_services)

View File

@@ -0,0 +1,171 @@
---
name: swift-testing-patterns
description: Unit testing, Mocking, UI testing patterns cho Swift Enterprise (XCTest, async testing). Use for ViewModels testing, mocking services, hoặc testing best practices.
compatibility: "Swift 5.9+, XCTest, Swift Testing"
metadata:
author: Velik Ho
version: "1.0"
---
# Swift Testing Patterns
Unit và Integration testing patterns cho Swift Enterprise.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Writing unit tests / Viết unit tests
- Mocking services / Mock services
- Testing ViewModels / Test ViewModels
- Async code testing / Test async code
## Core Patterns / Mẫu Chính
### Mock Service
```swift
// Tests/Mocks/MockAPIService.swift
final class MockAPIService: APIServiceProtocol {
var mockResult: Any?
var mockError: Error?
var requestCalled = false
func request<T: Decodable>(
endpoint: String,
method: HTTPMethod,
body: Encodable?,
headers: [String: String]?
) async throws -> T {
requestCalled = true
if let error = mockError {
throw error
}
guard let result = mockResult as? T else {
throw APIError.unknown
}
return result
}
}
```
### ViewModel Testing
```swift
// Tests/ViewModelTests/HomeViewModelTests.swift
import XCTest
@testable import MyApp
@MainActor
final class HomeViewModelTests: XCTestCase {
var sut: HomeViewModel!
var mockService: MockAPIService!
override func setUp() {
super.setUp()
mockService = MockAPIService()
sut = HomeViewModel(apiService: mockService)
}
override func tearDown() {
sut = nil
mockService = nil
super.tearDown()
}
func test_loadItems_success() async {
// Arrange
let expectedItems = [Item(id: "1", name: "Test")]
mockService.mockResult = expectedItems
// Act
await sut.loadItems()
// Assert
XCTAssertEqual(sut.items.count, 1)
XCTAssertEqual(sut.items.first?.name, "Test")
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.errorMessage)
}
func test_loadItems_failure() async {
// Arrange
mockService.mockError = APIError.networkError(NSError(domain: "", code: -1))
// Act
await sut.loadItems()
// Assert
XCTAssertTrue(sut.items.isEmpty)
XCTAssertNotNil(sut.errorMessage)
}
}
```
### Async Testing
```swift
func test_asyncOperation() async throws {
// Use async/await directly
let result = try await sut.performAsync()
XCTAssertTrue(result)
}
// With expectation (legacy)
func test_asyncWithExpectation() {
let expectation = expectation(description: "Async complete")
Task {
await sut.loadData()
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
XCTAssertFalse(sut.items.isEmpty)
}
```
### Published Property Testing
```swift
import Combine
func test_publishedProperty() {
var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Value changed")
sut.$items
.dropFirst() // Skip initial value
.sink { items in
XCTAssertEqual(items.count, 1)
expectation.fulfill()
}
.store(in: &cancellables)
Task {
await sut.loadItems()
}
wait(for: [expectation], timeout: 5.0)
}
```
## Quick Reference / Tham Chiếu Nhanh
| Pattern | Usage |
|---------|-------|
| Mock Service | Protocol + mock implementation |
| `@MainActor` | Required for ViewModel tests |
| Arrange-Act-Assert | Standard test structure |
| `async throws` | Direct async testing |
## Resources / Tài Nguyên
- [Swift Enterprise Architect](../swift-enterprise-architect/SKILL.md)
- [Apple XCTest](https://developer.apple.com/documentation/xctest)

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