Initial commit
This commit is contained in:
637
skills/ios-code-review/examples.md
Normal file
637
skills/ios-code-review/examples.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user