Initial commit
This commit is contained in:
158
skills/rxswift-memory-check/SKILL.md
Normal file
158
skills/rxswift-memory-check/SKILL.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: rxswift-memory-check
|
||||
description: Quick RxSwift memory leak detection for iOS. Finds missing dispose bags, retain cycles, and strong self references. Use when debugging memory issues, checking Observable subscriptions, or investigating retain cycles in RxSwift code.
|
||||
allowed-tools: Read, Grep, Glob
|
||||
---
|
||||
|
||||
# RxSwift Memory Leak Detector
|
||||
|
||||
Fast, focused check for RxSwift memory management issues in iOS code.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- "memory leak", "retain cycle", "dispose bag"
|
||||
- "RxSwift memory issues", "check subscriptions"
|
||||
- "[weak self]", "memory management"
|
||||
- Debugging memory problems or crashes
|
||||
|
||||
## Quick Check Process
|
||||
|
||||
### Step 1: Find RxSwift Subscriptions
|
||||
|
||||
Use Grep to locate all Observable subscriptions:
|
||||
- Pattern: `\.subscribe\(`
|
||||
- Check surrounding code for proper disposal
|
||||
|
||||
### Step 2: Verify Each Subscription
|
||||
|
||||
For every `.subscribe(`:
|
||||
1. ✅ Has `.disposed(by: disposeBag)`
|
||||
2. ✅ Uses `[weak self]` or `[unowned self]` in closures
|
||||
3. ✅ DisposeBag is a property, not local variable
|
||||
|
||||
### Step 3: Check Common Patterns
|
||||
|
||||
#### 🔴 Pattern 1: Missing Disposal
|
||||
```swift
|
||||
viewModel.data
|
||||
.subscribe(onNext: { data in })
|
||||
// MISSING: .disposed(by: disposeBag)
|
||||
```
|
||||
|
||||
#### 🔴 Pattern 2: Retain Cycle
|
||||
```swift
|
||||
viewModel.data
|
||||
.subscribe(onNext: { data in
|
||||
self.updateUI(data) // Strong self!
|
||||
})
|
||||
```
|
||||
|
||||
#### 🔴 Pattern 3: Local DisposeBag
|
||||
```swift
|
||||
func loadData() {
|
||||
let disposeBag = DisposeBag() // Local variable!
|
||||
// Cancels immediately when function ends
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Generate Report
|
||||
|
||||
Focused report with:
|
||||
- Critical issues by severity
|
||||
- File locations and line numbers
|
||||
- Current vs. fixed code
|
||||
- Impact assessment
|
||||
- Recommended fixes
|
||||
|
||||
## Search Patterns
|
||||
|
||||
### Find subscriptions without disposal
|
||||
```
|
||||
Pattern: \.subscribe\(
|
||||
Context: Check next 5 lines for .disposed
|
||||
```
|
||||
|
||||
### Find strong self references
|
||||
```
|
||||
Pattern: subscribe.*\{[^[]*self\.
|
||||
Context: -A 3 -B 1
|
||||
```
|
||||
|
||||
### Find local DisposeBag declarations
|
||||
```
|
||||
Pattern: let disposeBag = DisposeBag\(\)
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# RxSwift Memory Check Report
|
||||
|
||||
## Critical Issues: X
|
||||
|
||||
### 1. Missing Disposal - MEMORY LEAK
|
||||
**File**: `PaymentViewModel.swift:45`
|
||||
**Risk**: Memory accumulation, eventual crash
|
||||
|
||||
**Current**:
|
||||
```swift
|
||||
// Missing disposal
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```swift
|
||||
.disposed(by: disposeBag)
|
||||
```
|
||||
|
||||
**Impact**: [Explanation]
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
🔴 Critical: X (memory leaks/retain cycles)
|
||||
⚠️ Warnings: X (could use weak self)
|
||||
|
||||
## Files Status
|
||||
✅ Clean files
|
||||
⚠️ Files with warnings
|
||||
🔴 Files with critical issues
|
||||
```
|
||||
|
||||
## DisposeBag Best Practices
|
||||
|
||||
✅ **Correct**: Property-level DisposeBag
|
||||
```swift
|
||||
class ViewModel {
|
||||
private let disposeBag = DisposeBag()
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Wrong**: Local DisposeBag
|
||||
```swift
|
||||
func loadData() {
|
||||
let disposeBag = DisposeBag() // Cancels immediately!
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use `weak` vs `unowned`
|
||||
|
||||
- **Default**: Always use `[weak self]` (safer)
|
||||
- **Rare**: Use `[unowned self]` only if 100% sure self outlives subscription
|
||||
|
||||
## Quick Fix Guide
|
||||
|
||||
1. **Add Missing Disposal**: `.disposed(by: disposeBag)`
|
||||
2. **Add Weak Self**: `[weak self]` in closure
|
||||
3. **Move DisposeBag**: To property level
|
||||
|
||||
## Testing the Fix
|
||||
|
||||
Suggest verification:
|
||||
1. Run Instruments with Leaks template
|
||||
2. Navigate to/from screens multiple times
|
||||
3. Check Debug Memory Graph for cycles
|
||||
4. Verify view controllers deallocate
|
||||
|
||||
## Reference
|
||||
|
||||
**Detailed Examples**: See `examples.md` for extensive code samples and scenarios.
|
||||
536
skills/rxswift-memory-check/examples.md
Normal file
536
skills/rxswift-memory-check/examples.md
Normal file
@@ -0,0 +1,536 @@
|
||||
# RxSwift Memory Check Examples
|
||||
|
||||
Detailed examples of memory leaks, retain cycles, and proper RxSwift memory management.
|
||||
|
||||
## Critical Issue Examples
|
||||
|
||||
### Example 1: Missing Disposal (Memory Leak)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
private let paymentUC: PaymentUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func processPayment() {
|
||||
paymentUC.execute(amount: paymentAmount.value)
|
||||
.subscribe(onNext: { [weak self] result in
|
||||
self?.handleResult(result)
|
||||
})
|
||||
// ❌ MISSING: .disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Subscription never releases, accumulates in memory
|
||||
**Impact**: Memory grows over time, eventually crashes app
|
||||
**Symptoms**: Increasing memory usage, app slowdown
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
private let paymentUC: PaymentUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func processPayment() {
|
||||
paymentUC.execute(amount: paymentAmount.value)
|
||||
.subscribe(onNext: { [weak self] result in
|
||||
self?.handleResult(result)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Added
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Add `.disposed(by: disposeBag)` to every subscription
|
||||
**Result**: Subscription properly cleaned up when ViewModel deallocates
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Retain Cycle (Strong Self Reference)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class StoresViewModel: BaseViewModel<StoresState> {
|
||||
private let storesUC: StoresUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
let stores = BehaviorRelay<[Store]>(value: [])
|
||||
|
||||
func loadStores() {
|
||||
storesUC.getStores()
|
||||
.subscribe(onNext: { stores in
|
||||
// ❌ Strong self reference - retain cycle!
|
||||
self.stores.accept(stores)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Closure captures self strongly, creating retain cycle
|
||||
**Impact**: ViewModel never deallocates, memory leak
|
||||
**Symptoms**: View controllers don't deallocate, memory grows
|
||||
**Detection**: Xcode Debug Memory Graph shows cycle
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class StoresViewModel: BaseViewModel<StoresState> {
|
||||
private let storesUC: StoresUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
let stores = BehaviorRelay<[Store]>(value: [])
|
||||
|
||||
func loadStores() {
|
||||
storesUC.getStores()
|
||||
.subscribe(onNext: { [weak self] stores in
|
||||
// ✅ Weak self - no retain cycle
|
||||
self?.stores.accept(stores)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use `[weak self]` in closure capture list
|
||||
**Result**: ViewModel can deallocate properly, no memory leak
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Local DisposeBag (Early Cancellation)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||
func loadTransactions() {
|
||||
let disposeBag = DisposeBag() // ❌ Local variable!
|
||||
|
||||
transactionUC.getTransactions()
|
||||
.subscribe(onNext: { [weak self] transactions in
|
||||
self?.handleTransactions(transactions)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// ❌ disposeBag deallocates here, cancels subscription immediately!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: DisposeBag deallocates when function exits, canceling subscription
|
||||
**Impact**: Observable never completes, callbacks never fire
|
||||
**Symptoms**: Data doesn't load, UI doesn't update
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||
private let disposeBag = DisposeBag() // ✅ Property
|
||||
|
||||
func loadTransactions() {
|
||||
transactionUC.getTransactions()
|
||||
.subscribe(onNext: { [weak self] transactions in
|
||||
self?.handleTransactions(transactions)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Uses property
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Make DisposeBag a class property, not local variable
|
||||
**Result**: Subscription lives as long as the ViewModel
|
||||
|
||||
---
|
||||
|
||||
### Example 4: Multiple DisposeBags (Anti-Pattern)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class DashboardViewModel: BaseViewModel<DashboardState> {
|
||||
private var searchDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||
private var dataDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||
private var uiDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||
|
||||
func search(query: String) {
|
||||
searchService.search(query)
|
||||
.subscribe(onNext: { results in })
|
||||
.disposed(by: searchDisposeBag)
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
dataService.loadData()
|
||||
.subscribe(onNext: { data in })
|
||||
.disposed(by: dataDisposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Unnecessary complexity, harder to manage subscriptions
|
||||
**Impact**: Confusing code, potential for errors
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class DashboardViewModel: BaseViewModel<DashboardState> {
|
||||
private let disposeBag = DisposeBag() // ✅ Single DisposeBag
|
||||
|
||||
func search(query: String) {
|
||||
searchService.search(query)
|
||||
.subscribe(onNext: { results in })
|
||||
.disposed(by: disposeBag) // ✅ Same bag
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
dataService.loadData()
|
||||
.subscribe(onNext: { data in })
|
||||
.disposed(by: disposeBag) // ✅ Same bag
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use single DisposeBag per class
|
||||
**Result**: Simpler code, all subscriptions disposed together
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 5: Proper RxSwift Memory Management
|
||||
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
// Dependencies
|
||||
private let paymentUC: PaymentUseCase
|
||||
private let validationService: ValidationService
|
||||
|
||||
// DisposeBag property (not local!)
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
// State
|
||||
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||
let isProcessing = BehaviorRelay<Bool>(value: false)
|
||||
|
||||
init(paymentUC: PaymentUseCase, validationService: ValidationService) {
|
||||
self.paymentUC = paymentUC
|
||||
self.validationService = validationService
|
||||
super.init()
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
private func setupBindings() {
|
||||
// ✅ Proper disposal with weak self
|
||||
paymentAmount
|
||||
.map { [weak self] amount in
|
||||
self?.validationService.validateAmount(amount) ?? false
|
||||
}
|
||||
.subscribe(onNext: { [weak self] isValid in
|
||||
self?.setState(isValid ? .valid : .invalid)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
func processPayment() {
|
||||
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) // ✅ Always disposed
|
||||
}
|
||||
|
||||
private func handleSuccess(_ result: PaymentResult) {
|
||||
setState(.success(result))
|
||||
}
|
||||
|
||||
private func handleError(_ error: Error) {
|
||||
setState(.error(error))
|
||||
isProcessing.accept(false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ DisposeBag is a property
|
||||
- ✅ Every subscription uses `.disposed(by:)`
|
||||
- ✅ Every closure uses `[weak self]`
|
||||
- ✅ Proper error handling
|
||||
- ✅ Correct scheduler usage
|
||||
|
||||
---
|
||||
|
||||
### Example 6: ViewController with Proper Bindings
|
||||
|
||||
```swift
|
||||
class PaymentViewController: UIViewController {
|
||||
@IBOutlet weak var amountTextField: UITextField!
|
||||
@IBOutlet weak var confirmButton: UIButton!
|
||||
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
|
||||
|
||||
private let viewModel: PaymentViewModel
|
||||
private let disposeBag = DisposeBag() // ✅ Property
|
||||
|
||||
init(viewModel: PaymentViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Use dependency injection")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
bindViewModel()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
title = "Payment"
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
// Input: TextField → ViewModel
|
||||
amountTextField.rx.text
|
||||
.orEmpty
|
||||
.bind(to: viewModel.paymentAmount)
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
// Input: Button tap → ViewModel action
|
||||
confirmButton.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.viewModel.processPayment()
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
// Output: ViewModel → UI
|
||||
viewModel.isProcessing
|
||||
.bind(to: loadingIndicator.rx.isAnimating)
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
viewModel.isProcessing
|
||||
.map { !$0 }
|
||||
.bind(to: confirmButton.rx.isEnabled)
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
// State handling
|
||||
viewModel.getState()
|
||||
.compactMap { $0 }
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] state in
|
||||
self?.handleState(state.name)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
}
|
||||
|
||||
private func handleState(_ state: PaymentState) {
|
||||
switch state {
|
||||
case .success(let result):
|
||||
showSuccessAlert(result)
|
||||
case .error(let error):
|
||||
showErrorAlert(error)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Single DisposeBag for all bindings
|
||||
- ✅ All UI bindings properly disposed
|
||||
- ✅ Weak self in subscribe closures
|
||||
- ✅ Clean separation of concerns
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: Timer/Interval Observable
|
||||
|
||||
#### ❌ Problem
|
||||
```swift
|
||||
func startTimer() {
|
||||
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] tick in
|
||||
self?.updateTime(tick)
|
||||
})
|
||||
// ❌ No disposal - timer runs forever!
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Solution
|
||||
```swift
|
||||
class TimerViewModel {
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func startTimer() {
|
||||
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] tick in
|
||||
self?.updateTime(tick)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Stops when ViewModel deallocates
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Network Requests
|
||||
|
||||
#### ❌ Problem
|
||||
```swift
|
||||
func loadData() {
|
||||
networkService.fetchData()
|
||||
.subscribe(onNext: { data in
|
||||
self.data = data // ❌ Strong self
|
||||
})
|
||||
// ❌ No disposal
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Solution
|
||||
```swift
|
||||
class DataViewModel {
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func loadData() {
|
||||
networkService.fetchData()
|
||||
.subscribe(
|
||||
onNext: { [weak self] data in
|
||||
self?.data = data
|
||||
},
|
||||
onError: { [weak self] error in
|
||||
self?.handleError(error)
|
||||
}
|
||||
)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Chained Observables
|
||||
|
||||
#### ❌ Problem
|
||||
```swift
|
||||
func processData() {
|
||||
fetchData()
|
||||
.flatMap { data in
|
||||
return self.transform(data) // ❌ Strong self
|
||||
}
|
||||
.flatMap { transformed in
|
||||
return self.save(transformed) // ❌ Strong self
|
||||
}
|
||||
.subscribe(onNext: { result in
|
||||
self.handleResult(result) // ❌ Strong self
|
||||
})
|
||||
// ❌ No disposal
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Solution
|
||||
```swift
|
||||
class DataProcessor {
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func processData() {
|
||||
fetchData()
|
||||
.flatMap { [weak self] data -> Observable<TransformedData> in
|
||||
guard let self = self else { return .empty() }
|
||||
return self.transform(data)
|
||||
}
|
||||
.flatMap { [weak self] transformed -> Observable<Result> in
|
||||
guard let self = self else { return .empty() }
|
||||
return self.save(transformed)
|
||||
}
|
||||
.subscribe(
|
||||
onNext: { [weak self] result in
|
||||
self?.handleResult(result)
|
||||
},
|
||||
onError: { [weak self] error in
|
||||
self?.handleError(error)
|
||||
}
|
||||
)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Detection Tools
|
||||
|
||||
### Using Xcode Debug Memory Graph
|
||||
|
||||
1. Run app in Simulator/Device
|
||||
2. Navigate to screen with suspected leak
|
||||
3. Pop back
|
||||
4. Xcode → Debug → View Memory Graph
|
||||
5. Look for objects that should have deallocated
|
||||
6. Check retain cycle graph
|
||||
|
||||
### Using Instruments
|
||||
|
||||
1. Product → Profile
|
||||
2. Choose "Leaks" template
|
||||
3. Record while using app
|
||||
4. Navigate between screens
|
||||
5. Check for red leak indicators
|
||||
6. Inspect stack traces
|
||||
|
||||
### Manual Verification
|
||||
|
||||
Add `deinit` to ViewModels and ViewControllers:
|
||||
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
deinit {
|
||||
print("✅ PaymentViewModel deallocated") // Should print when leaving screen
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentViewController: UIViewController {
|
||||
deinit {
|
||||
print("✅ PaymentViewController deallocated") // Should print when popping
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `deinit` doesn't print, you have a memory leak!
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Memory Management Checklist
|
||||
|
||||
```markdown
|
||||
## RxSwift Memory Check
|
||||
|
||||
For each Observable subscription:
|
||||
- [ ] Has `.disposed(by: disposeBag)`
|
||||
- [ ] Uses `[weak self]` in closures
|
||||
- [ ] DisposeBag is a property (not local)
|
||||
- [ ] No strong reference cycles
|
||||
- [ ] Error handling present
|
||||
- [ ] deinit prints when tested
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
| Pattern | Issue | Fix |
|
||||
|---------|-------|-----|
|
||||
| Missing disposal | Memory leak | Add `.disposed(by: disposeBag)` |
|
||||
| Strong self | Retain cycle | Use `[weak self]` |
|
||||
| Local DisposeBag | Early cancel | Make it a property |
|
||||
| Multiple bags | Complexity | Use single DisposeBag |
|
||||
| No error handling | Crashes | Add `onError` handler |
|
||||
Reference in New Issue
Block a user