From c437ea4c9fd4a26ec0d6e182eaed88b2ada53a75 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Fri, 16 Jan 2026 10:53:16 +0700 Subject: [PATCH] =?UTF-8?q?docs:=20Th=C3=AAm=20t=C3=A0i=20li=E1=BB=87u=20k?= =?UTF-8?q?i=E1=BA=BFn=20tr=C3=BAc=20`ARCHITECTURE.md`=20v=C3=A0=20c?= =?UTF-8?q?=E1=BA=ADp=20nh=E1=BA=ADt=20`README.md`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/app-client-base-swift/ARCHITECTURE.md | 301 +++++++++++++++ .../UserInterfaceState.xcuserstate | Bin 50051 -> 53446 bytes .../Core/Constants/Constants.swift | 15 +- .../Services/APIService.swift | 90 +++++ apps/app-client-base-swift/README.md | 352 ++++++++++++------ 5 files changed, 642 insertions(+), 116 deletions(-) create mode 100644 apps/app-client-base-swift/ARCHITECTURE.md diff --git a/apps/app-client-base-swift/ARCHITECTURE.md b/apps/app-client-base-swift/ARCHITECTURE.md new file mode 100644 index 00000000..aebfdf68 --- /dev/null +++ b/apps/app-client-base-swift/ARCHITECTURE.md @@ -0,0 +1,301 @@ +# Architecture / Kiến Trúc + +> **EN**: Detailed architecture documentation for AppClientBaseSwift iOS application. +> **VI**: Tài liệu kiến trúc chi tiết cho ứng dụng iOS AppClientBaseSwift. + +## Overview / Tổng Quan + +AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-ViewModel)** architecture pattern with **SwiftUI** for declarative UI. The app follows Apple's modern development best practices including: + +- **Swift Concurrency** (async/await) +- **Combine** for reactive data binding +- **Protocol-oriented programming** for testability +- **Keychain Services** for secure storage + +## Architecture Diagram / Sơ Đồ Kiến Trúc + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PRESENTATION LAYER │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ SwiftUI Views │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ SplashView │ │ HomeView │ │ ExploreView │ │ ProfileView │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ LoginView │ │RegisterView │ │ForgotPasswd │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ @StateObject / @EnvironmentObject │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ViewModels (@MainActor) │ │ +│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ +│ │ │ AuthViewModel │ │ HomeViewModel │ │ProfileViewModel│ │ │ +│ │ │ @Published │ │ @Published │ │ @Published │ │ │ +│ │ │ - isLoading │ │ - items │ │ - user │ │ │ +│ │ │ - errorMessage │ │ - greeting │ │ - isEditing │ │ │ +│ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + Dependency Injection (Protocol-based) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ SERVICE LAYER │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ APIService │ │ AuthManager │ │ +│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ +│ │ │ APIServiceProtocol │ │ │ │ @Published authState │ │ │ +│ │ │ - request() │ │ │ │ - login() │ │ │ +│ │ │ - get(), post() │ │ │ │ - register() │ │ │ +│ │ │ - put(), delete() │ │ │ │ - logout() │ │ │ +│ │ └───────────────────────┘ │ │ │ - refreshToken() │ │ │ +│ │ URLSession │ │ │ Keychain │ │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ DATA LAYER │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Models │ │ Constants │ │ +│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ +│ │ │ User (Codable) │ │ │ │ APIConfig │ │ │ +│ │ │ HomeItem │ │ │ │ AppConstants │ │ │ +│ │ │ AuthState │ │ │ │ StorageKeys │ │ │ +│ │ └───────────────────────┘ │ │ │ DesignSystem │ │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Component Details / Chi Tiết Component + +### 1. Presentation Layer + +#### Views +| Component | Responsibility / Trách nhiệm | +|-----------|------------------------------| +| `SplashView` | Animated splash screen, delayed navigation | +| `ContentView` | Root TabView container, auth state routing | +| `AuthContainerView` | Auth flow navigation (Login/Register/Forgot) | +| `HomeView` | Home tab with greeting, promo, services | +| `ExploreView` | Discovery and search features | +| `ProfileView` | User profile and settings | + +#### ViewModels +```swift +@MainActor +final class HomeViewModel: ObservableObject { + // Reactive properties + @Published var isLoading: Bool = false + @Published var items: [HomeItem] = [] + @Published var errorMessage: String? + + // Dependencies injected via init + private let apiService: APIServiceProtocol + + // Async methods using Swift Concurrency + func loadData() async { ... } +} +``` + +### 2. Service Layer + +#### APIService +HTTP client following **Single Responsibility Principle**: + +```swift +protocol APIServiceProtocol { + func request( + endpoint: String, + method: HTTPMethod, + body: Encodable?, + headers: [String: String]? + ) async throws -> T +} +``` + +**Features:** +- Generic request/response handling +- Automatic JSON encoding/decoding (snake_case ↔ camelCase) +- Bearer token injection +- HTTP status code handling +- Error categorization + +#### AuthManager +Singleton for authentication state: + +```swift +final class AuthManager: ObservableObject { + @MainActor static let shared = AuthManager() + + @Published var authState: AuthState = .unknown + + // Keychain-backed tokens + var accessToken: String? { get } + var refreshToken: String? { get } +} +``` + +**AuthState Enum:** +```swift +enum AuthState { + case unknown // Initial state / Trạng thái khởi tạo + case unauthenticated // Logged out / Chưa đăng nhập + case authenticated(User) // Logged in / Đã đăng nhập +} +``` + +### 3. Data Layer + +#### Models +```swift +struct User: Codable, Identifiable, Equatable { + let id: String + let email: String + let name: String + let avatarUrl: String? + let phoneNumber: String? + let isEmailVerified: Bool + let createdAt: Date? + let updatedAt: Date? +} +``` + +#### Constants +Organized into semantic enums: +- `APIConfig`: Base URL, version, timeout +- `AppConstants`: App name, bundle ID, keychain service +- `StorageKeys`: UserDefaults/Keychain keys +- `DesignSystem`: Spacing, corner radius, font sizes + +## Data Flow / Luồng Dữ Liệu + +```mermaid +sequenceDiagram + participant V as View + participant VM as ViewModel + participant S as Service + participant API as Backend API + + V->>VM: User Action (button tap) + VM->>VM: Set isLoading = true + VM->>S: await service.request() + S->>API: HTTP Request + API-->>S: JSON Response + S-->>VM: Decoded Model + VM->>VM: Update @Published + VM-->>V: SwiftUI re-render +``` + +## Authentication Flow / Luồng Xác Thực + +```mermaid +stateDiagram-v2 + [*] --> Unknown: App Launch + Unknown --> Authenticated: Token Found + Valid + Unknown --> Unauthenticated: No Token + + Unauthenticated --> Login: Show Login + Login --> Authenticated: Login Success + Login --> Register: Navigate + Register --> Authenticated: Register Success + + Authenticated --> HomeScreen: Show Main App + HomeScreen --> Unauthenticated: Logout + + Authenticated --> TokenRefresh: Token Expired + TokenRefresh --> Authenticated: Refresh Success + TokenRefresh --> Unauthenticated: Refresh Failed +``` + +## Design Decisions / Quyết Định Thiết Kế + +### 1. Why MVVM? / Tại Sao MVVM? + +| Benefit / Lợi ích | Description / Mô tả | +|-------------------|---------------------| +| Testability | ViewModel có thể test độc lập không cần UI | +| Separation of Concerns | View chỉ hiển thị, logic nằm ở ViewModel | +| SwiftUI Compatibility | `@ObservableObject` + `@Published` native | +| Reactive Updates | Combine-based automatic UI refresh | + +### 2. Why Protocol-based DI? + +```swift +// Protocol enables mocking for tests +protocol APIServiceProtocol { + func get(endpoint: String) async throws -> T +} + +// Production implementation +final class APIService: APIServiceProtocol { ... } + +// Test mock +final class MockAPIService: APIServiceProtocol { ... } +``` + +### 3. Why Keychain over UserDefaults? + +| Keychain | UserDefaults | +|----------|--------------| +| ✅ Encrypted at rest | ❌ Plain text | +| ✅ Secure enclave | ❌ Accessible | +| ✅ App-specific | ❌ Shared prefs | + +### 4. Why @MainActor on ViewModels? + +```swift +@MainActor +final class HomeViewModel: ObservableObject { ... } +``` + +- Ensures all `@Published` updates happen on main thread +- Prevents concurrency issues with SwiftUI +- Explicit thread safety contract + +## Security Architecture / Kiến Trúc Bảo Mật + +``` +┌────────────────────────────────────────────────────────────┐ +│ SECURITY LAYERS │ +├────────────────────────────────────────────────────────────┤ +│ Layer 1: Transport Security (HTTPS/TLS) │ +│ • All API calls use HTTPS │ +│ • Certificate pinning (TODO) │ +├────────────────────────────────────────────────────────────┤ +│ Layer 2: Token Security (Keychain) │ +│ • Access token stored in Keychain │ +│ • Refresh token stored in Keychain │ +│ • kSecClassGenericPassword protection │ +├────────────────────────────────────────────────────────────┤ +│ Layer 3: Session Security │ +│ • Token expiry validation │ +│ • Automatic token refresh │ +│ • Secure logout (clear all tokens) │ +├────────────────────────────────────────────────────────────┤ +│ Layer 4: Input Validation │ +│ • Email format validation │ +│ • Password strength checking │ +│ • Form field sanitization │ +└────────────────────────────────────────────────────────────┘ +``` + +## Future Considerations / Hướng Phát Triển + +| Feature | Priority | Description | +|---------|----------|-------------| +| Certificate Pinning | High | TLS certificate validation | +| Biometric Auth | High | Face ID / Touch ID login | +| Offline Mode | Medium | Local caching with SwiftData | +| Push Notifications | Medium | APNs integration | +| Analytics | Low | Event tracking system | + +## Related Documentation / Tài Liệu Liên Quan + +- [README.md](./README.md) - Quick start guide +- [Swift Enterprise Architect Skill](../../.agent/skills/swift-enterprise-architect/SKILL.md) +- [Swift Security Skill](../../.agent/skills/swift-security/SKILL.md) +- [Swift Networking Skill](../../.agent/skills/swift-networking/SKILL.md) diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate index 4d5c84cf119f04476848452c068287d2b5cc7414..b83ef307bf469d349b13acc693cb8df742ca7783 100644 GIT binary patch delta 23552 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 H4KMx!5hf)B delta 20343 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: String) async throws -> T { try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil) } + + // MARK: - OAuth2 Methods + // Các phương thức OAuth2 + + /// POST form-urlencoded request (for OAuth2 token endpoint) + /// Request POST dạng form-urlencoded (cho OAuth2 token endpoint) + /// - Parameters: + /// - endpoint: Token endpoint path / Đường dẫn token endpoint + /// - formData: Form data dictionary / Dictionary form data + /// - Returns: Decoded response / Response đã giải mã + func postForm(endpoint: String, formData: [String: String]) async throws -> T { + guard let url = URL(string: APIConfig.baseURL + endpoint) else { + throw APIError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.timeoutInterval = APIConfig.timeout + + // Encode form data + // Mã hóa form data + let formBody = formData.map { key, value in + let escapedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + return "\(key)=\(escapedValue)" + }.joined(separator: "&") + request.httpBody = formBody.data(using: .utf8) + + // Perform request + // Thực hiện request + let (data, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.unknown + } + + // Handle OAuth2 error responses + // Xử lý OAuth2 error responses + if httpResponse.statusCode != 200 { + if let errorResponse = try? decoder.decode(OAuthErrorResponse.self, from: data) { + throw APIError.serverError( + statusCode: httpResponse.statusCode, + message: errorResponse.errorDescription ?? errorResponse.error + ) + } + let message = String(data: data, encoding: .utf8) + throw APIError.serverError(statusCode: httpResponse.statusCode, message: message) + } + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw APIError.decodingError(error) + } + } + try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil) + } } diff --git a/apps/app-client-base-swift/README.md b/apps/app-client-base-swift/README.md index 4c5146f2..1277757f 100644 --- a/apps/app-client-base-swift/README.md +++ b/apps/app-client-base-swift/README.md @@ -1,164 +1,290 @@ -# App Client Base Swift +# App Client Base Swift / Ứng Dụng Client iOS -Ứng dụng iOS native cho nền tảng GoodGo, xây dựng bằng Swift và SwiftUI. +> **EN**: Native iOS client application for GoodGo platform, built with Swift and SwiftUI following MVVM architecture. +> **VI**: Ứng dụng iOS native cho nền tảng GoodGo, xây dựng bằng Swift và SwiftUI theo kiến trúc MVVM. -## 📱 Tổng quan +## 📱 Features / Tính Năng -App client iOS native sử dụng kiến trúc MVVM với SwiftUI, tương tự như `app-client-base-net` (MAUI) nhưng dành riêng cho nền tảng Apple. +| Feature / Tính năng | Description / Mô tả | +|---------------------|---------------------| +| 🔐 Authentication | Login, Register, Forgot Password với form validation | +| 🏠 Home Dashboard | Greeting động, Featured items, Activity feed | +| 🔍 Explore | Khám phá địa điểm và dịch vụ | +| 👤 Profile | Quản lý thông tin cá nhân và cài đặt | +| 🌓 Dark Mode | Hỗ trợ chế độ tối tự động | +| 🌐 i18n | Đa ngôn ngữ (Tiếng Việt & English) | -## 🛠️ Công nghệ +## 🛠️ Tech Stack / Công Nghệ -| Công nghệ | Phiên bản | Mục đích | -|-----------|-----------|----------| -| Swift | 5.9+ | Ngôn ngữ lập trình | -| SwiftUI | iOS 15+ | UI Framework | -| Xcode | 15+ | IDE | -| URLSession | Native | Networking | -| Keychain | Native | Secure Storage | +| Technology | Version | Purpose / Mục đích | +|------------|---------|-------------------| +| Swift | 5.9+ | Primary language / Ngôn ngữ chính | +| SwiftUI | iOS 15+ | Declarative UI framework | +| Xcode | 15.0+ | IDE development | +| URLSession | Native | HTTP networking | +| Keychain | Native | Secure token storage / Lưu trữ token bảo mật | +| Combine | Native | Reactive programming | -## 📂 Cấu trúc thư mục +## � Prerequisites / Yêu Cầu + +- **macOS**: 14.0+ (Sonoma) +- **Xcode**: 15.0+ +- **iOS Target**: 15.0+ +- **Apple Developer Account**: Required for device deployment / Cần thiết cho deploy lên thiết bị + +## 🚀 Quick Start / Bắt Đầu Nhanh + +### 1. Clone và mở project +```bash +cd apps/app-client-base-swift +open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +``` + +### 2. Chọn Simulator +- Xcode menu: **Product > Destination > iPhone 15 Pro** (hoặc simulator khác) + +### 3. Build và Run +```bash +# Sử dụng shortcut +⌘R (Command + R) + +# Hoặc từ terminal +xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ + build +``` + +### 4. Mock Login (để test) +``` +Email: admin@goodgo.com +Password: 123456 +``` + +## 📂 Project Structure / Cấu Trúc Project ``` AppClientBaseSwift/ ├── App/ -│ └── AppClientBaseSwiftApp.swift # @main entry point +│ └── AppClientBaseSwiftApp.swift # @main entry point +│ ├── Core/ │ ├── Constants/ -│ │ └── Constants.swift # App-wide constants +│ │ └── Constants.swift # API, App, Storage, DesignSystem │ └── Extensions/ -│ ├── View+Extensions.swift # SwiftUI View helpers -│ └── String+Extensions.swift # String utilities +│ ├── View+Extensions.swift # SwiftUI modifiers +│ └── String+Extensions.swift # Validation, formatting +│ ├── Models/ -│ └── User.swift # User data model -├── ViewModels/ -│ └── HomeViewModel.swift # MVVM ViewModel +│ └── User.swift # User entity + extensions +│ +├── ViewModels/ # MVVM ViewModels +│ ├── AuthViewModel.swift # Login/Register/ForgotPassword +│ ├── HomeViewModel.swift # Home screen logic +│ └── ProfileViewModel.swift # Profile management +│ ├── Views/ -│ └── Screens/ -│ ├── ContentView.swift # Main tab container -│ ├── WelcomeView.swift # Onboarding screen -│ ├── HomeView.swift # Home tab -│ ├── ExploreView.swift # Explore tab -│ └── ProfileView.swift # Profile tab +│ ├── Auth/ # Authentication screens +│ │ ├── AuthContainerView.swift # Auth navigation container +│ │ ├── LoginView.swift # Login UI +│ │ ├── RegisterView.swift # Registration UI +│ │ └── ForgotPasswordView.swift # Password reset UI +│ │ +│ ├── Home/ # Home components +│ │ ├── WalletCard.swift # Wallet balance card +│ │ ├── PromoCarousel.swift # Promotions carousel +│ │ ├── ServiceGrid.swift # Services grid +│ │ └── ActivityFeed.swift # Recent activities +│ │ +│ └── Screens/ # Main screens +│ ├── ContentView.swift # Root container + TabBar +│ ├── SplashView.swift # Splash animation +│ ├── WelcomeView.swift # Onboarding +│ ├── HomeView.swift # Home tab +│ ├── ExploreView.swift # Explore tab +│ └── ProfileView.swift # Profile tab +│ ├── Services/ -│ ├── APIService.swift # HTTP client -│ └── AuthManager.swift # Authentication -├── Resources/ -│ ├── Assets.xcassets/ # Images & Colors -│ ├── en.lproj/ # English strings -│ └── vi.lproj/ # Vietnamese strings -└── Info.plist # App configuration +│ ├── APIService.swift # HTTP client với URLSession +│ └── AuthManager.swift # Auth state + Keychain +│ +└── Resources/ + ├── Assets.xcassets/ # Images & Colors + ├── en.lproj/ # English localization + └── vi.lproj/ # Vietnamese localization ``` -## 🚀 Bắt đầu - -### Yêu cầu - -- macOS 14.0+ (Sonoma) -- Xcode 15.0+ -- iOS 15.0+ target - -### Mở project - -1. Mở Xcode -2. Chọn **File > Open** hoặc nhấn `⌘O` -3. Chọn thư mục `app-client-base-swift` -4. Build và chạy trên Simulator (`⌘R`) - -### Tạo Xcode Project (Nếu chưa có) - -Do project này chỉ chứa source files, bạn cần tạo Xcode project: - -```bash -# Mở Xcode và tạo project mới -# 1. File > New > Project -# 2. Chọn "App" template -# 3. Product Name: AppClientBaseSwift -# 4. Team: Chọn team của bạn -# 5. Organization Identifier: vn.goodgo -# 6. Interface: SwiftUI -# 7. Language: Swift -# 8. Lưu vào thư mục app-client-base-swift -# 9. Xóa các file generated và thay thế bằng files trong thư mục này -``` - -## 🎨 Kiến trúc +## 🎨 Architecture / Kiến Trúc ### MVVM Pattern ``` ┌─────────────────────────────────────────────────────────────┐ -│ VIEW (SwiftUI) │ -│ ContentView, HomeView, ExploreView, ProfileView │ +│ VIEW (SwiftUI) │ +│ HomeView, ProfileView, AuthContainerView, LoginView... │ ├─────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────┐ │ -│ │ @StateObject │ │ -│ │ @EnvironmentObject │ │ -│ └──────────────────────────────────┘ │ +│ @StateObject / @EnvironmentObject │ │ │ │ ├────────────────────────────▼────────────────────────────────┤ -│ VIEWMODEL │ -│ HomeViewModel (ObservableObject) │ -│ - @Published properties │ -│ - async/await methods │ +│ VIEWMODEL (ObservableObject) │ +│ HomeViewModel, AuthViewModel, ProfileViewModel │ +│ • @Published properties for reactive UI │ +│ • async/await methods for data loading │ +│ • Business logic and validation │ ├─────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────┐ │ -│ │ Protocol / Dependency │ │ -│ │ Injection │ │ -│ └──────────────────────────────────┘ │ +│ Protocol-based Dependency Injection │ │ │ │ ├────────────────────────────▼────────────────────────────────┤ -│ SERVICES │ -│ APIService, AuthManager │ +│ SERVICES │ +│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ └─────────────────────────────────────────────────────────────┘ ``` -### Key Features +### Authentication Flow -- **SwiftUI**: UI declarative hiện đại -- **Async/Await**: Xử lý bất đồng bộ native -- **Keychain**: Lưu trữ token bảo mật -- **Localization**: Hỗ trợ đa ngôn ngữ (VI/EN) -- **Dark Mode**: Hỗ trợ giao diện tối +```mermaid +stateDiagram-v2 + [*] --> SplashScreen + SplashScreen --> CheckAuth: App Launch + CheckAuth --> Authenticated: Token Valid + CheckAuth --> Unauthenticated: No Token + Unauthenticated --> Login + Login --> Authenticated: Success + Login --> Register: Sign Up + Register --> Authenticated: Success + Authenticated --> HomeScreen + HomeScreen --> Unauthenticated: Logout +``` -## 📋 Conventions +### Data Flow -### Coding Style +``` +User Action → View → ViewModel.method() → Service.request() → API + ↓ + @Published update + ↓ + View rerender +``` +## 📋 Coding Conventions / Quy Ước Code + +### File Structure +```swift +// MARK: - Imports +import SwiftUI + +// MARK: - Type Definition +/// Description in English +/// Mô tả bằng tiếng Việt +struct/class/enum TypeName { + + // MARK: - Properties + + // MARK: - Init + + // MARK: - Public Methods + + // MARK: - Private Methods +} + +// MARK: - Extensions + +// MARK: - Preview Provider (DEBUG only) +``` + +### ViewModel Pattern ```swift -// ViewModel với ObservableObject @MainActor -class HomeViewModel: ObservableObject { - @Published var isLoading: Bool = false +final class FeatureViewModel: ObservableObject { + // Published properties for UI binding + @Published var isLoading = false + @Published var errorMessage: String? + @Published var data: [Model] = [] + // Dependencies via init + private let apiService: APIServiceProtocol + + init(apiService: APIServiceProtocol = APIService.shared) { + self.apiService = apiService + } + + // Async methods func loadData() async { - // Async data loading - } -} - -// View với MVVM binding -struct HomeView: View { - @StateObject private var viewModel = HomeViewModel() - - var body: some View { - // SwiftUI body + isLoading = true + defer { isLoading = false } + + do { + data = try await apiService.get(endpoint: "/data") + } catch { + errorMessage = error.localizedDescription + } } } ``` -### Comments (Bilingual) - +### Bilingual Comments ```swift -/// Load home screen data -/// Tải dữ liệu màn hình home -func loadData() async { } +/// Load user profile data +/// Tải dữ liệu hồ sơ người dùng +func loadProfile() async { } ``` -## 🔗 Liên kết +## ⚙️ Configuration / Cấu Hình -- [app-client-base-net](../app-client-base-net) - .NET MAUI client -- [web-client](../web-client) - Web client +### API Configuration +```swift +// Core/Constants/Constants.swift +enum APIConfig { + static let baseURL = "https://api.goodgo.vn" + static let apiVersion = "/api/v1" + static let timeout: TimeInterval = 30.0 +} +``` + +### Environment Variables +| Key | Description / Mô tả | Default | +|-----|---------------------|---------| +| `API_BASE_URL` | Backend API URL | `https://api.goodgo.vn` | +| `API_VERSION` | API version prefix | `/api/v1` | + +## 🧪 Testing / Kiểm Thử + +### Run Unit Tests +```bash +xcodebuild test \ + -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +### Test Plan +Located at: `AppClientBaseSwift.xctestplan` + +## 🔐 Security / Bảo Mật + +| Feature | Implementation / Triển khai | +|---------|----------------------------| +| Token Storage | Keychain Services (not UserDefaults) | +| Secure Requests | HTTPS only, Bearer token auth | +| Session Management | Auto token refresh, secure logout | +| Data Protection | Sensitive data encrypted at rest | + +## 📱 Supported Devices / Thiết Bị Hỗ Trợ + +- **iPhone**: 8 and later (iOS 15+) +- **iPad**: All iPads with iOS 15+ +- **Orientations**: Portrait (primary), Landscape (supported) + +## 🔗 Related Projects / Dự Án Liên Quan + +- [app-client-base-net](../app-client-base-net) - .NET MAUI cross-platform client +- [iam-service-net](../../services/iam-service-net) - Authentication backend +- [web-client](../web-client) - Web application + +## 📚 Additional Documentation / Tài Liệu Bổ Sung + +- [ARCHITECTURE.md](./ARCHITECTURE.md) - Chi tiết kiến trúc và design decisions +- [Swift Enterprise Skills](../../.agent/skills/swift-enterprise-architect/SKILL.md) - Swift development guidelines ## 📄 License