Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:22 +08:00
commit 2c8fd6d9a0
22 changed files with 8353 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
---
name: ios-code-review
description: Comprehensive iOS Swift code review for Payoo Merchant app. Checks RxSwift patterns, Clean Architecture, naming conventions, memory management, security, and performance. Use when reviewing Swift files, pull requests, or ViewModels, ViewControllers, UseCases, and Repositories.
allowed-tools: Read, Grep, Glob
---
# iOS Code Review
Expert iOS code reviewer for Payoo Merchant application, specializing in Swift, RxSwift reactive programming, and Clean Architecture patterns.
## When to Activate
- "review code", "check this file", "review PR"
- Mentions Swift files: ViewController, ViewModel, UseCase, Repository
- "code quality", "best practices", "check standards"
- RxSwift, Clean Architecture, MVVM patterns
## Review Process
### Step 1: Identify Scope
Determine what to review:
- Specific files (e.g., "PaymentViewModel.swift")
- Directories (e.g., "Payment module")
- Git changes (recent commits, PR diff)
- Entire module or feature
### Step 2: Read and Analyze
Use Read tool to examine files, checking against 6 core categories.
### Step 3: Apply Standards
#### 1. Naming Conventions ✅
- **Types**: PascalCase, descriptive (e.g., `PaymentViewModel`)
- **Variables**: camelCase (e.g., `paymentAmount`, `isLoading`)
- **Booleans**: Prefix with `is`, `has`, `should`, `can`
- **No abbreviations** except URL, ID, VC, UC
- **IBOutlets**: Include type suffix (e.g., `amountTextField`)
#### 2. RxSwift Patterns 🔄
- **Disposal**: Every `.subscribe()` has `.disposed(by: disposeBag)`
- **Memory**: Use `[weak self]` in closures
- **Schedulers**: `subscribeOn(background)` for work, `observeOn(main)` for UI
- **Errors**: All chains handle errors
- **Relays**: Use `BehaviorRelay` not `BehaviorSubject`
#### 3. Clean Architecture 🏗️
- **Flow**: ViewModel → UseCase → Repository → API/DB
- **ViewModels**: Extend `BaseViewModel<State>`, no business logic
- **UseCases**: Contain all business logic
- **DI**: Dependencies injected via constructor (Swinject)
#### 4. Security 🔒
- Payment data in Keychain, never UserDefaults
- No sensitive data in logs
- HTTPS with certificate pinning
- Input validation on amounts
#### 5. UI/UX 🎨
- Simple titles: Use `title` property
- Complex titles: Use `navigationItem.titleView` only when subtitle exists
- Accessibility labels and hints
- Loading states with feedback
#### 6. Performance ⚡
- Database ops on background threads
- No retain cycles
- Image caching
- Proper memory management
### Step 4: Generate Report
Provide structured output with:
- **Summary**: Issue counts by severity (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
- **Issues by category**: Organized findings
- **Code examples**: Current vs. fixed code
- **Explanations**: Why it matters
- **Recommendations**: Prioritized actions
## Severity Levels
🔴 **Critical** - Fix immediately
- Missing `.disposed(by: disposeBag)` → Memory leak
- Strong `self` references → Retain cycle
- Payment data in UserDefaults → Security risk
- UI updates off main thread → Crash risk
🟠 **High Priority** - Fix soon
- No error handling in Observable chains
- Wrong scheduler usage
- ViewModel calling API directly
- Business logic in ViewModel
🟡 **Medium Priority** - Should improve
- Using deprecated `BehaviorSubject`
- Poor naming (abbreviations)
- Missing accessibility labels
🟢 **Low Priority** - Nice to have
- Inconsistent style
- Could be more descriptive
## Output Format
```markdown
# iOS Code Review Report
## Summary
- 🔴 Critical: X | 🟠 High: X | 🟡 Medium: X | 🟢 Low: X
- By category: Naming: X, RxSwift: X, Architecture: X, etc.
## Critical Issues
### 🔴 [Category] - [Issue Title]
**File**: `path/to/file.swift:line`
**Current**:
```swift
// problematic code
```
**Fix**:
```swift
// corrected code
```
**Why**: [Explanation of impact]
---
## Recommendations
1. Fix all critical issues immediately
2. Address high priority before next release
3. Plan medium priority for next sprint
## Positive Observations
✅ [Acknowledge well-written code]
```
## Quick Reference
**Standards**: `.github/instructions/ios-merchant-code-review.instructions.md`
- Lines 36-393: Naming Conventions
- Lines 410-613: RxSwift Patterns
- Lines 615-787: Architecture
- Lines 789-898: Security
- Lines 1181-1288: Testing
- Lines 1363-1428: Performance
**Detailed Examples**: See `examples.md` in this skill directory for extensive code examples and patterns.
## Tips
- **Be thorough**: Check all 6 categories
- **Be specific**: Reference exact line numbers
- **Be constructive**: Explain why, not just what
- **Be practical**: Prioritize by severity
- **Be encouraging**: Acknowledge good code

View File

@@ -0,0 +1,637 @@
# iOS Code Review Examples
Detailed examples for each review category with good/bad patterns.
## 1. Naming Conventions Examples
### ✅ Good Naming
```swift
// Classes and Types
class PaymentViewController: UIViewController { }
class RefundRequestViewModel: BaseViewModel<RefundRequestState> { }
protocol PaymentUseCase { }
// Variables and Properties
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
let transactions = BehaviorRelay<[Transaction]>(value: [])
// Functions
func loadTransactions() { }
func processPaymentRequest(amount: Double) { }
func validatePaymentAmount(_ amount: String) -> Bool { }
// IBOutlets
@IBOutlet weak var paymentAmountTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton!
@IBOutlet weak var transactionTableView: UITableView!
```
### ❌ Bad Naming
```swift
// Classes - Too abbreviated or generic
class PayVC: UIViewController { } // What is "Pay"?
class RefReqVM { } // Too abbreviated
class Manager { } // Too generic
// Variables - Unclear or abbreviated
let amt = BehaviorRelay<String>(value: "") // What is "amt"?
let flag = BehaviorRelay<Bool>(value: false) // Meaningless
let data = BehaviorRelay<[Any]>(value: []) // Too generic
// Functions - Vague
func doSomething() { } // What does it do?
func process() { } // Process what?
func handle() { } // Handle what?
// IBOutlets - Missing type suffix
@IBOutlet weak var amount: UITextField! // Should be amountTextField
@IBOutlet weak var btn: UIButton! // Should be confirmButton
```
---
## 2. RxSwift Pattern Examples
### ✅ Proper RxSwift Usage
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase
private let disposeBag = DisposeBag()
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessing = BehaviorRelay<Bool>(value: false)
init(paymentUC: PaymentUseCase) {
self.paymentUC = paymentUC
super.init()
}
func processPayment() {
guard !paymentAmount.value.isEmpty else {
setState(.showError(PaymentError.invalidAmount))
return
}
isProcessing.accept(true)
paymentUC.execute(amount: paymentAmount.value)
.subscribeOn(ConcurrentScheduler.background)
.observeOn(MainScheduler.instance)
.subscribe(
onNext: { [weak self] result in
self?.handleSuccess(result)
},
onError: { [weak self] error in
self?.handleError(error)
},
onCompleted: { [weak self] in
self?.isProcessing.accept(false)
}
)
.disposed(by: disposeBag) // Proper disposal
}
}
```
### ❌ Common RxSwift Mistakes
```swift
class PaymentViewModel: BaseViewModel<PaymentState> {
// Missing DisposeBag property
func processPayment() {
// MEMORY LEAK: No disposal
paymentUC.execute(amount: paymentAmount.value)
.subscribe(onNext: { result in
// RETAIN CYCLE: Strong self reference
self.handleSuccess(result)
})
// MISSING: .disposed(by: disposeBag)
}
func loadData() {
// Wrong scheduler for UI updates
networkService.fetchData()
.subscribeOn(MainScheduler.instance) // Wrong!
.subscribe(onNext: { data in
self.tableView.reloadData() // May be on background thread
})
.disposed(by: disposeBag)
}
func refreshData() {
// No error handling
dataSource.getData()
.subscribe(onNext: { data in
// Handle data
})
// MISSING: onError handler
.disposed(by: disposeBag)
}
}
```
### DisposeBag Anti-Patterns
```swift
// BAD: Local DisposeBag
func loadData() {
let disposeBag = DisposeBag() // Local variable!
api.fetchData()
.subscribe(onNext: { data in
// Handle data
})
.disposed(by: disposeBag)
// DisposeBag deallocates here, cancels subscription immediately!
}
// BAD: Multiple DisposeBags
class ViewModel {
private var searchDisposeBag = DisposeBag() // Anti-pattern
private var dataDisposeBag = DisposeBag() // Anti-pattern
}
// GOOD: Single DisposeBag property
class ViewModel {
private let disposeBag = DisposeBag() // Correct!
}
```
---
## 3. Clean Architecture Examples
### ✅ Proper Layer Separation
```swift
// PRESENTATION LAYER - ViewModel
class PaymentViewModel: BaseViewModel<PaymentState> {
private let paymentUC: PaymentUseCase // Uses UseCase
init(paymentUC: PaymentUseCase) {
self.paymentUC = paymentUC
super.init()
}
func processPayment(amount: String) {
paymentUC.execute(amount: amount) // Delegates to UseCase
.subscribe(
onNext: { [weak self] result in
self?.setState(.success(result))
},
onError: { [weak self] error in
self?.setState(.error(error))
}
)
.disposed(by: disposeBag)
}
}
// DOMAIN LAYER - UseCase
protocol PaymentUseCase {
func execute(amount: String) -> Single<PaymentResult>
}
class PaymentUseCaseImpl: PaymentUseCase {
private let paymentRepository: PaymentRepository
private let validationService: ValidationService
init(paymentRepository: PaymentRepository,
validationService: ValidationService) {
self.paymentRepository = paymentRepository
self.validationService = validationService
}
func execute(amount: String) -> Single<PaymentResult> {
// Business logic in UseCase
return validationService.validateAmount(amount)
.flatMap { validAmount in
return self.paymentRepository.processPayment(amount: validAmount)
}
}
}
// DATA LAYER - Repository
protocol PaymentRepository {
func processPayment(amount: Double) -> Single<PaymentResult>
}
class PaymentRepositoryImpl: PaymentRepository {
private let apiService: PaymentApiService
private let localStorage: PaymentLocalStorage
func processPayment(amount: Double) -> Single<PaymentResult> {
return apiService.processPayment(amount: amount)
.do(onSuccess: { [weak self] result in
self?.localStorage.savePaymentRecord(result)
})
}
}
```
### ❌ Layer Violations
```swift
// BAD: ViewModel bypassing UseCase
class PaymentViewModel: BaseViewModel<PaymentState> {
private let apiService: PaymentApiService // Wrong layer!
func processPayment() {
// Direct API call, no business logic
apiService.processPayment(amount: amount)
.subscribe(onNext: { result in
// Direct storage access
RealmManager.shared.save(result)
})
.disposed(by: disposeBag)
}
}
// BAD: Business logic in ViewModel
class PaymentViewModel: BaseViewModel<PaymentState> {
func processPayment(amount: Double) {
// Validation logic in ViewModel
guard amount > 1000 else { return }
guard amount < 50_000_000 else { return }
// Business rules in ViewModel
let fee = amount * 0.01
let total = amount + fee
// This should all be in UseCase!
}
}
// BAD: Direct instantiation
class PaymentViewController: UIViewController {
// Hard-coded dependencies
private let viewModel = PaymentViewModel(
paymentUC: PaymentUseCaseImpl() // Direct instantiation
)
}
```
---
## 4. Security Examples
### ✅ Secure Payment Handling
```swift
class PaymentSecurityManager {
private let keychain = KeychainWrapper.standard
// Store in Keychain
func storePaymentToken(_ token: String, for merchantId: String) {
let key = "payment_token_\(merchantId)"
keychain.set(token, forKey: key,
withAccessibility: .whenUnlockedThisDeviceOnly)
}
func retrievePaymentToken(for merchantId: String) -> String? {
let key = "payment_token_\(merchantId)"
return keychain.string(forKey: key)
}
}
// Mask sensitive data in logs
class PaymentRequest {
let amount: Double
let merchantId: String
override var description: String {
return "PaymentRequest(amount: \(amount), merchantId: ***)"
}
}
// HTTPS with certificate pinning
class PaymentNetworkManager {
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
return URLSession(
configuration: config,
delegate: CertificatePinningDelegate(),
delegateQueue: nil
)
}()
}
```
### ❌ Security Violations
```swift
// BAD: Insecure storage
class PaymentManager {
func storePaymentToken(_ token: String) {
// UserDefaults is not secure!
UserDefaults.standard.set(token, forKey: "payment_token")
}
func processPayment(_ request: PaymentRequest) {
// Logging sensitive data!
print("Processing payment: \(request)")
print("Card number: \(request.cardNumber)")
}
}
// BAD: No certificate pinning
class PaymentNetworkManager {
func processPayment(_ request: PaymentRequest) {
let url = URL(string: "http://api.payoo.vn/payment")! // HTTP!
// No encryption
let data = try! JSONEncoder().encode(request)
// No certificate pinning
URLSession.shared.dataTask(with: url) { _, _, _ in }
}
}
```
---
## 5. UI/UX Examples
### ✅ Proper Navigation Setup
```swift
// Simple title
class PaymentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Payment" // Use title property
}
}
// Title with subtitle (only when subtitle exists)
class StoreSelectionViewController: UIViewController {
private let titleDescription: String?
override func viewDidLoad() {
super.viewDidLoad()
if let description = titleDescription, !description.isEmpty {
// Only use titleView when subtitle exists
navigationItem.titleView = createTitleView(
title: "Store Selection",
description: description
)
} else {
// Use simple title when no subtitle
title = "Store Selection"
}
}
}
// Loading states with feedback
class QRSaleViewController: UIViewController {
private func bindLoadingStates() {
viewModel.isProcessing
.bind(to: loadingIndicator.rx.isAnimating)
.disposed(by: disposeBag)
viewModel.isProcessing
.map { !$0 }
.bind(to: processButton.rx.isEnabled)
.disposed(by: disposeBag)
viewModel.isProcessing
.map { $0 ? "Processing..." : "Process Payment" }
.bind(to: processButton.rx.title(for: .normal))
.disposed(by: disposeBag)
}
}
// Accessibility
class PaymentAmountView: UIView {
private func setupAccessibility() {
amountTextField.isAccessibilityElement = true
amountTextField.accessibilityLabel = "Payment amount"
amountTextField.accessibilityHint = "Enter the payment amount in VND"
// Dynamic Type support
amountTextField.font = UIFont.preferredFont(forTextStyle: .title2)
amountTextField.adjustsFontForContentSizeCategory = true
}
}
```
### ❌ UI/UX Issues
```swift
// BAD: Using titleView for simple title
class PaymentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Unnecessary custom title view
let titleLabel = UILabel()
titleLabel.text = "Payment"
navigationItem.titleView = titleLabel // Should use title property!
}
}
// BAD: No loading feedback
class QRSaleViewController: UIViewController {
private func processPayment() {
// No loading indicator
viewModel.processPayment() // User has no feedback!
}
}
// BAD: No accessibility
class PaymentAmountView: UIView {
// No accessibility setup
// No Dynamic Type support
// Missing accessibility labels
}
```
---
## 6. Performance Examples
### ✅ Proper Memory Management
```swift
class ImageDownloadManager {
private let cache = NSCache<NSString, UIImage>()
private var activeDownloads: [String: Disposable] = [:]
func downloadImage(from url: String) -> Observable<UIImage> {
let cacheKey = url as NSString
// Check cache first
if let cachedImage = cache.object(forKey: cacheKey) {
return .just(cachedImage)
}
// Cancel existing download
activeDownloads[url]?.dispose()
let download = URLSession.shared.rx
.data(request: URLRequest(url: URL(string: url)!))
.compactMap { UIImage(data: $0) }
.do(
onNext: { [weak self] image in
self?.cache.setObject(image, forKey: cacheKey)
},
onDispose: { [weak self] in
self?.activeDownloads.removeValue(forKey: url)
}
)
.share(replay: 1, scope: .whileConnected)
activeDownloads[url] = download.connect()
return download
}
}
// Database on background thread
class TransactionRepository {
func getTransactions() -> Observable<[Transaction]> {
return Observable.collection(from: realm.objects(TransactionObject.self))
.map { results in
return results.map { Transaction(from: $0) }
}
.subscribeOn(ConcurrentScheduler.background) // Background
.observeOn(MainScheduler.instance) // Main for UI
}
}
```
### ❌ Performance Issues
```swift
// BAD: Memory leak from strong references
class ImageDownloadManager {
private var downloads: [URLSessionDataTask] = [] // Strong references
func downloadImage(from url: String) -> Observable<UIImage> {
return Observable.create { observer in
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, _ in
// Process
}
self.downloads.append(task) // Never removed - leak!
task.resume()
return Disposables.create {
task.cancel()
// Still in downloads array!
}
}
}
}
// BAD: Blocking main thread
class TransactionRepository {
func getTransactions() -> Observable<[Transaction]> {
return Observable.create { observer in
// Blocking operation on main thread!
let realm = try! Realm()
let results = realm.objects(TransactionObject.self)
let transactions = results.map { Transaction(from: $0) }
observer.onNext(Array(transactions))
observer.onCompleted()
return Disposables.create()
}
}
}
```
---
## Common Review Scenarios
### Scenario 1: New Feature Review
**Code**: New payment processing feature
**Check**:
1. Naming: All classes/variables descriptive?
2. RxSwift: Disposal and memory management?
3. Architecture: Proper layer separation?
4. Security: Payment data handled securely?
5. Tests: Unit tests included?
6. Performance: No blocking operations?
### Scenario 2: Bug Fix Review
**Code**: Fix for crash in transaction list
**Check**:
1. Root cause addressed?
2. No force unwrapping?
3. Proper error handling added?
4. Tests for the bug scenario?
5. No new issues introduced?
### Scenario 3: Refactoring Review
**Code**: Refactor ViewModel to use Clean Architecture
**Check**:
1. UseCase layer added?
2. Business logic moved from ViewModel?
3. DI setup correctly?
4. Tests still pass?
5. No breaking changes?
---
**Detailed Examples**: See `examples.md` for extensive code samples and scenarios.
## Quick Reference Checklist
Copy this for quick reviews:
```markdown
## Review Checklist
### Naming ✅
- [ ] Classes: PascalCase, descriptive
- [ ] Variables: camelCase, meaningful
- [ ] Booleans: is/has/should/can prefix
- [ ] No abbreviations
- [ ] IBOutlets: type suffix
### RxSwift 🔄
- [ ] All subscriptions disposed
- [ ] [weak self] in closures
- [ ] Correct schedulers
- [ ] Error handling present
- [ ] Using BehaviorRelay
### Architecture 🏗️
- [ ] ViewModel → UseCase → Repository
- [ ] No business logic in ViewModel
- [ ] Dependencies injected
- [ ] BaseViewModel extended
- [ ] Repository pattern used
### Security 🔒
- [ ] Payment data in Keychain
- [ ] No sensitive logs
- [ ] HTTPS with pinning
- [ ] Input validation
### UI/UX 🎨
- [ ] title for simple titles
- [ ] titleView only with subtitle
- [ ] Accessibility configured
- [ ] Loading states shown
### Performance ⚡
- [ ] DB on background thread
- [ ] No retain cycles
- [ ] Image caching
- [ ] Memory management proper
```