Skip to content

Commit 2bc96d0

Browse files
committed
Add a retain cycle sample when using UICollectionView + Diffable Data Source
1 parent 558dd7b commit 2bc96d0

File tree

12 files changed

+407
-10
lines changed

12 files changed

+407
-10
lines changed

LeakDetectorDemo.xcodeproj/project.pbxproj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
7F18D5912565F4E70045F6FC /* ChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18D5902565F4E70045F6FC /* ChildViewController.swift */; };
1212
7F18D5952565F9730045F6FC /* TimerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18D5942565F9730045F6FC /* TimerViewController.swift */; };
1313
7F18D5992565FA730045F6FC /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F18D5982565FA730045F6FC /* MainViewController.swift */; };
14+
7F28E4D22912202300A1B905 /* UICollectionViewRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F28E4D12912202300A1B905 /* UICollectionViewRootViewController.swift */; };
15+
7F28E4D4291221AC00A1B905 /* UICollectionViewControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F28E4D3291221AC00A1B905 /* UICollectionViewControllers.swift */; };
16+
7F28E4D62912615D00A1B905 /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F28E4D52912615D00A1B905 /* UIViewController+Alert.swift */; };
17+
7F28E4D82912655700A1B905 /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F28E4D72912655700A1B905 /* XCUIElement+Extensions.swift */; };
1418
7F4A212A291218660063D1FC /* NoLeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4A2129291218660063D1FC /* NoLeakTests.swift */; };
1519
7F4F1C40255EE0C700A90DA8 /* CombineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4F1C3F255EE0C700A90DA8 /* CombineViewController.swift */; };
1620
7F4F1C502561726C00A90DA8 /* LeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4F1C4F2561726C00A90DA8 /* LeakTests.swift */; };
@@ -81,6 +85,10 @@
8185
7F18D5902565F4E70045F6FC /* ChildViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChildViewController.swift; sourceTree = "<group>"; };
8286
7F18D5942565F9730045F6FC /* TimerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerViewController.swift; sourceTree = "<group>"; };
8387
7F18D5982565FA730045F6FC /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
88+
7F28E4D12912202300A1B905 /* UICollectionViewRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewRootViewController.swift; sourceTree = "<group>"; };
89+
7F28E4D3291221AC00A1B905 /* UICollectionViewControllers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewControllers.swift; sourceTree = "<group>"; };
90+
7F28E4D52912615D00A1B905 /* UIViewController+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Alert.swift"; sourceTree = "<group>"; };
91+
7F28E4D72912655700A1B905 /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = "<group>"; };
8492
7F4A2129291218660063D1FC /* NoLeakTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoLeakTests.swift; sourceTree = "<group>"; };
8593
7F4F1C3F255EE0C700A90DA8 /* CombineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineViewController.swift; sourceTree = "<group>"; };
8694
7F4F1C4D2561726C00A90DA8 /* LeakDetectorDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeakDetectorDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -165,13 +173,24 @@
165173
7F1131772562D8C500438910 /* LeakDetectableViewController.swift */,
166174
7F18D5902565F4E70045F6FC /* ChildViewController.swift */,
167175
D7A55BB226DF2F4100F0DA35 /* LeakDetectableRxSwiftTableViewController.swift */,
176+
7F28E4D52912615D00A1B905 /* UIViewController+Alert.swift */,
168177
);
169178
path = Utils;
170179
sourceTree = "<group>";
171180
};
181+
7F28E4D029121FD000A1B905 /* UICollectionView */ = {
182+
isa = PBXGroup;
183+
children = (
184+
7F28E4D12912202300A1B905 /* UICollectionViewRootViewController.swift */,
185+
7F28E4D3291221AC00A1B905 /* UICollectionViewControllers.swift */,
186+
);
187+
path = UICollectionView;
188+
sourceTree = "<group>";
189+
};
172190
7F4F1C3A255EE07800A90DA8 /* Leak */ = {
173191
isa = PBXGroup;
174192
children = (
193+
7F28E4D029121FD000A1B905 /* UICollectionView */,
175194
D7A55BAF26DF2EE500F0DA35 /* LeakDetectorRxSwift */,
176195
D62DE94D2588B2C200CF5CE1 /* Coordinator */,
177196
7FB179E8257375C000209C04 /* SimpleCases */,
@@ -205,6 +224,7 @@
205224
7F4A2129291218660063D1FC /* NoLeakTests.swift */,
206225
7F4F1C4F2561726C00A90DA8 /* LeakTests.swift */,
207226
7F4F1C512561726C00A90DA8 /* Info.plist */,
227+
7F28E4D72912655700A1B905 /* XCUIElement+Extensions.swift */,
208228
);
209229
path = LeakDetectorDemoUITests;
210230
sourceTree = "<group>";
@@ -495,6 +515,7 @@
495515
isa = PBXSourcesBuildPhase;
496516
buildActionMask = 2147483647;
497517
files = (
518+
7F28E4D82912655700A1B905 /* XCUIElement+Extensions.swift in Sources */,
498519
7F4F1C502561726C00A90DA8 /* LeakTests.swift in Sources */,
499520
7F4A212A291218660063D1FC /* NoLeakTests.swift in Sources */,
500521
);
@@ -517,6 +538,7 @@
517538
7F18D5952565F9730045F6FC /* TimerViewController.swift in Sources */,
518539
7FE0480D25681B29005BE7C7 /* NestedClosuresViewController.swift in Sources */,
519540
D6FEB02C255EBDE1009B2235 /* AppDelegate.swift in Sources */,
541+
7F28E4D4291221AC00A1B905 /* UICollectionViewControllers.swift in Sources */,
520542
7FB179D62572970800209C04 /* LazyVarRootViewController.swift in Sources */,
521543
7FB179F02573768B00209C04 /* SimpleCasesViewController.swift in Sources */,
522544
7F4F1C40255EE0C700A90DA8 /* CombineViewController.swift in Sources */,
@@ -542,11 +564,13 @@
542564
7F1131782562D8C500438910 /* LeakDetectableViewController.swift in Sources */,
543565
D62DE9532588B2C200CF5CE1 /* CoordinatorRootViewController.swift in Sources */,
544566
7FF1190925673784000B6C59 /* NoLeakNonEscapingClosureViewController.swift in Sources */,
567+
7F28E4D62912615D00A1B905 /* UIViewController+Alert.swift in Sources */,
545568
7FBA06F2256B22E2000A42C4 /* FutureViewController.swift in Sources */,
546569
7FE047E82567F6E0005BE7C7 /* AnimateRootViewController.swift in Sources */,
547570
D7A55BB526DF320900F0DA35 /* LeakDetectorRxSwiftChildViewControllers.swift in Sources */,
548571
7FE047D525673F34005BE7C7 /* DispatchWorkItemViewController.swift in Sources */,
549572
D6FEB02E255EBDE1009B2235 /* SceneDelegate.swift in Sources */,
573+
7F28E4D22912202300A1B905 /* UICollectionViewRootViewController.swift in Sources */,
550574
7F7353BF290531CA00958751 /* LeakDetectorRxSwiftMultiVCRootViewController.swift in Sources */,
551575
7FE047DF25674C4A005BE7C7 /* NoLeakHighOrderFunctionViewController.swift in Sources */,
552576
);

LeakDetectorDemo/Base.lproj/Main.storyboard

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,35 @@
295295
</subviews>
296296
</tableViewCellContentView>
297297
</tableViewCell>
298+
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Sbd-Tq-hLm" style="IBUITableViewCellStyleDefault" id="Ue5-4Z-BfR">
299+
<rect key="frame" x="0.0" y="740.5" width="414" height="43.5"/>
300+
<autoresizingMask key="autoresizingMask"/>
301+
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Ue5-4Z-BfR" id="sFE-Jw-IeU">
302+
<rect key="frame" x="0.0" y="0.0" width="385.5" height="43.5"/>
303+
<autoresizingMask key="autoresizingMask"/>
304+
<subviews>
305+
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Leak by UICollectionView" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Sbd-Tq-hLm">
306+
<rect key="frame" x="20" y="0.0" width="357.5" height="43.5"/>
307+
<autoresizingMask key="autoresizingMask"/>
308+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
309+
<nil key="textColor"/>
310+
<nil key="highlightedColor"/>
311+
</label>
312+
</subviews>
313+
</tableViewCellContentView>
314+
</tableViewCell>
298315
</cells>
299316
</tableViewSection>
300317
<tableViewSection headerTitle="No Leak" id="w2m-HG-JUB" userLabel="No Leak">
301318
<cells>
302319
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="mfo-rT-f1S" style="IBUITableViewCellStyleDefault" id="SrQ-Fr-ZSC">
303-
<rect key="frame" x="0.0" y="807.5" width="414" height="43.5"/>
320+
<rect key="frame" x="0.0" y="851" width="414" height="43.5"/>
304321
<autoresizingMask key="autoresizingMask"/>
305322
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="SrQ-Fr-ZSC" id="8x2-zc-fwW">
306323
<rect key="frame" x="0.0" y="0.0" width="385.5" height="43.5"/>
307324
<autoresizingMask key="autoresizingMask"/>
308325
<subviews>
309-
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="No Leak by high order functions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="mfo-rT-f1S">
326+
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" text="No Leak by high order functions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="mfo-rT-f1S">
310327
<rect key="frame" x="20" y="0.0" width="357.5" height="43.5"/>
311328
<autoresizingMask key="autoresizingMask"/>
312329
<fontDescription key="fontDescription" type="system" pointSize="17"/>

LeakDetectorDemo/Leak/Delegate/DelegateRootViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ final class DelegateRootViewController: LeakDetectableTableViewController {
113113
}
114114

115115
default:
116-
fatalError("invalid section")
116+
break
117117
}
118118
}
119119
}

LeakDetectorDemo/Leak/LazyVar/LazyVarViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class LeakyHTMLElement {
3232

3333
}
3434

35-
class LazyVarViewController1: LeakDetectableViewController {
35+
final class LazyVarViewController1: LeakDetectableViewController {
3636

3737
private var heading: LeakyHTMLElement? = LeakyHTMLElement(name: "h1", text: "Hello")
3838

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// UICollectionViewControllers.swift
3+
// LeakDetectorDemo
4+
//
5+
// Created by An Tran on 2/11/22.
6+
//
7+
8+
import Foundation
9+
import UIKit
10+
11+
@available(iOS 14.0, *)
12+
class BaseUICollectionViewController: UIViewController {
13+
14+
private static let cellReuseID = "product-cell"
15+
16+
private lazy var collectionView = makeCollectionView()
17+
private lazy var dataSource = makeDataSource()
18+
19+
enum Section: Int, CaseIterable {
20+
case main
21+
}
22+
23+
typealias Cell = UICollectionViewCell
24+
typealias CellRegistration = UICollectionView.CellRegistration<Cell, Product>
25+
26+
func makeCellRegistration() -> CellRegistration {
27+
CellRegistration { cell, indexPath, product in
28+
// cell.textLabel.text = product.name
29+
}
30+
}
31+
32+
override func viewDidLoad() {
33+
super.viewDidLoad()
34+
35+
view.backgroundColor = .systemBackground
36+
37+
collectionView.register(
38+
UICollectionViewCell.self,
39+
forCellWithReuseIdentifier: Self.cellReuseID
40+
)
41+
collectionView.dataSource = dataSource
42+
}
43+
44+
func makeCollectionView() -> UICollectionView {
45+
UICollectionView(
46+
frame: .zero,
47+
collectionViewLayout: makeCollectionViewLayout()
48+
)
49+
}
50+
51+
func makeCollectionViewLayout() -> UICollectionViewLayout {
52+
fatalError("needs implementation")
53+
}
54+
55+
func makeListLayoutSection() -> NSCollectionLayoutSection {
56+
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
57+
widthDimension: .fractionalWidth(1),
58+
heightDimension: .fractionalHeight(1)
59+
))
60+
61+
let group = NSCollectionLayoutGroup.vertical(
62+
layoutSize: NSCollectionLayoutSize(
63+
widthDimension: .fractionalWidth(1),
64+
heightDimension: .absolute(50)
65+
),
66+
subitems: [item]
67+
)
68+
69+
return NSCollectionLayoutSection(group: group)
70+
}
71+
72+
73+
func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Product> {
74+
let cellRegistration = makeCellRegistration()
75+
76+
return UICollectionViewDiffableDataSource(
77+
collectionView: collectionView,
78+
cellProvider: { collectionView, indexPath, product in
79+
collectionView.dequeueConfiguredReusableCell(
80+
using: cellRegistration,
81+
for: indexPath,
82+
item: product
83+
)
84+
}
85+
)
86+
}
87+
88+
func productListDidLoad(_ list: ProductList) {
89+
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
90+
snapshot.appendSections(Section.allCases)
91+
92+
snapshot.appendItems(list.products, toSection: .main)
93+
94+
dataSource.apply(snapshot)
95+
}
96+
}
97+
98+
99+
@available(iOS 14.0, *)
100+
final class LeakUICollectionViewController: BaseUICollectionViewController {
101+
102+
override func makeCollectionViewLayout() -> UICollectionViewLayout {
103+
// self is captured strongly here, which causes a retain cycle
104+
// self -> collectionView -> collectionViewLayout -> self
105+
UICollectionViewCompositionalLayout { sectionIndex, _ in
106+
switch Section(rawValue: sectionIndex) {
107+
case .main:
108+
return self.makeListLayoutSection()
109+
case nil:
110+
return nil
111+
}
112+
}
113+
}
114+
}
115+
116+
@available(iOS 14.0, *)
117+
final class NoLeakUICollectionViewController: BaseUICollectionViewController {
118+
override func makeCollectionViewLayout() -> UICollectionViewLayout {
119+
// Capture self weakly here to avoid retain cycle
120+
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
121+
switch Section(rawValue: sectionIndex) {
122+
case .main:
123+
return self?.makeListLayoutSection()
124+
case nil:
125+
return nil
126+
}
127+
}
128+
}
129+
}
130+
131+
struct Product: Hashable {
132+
let name: String
133+
}
134+
135+
struct ProductList: Hashable {
136+
let products: [Product]
137+
}

0 commit comments

Comments
 (0)