Files
gh-daispacy-py-claude-marke…/skills/ios-code-review/examples.md
2025-11-29 18:17:22 +08:00

17 KiB

iOS Code Review Examples

Detailed examples for each review category with good/bad patterns.

1. Naming Conventions Examples

Good Naming

// 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

// 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

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

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

// ❌ 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

// 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

// ❌ 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

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

// ❌ 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

// ✓ 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

// ❌ 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

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

// ❌ 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:

## 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