Skip to content

Commit c2c4398

Browse files
authoredOct 18, 2022
Fix unnecessary retains to prevent zombie Analytics instances (#164)
* Fixed memory leaks * Added leak testing * Removed unnecessary subscriber conformance from Timeline. * Some cleanup * Analytics property on plugins now needs to be marked `weak`. * Fixed leak test for linux.
1 parent 2b85609 commit c2c4398

31 files changed

+171
-48
lines changed
 

‎Examples/destination_plugins/AdjustDestination.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class AdjustDestination: NSObject, DestinationPlugin, RemoteNotifications {
4141
let timeline = Timeline()
4242
let type = PluginType.destination
4343
let key = "Adjust"
44-
var analytics: Analytics? = nil
44+
weak var analytics: Analytics? = nil
4545

4646
private var settings: AdjustSettings? = nil
4747

‎Examples/destination_plugins/ComscoreDestination.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ComscoreDestination: DestinationPlugin {
4444
let timeline = Timeline()
4545
let type = PluginType.destination
4646
let key = "comScore"
47-
var analytics: Analytics? = nil
47+
weak var analytics: Analytics? = nil
4848

4949
private var comscoreSettings: ComscoreSettings?
5050
private var comscoreEnrichment: ComscoreEnrichment?

‎Examples/destination_plugins/ExampleDestination.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class ExampleDestination: DestinationPlugin {
4646
public let type = PluginType.destination
4747
// TODO: Fill this out with your settings key that matches your destination in the Segment App
4848
public let key = "Example"
49-
public var analytics: Analytics? = nil
49+
public weak var analytics: Analytics? = nil
5050

5151
private var exampleSettings: ExampleSettings?
5252

‎Examples/destination_plugins/FlurryDestination.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class FlurryDestination: DestinationPlugin {
4646
let timeline = Timeline()
4747
let type = PluginType.destination
4848
let key = "Flurry"
49-
var analytics: Analytics? = nil
49+
weak var analytics: Analytics? = nil
5050

5151
var screenTracksEvents = false
5252

‎Examples/destination_plugins/IntercomDestination.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class IntercomDestination: DestinationPlugin {
4343
let timeline = Timeline()
4444
let type = PluginType.destination
4545
let key = "Intercom"
46-
var analytics: Analytics? = nil
46+
weak var analytics: Analytics? = nil
4747

4848
private var intercomSettings: IntercomSettings?
4949
private var configurationLabels = [String: Any]()

‎Examples/other_plugins/CellularCarrier.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import CoreTelephony
5454
class CellularCarrier: Plugin {
5555
var type: PluginType = .enrichment
5656

57-
var analytics: Analytics?
57+
weak var analytics: Analytics?
5858

5959
func execute<T: RawEvent>(event: T?) -> T? {
6060
guard var workingEvent = event else { return event }

‎Examples/other_plugins/ConsentTracking.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import UIKit
4646
*/
4747
class ConsentTracking: Plugin {
4848
let type = PluginType.before
49-
var analytics: Analytics? = nil
49+
weak var analytics: Analytics? = nil
5050

5151
var queuedEvents = [RawEvent]()
5252

‎Examples/other_plugins/ConsoleLogger.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import Segment
4242
class ConsoleLogger: Plugin {
4343
let type = PluginType.after
4444
let name: String
45-
var analytics: Analytics? = nil
45+
weak var analytics: Analytics? = nil
4646

4747
var identifier: String? = nil
4848

‎Examples/other_plugins/IDFACollection.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import AppTrackingTransparency
4646
*/
4747
class IDFACollection: Plugin {
4848
let type = PluginType.enrichment
49-
var analytics: Analytics? = nil
49+
weak var analytics: Analytics? = nil
5050
@Atomic private var alreadyAsked = false
5151

5252
func execute<T: RawEvent>(event: T?) -> T? {

‎Examples/other_plugins/NotificationTracking.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import Segment
4040

4141
class NotificationTracking: Plugin {
4242
var type: PluginType = .utility
43-
var analytics: Analytics?
43+
weak var analytics: Analytics?
4444

4545
func trackNotification(_ properties: [String: Any], fromLaunch launch: Bool) {
4646
if launch {

‎Examples/other_plugins/UIKitScreenTracking.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class UIKitScreenTracking: UtilityPlugin {
5151
static let controllerKey = "controller"
5252

5353
let type = PluginType.utility
54-
var analytics: Analytics? = nil
54+
weak var analytics: Analytics? = nil
5555

5656
init() {
5757
setupUIKitHooks()

‎Sources/Segment/Plugins/Context.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public class Context: PlatformPlugin {
1111
public let type: PluginType = .before
12-
public var analytics: Analytics?
12+
public weak var analytics: Analytics?
1313

1414
internal var staticContext = staticContextData()
1515
internal static var device = VendorSystem.current

‎Sources/Segment/Plugins/DestinationMetadataPlugin.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
*/
1414
public class DestinationMetadataPlugin: Plugin {
1515
public let type: PluginType = PluginType.enrichment
16-
public var analytics: Analytics?
16+
public weak var analytics: Analytics?
1717
private var analyticsSettings: Settings? = nil
1818

1919
public func update(settings: Settings, type: UpdateType) {

‎Sources/Segment/Plugins/DeviceToken.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public class DeviceToken: PlatformPlugin {
1111
public let type = PluginType.before
12-
public var analytics: Analytics?
12+
public weak var analytics: Analytics?
1313

1414
public var token: String? = nil
1515

‎Sources/Segment/Plugins/Logger/SegmentLog.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111

1212
internal class SegmentLog: UtilityPlugin {
1313
public var filterKind = LogFilterKind.debug
14-
var analytics: Analytics?
14+
weak var analytics: Analytics?
1515

1616
let type = PluginType.utility
1717

@@ -22,7 +22,7 @@ internal class SegmentLog: UtilityPlugin {
2222

2323
// For internal use only. Note: This will contain the last created instance
2424
// of analytics when used in a multi-analytics environment.
25-
internal static var sharedAnalytics: Analytics? = nil
25+
internal static weak var sharedAnalytics: Analytics? = nil
2626

2727
#if DEBUG
2828
internal static var globalLogger: SegmentLog {

‎Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ import Foundation
1010
#if os(Linux)
1111
class LinuxLifecycleMonitor: PlatformPlugin {
1212
let type = PluginType.utility
13-
var analytics: Analytics?
13+
weak var analytics: Analytics?
1414
}
1515
#endif

‎Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle {
1616
static var buildKey = "SEGBuildKeyV2"
1717

1818
let type = PluginType.before
19-
var analytics: Analytics?
19+
weak var analytics: Analytics?
2020

2121
/// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI,
2222
/// this gets around by using a flag in user defaults to check for big events like application updating,

‎Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class macOSLifecycleMonitor: PlatformPlugin {
4646
static var specificName = "Segment_macOSLifecycleMonitor"
4747
let type = PluginType.utility
4848
let name = specificName
49-
var analytics: Analytics?
49+
weak var analytics: Analytics?
5050

5151
private var application: NSApplication
5252
private var appNotifications: [NSNotification.Name] =

‎Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ internal class watchOSVendorSystem: VendorSystem {
172172
}
173173

174174
override var requiredPlugins: [PlatformPlugin] {
175-
return [watchOSLifecycleMonitor()]
175+
return [watchOSLifecycleMonitor(), DeviceToken()]
176176
}
177177

178178
private func deviceModel() -> String {

‎Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle {
1616
static var buildKey = "SEGBuildKeyV2"
1717

1818
let type = PluginType.before
19-
var analytics: Analytics?
19+
weak var analytics: Analytics?
2020

2121
/// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI,
2222
/// this gets around by using a flag in user defaults to check for big events like application updating,

‎Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public extension iOSLifecycle {
3838

3939
class iOSLifecycleMonitor: PlatformPlugin {
4040
let type = PluginType.utility
41-
var analytics: Analytics?
41+
weak var analytics: Analytics?
4242

4343
private var application: UIApplication? = nil
4444
private var appNotifications: [NSNotification.Name] = [UIApplication.didEnterBackgroundNotification,

‎Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class watchOSLifecycleEvents: PlatformPlugin, watchOSLifecycle {
1515
static var buildKey = "SEGBuildKeyV2"
1616

1717
let type = PluginType.before
18-
var analytics: Analytics?
18+
weak var analytics: Analytics?
1919

2020
func applicationDidFinishLaunching(watchExtension: WKExtension) {
2121
if analytics?.configuration.values.trackApplicationLifecycleEvents == false {

‎Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public extension watchOSLifecycle {
2828

2929

3030
class watchOSLifecycleMonitor: PlatformPlugin {
31-
var type = PluginType.utility
32-
var analytics: Analytics?
31+
let type = PluginType.utility
32+
weak var analytics: Analytics?
3333
var wasBackgrounded: Bool = false
3434

3535
private var watchExtension = WKExtension.shared()

‎Sources/Segment/Plugins/SegmentDestination.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class SegmentDestination: DestinationPlugin {
2525
public let type = PluginType.destination
2626
public let key: String = Constants.integrationName.rawValue
2727
public let timeline = Timeline()
28-
public var analytics: Analytics? {
28+
public weak var analytics: Analytics? {
2929
didSet {
3030
initialSetup()
3131
}
@@ -54,8 +54,8 @@ public class SegmentDestination: DestinationPlugin {
5454
guard let analytics = self.analytics else { return }
5555
storage = analytics.storage
5656
httpClient = HTTPClient(analytics: analytics)
57-
flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) {
58-
self.flush()
57+
flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) { [weak self] in
58+
self?.flush()
5959
}
6060
// Add DestinationMetadata enrichment plugin
6161
add(plugin: DestinationMetadataPlugin())

‎Sources/Segment/Plugins/StartupQueue.swift

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ public class StartupQueue: Plugin, Subscriber {
1515

1616
public let type: PluginType = .before
1717

18-
public var analytics: Analytics? = nil {
18+
public weak var analytics: Analytics? = nil {
1919
didSet {
20-
analytics?.store.subscribe(self, handler: runningUpdate)
20+
analytics?.store.subscribe(self) { [weak self] (state: System) in
21+
self?.runningUpdate(state: state)
22+
}
2123
}
2224
}
2325

‎Sources/Segment/Startup.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ extension Analytics {
7676
// do the first one
7777
checkSettings()
7878
// set up return-from-background to do it again.
79-
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { (notification) in
79+
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in
8080
guard let app = notification.object as? UIApplication else { return }
8181
if app.applicationState == .background {
82-
self.checkSettings()
82+
self?.checkSettings()
8383
}
8484
}
8585
}
@@ -100,8 +100,8 @@ extension Analytics {
100100
// now set up a timer to do it every 24 hrs.
101101
// mac apps change focus a lot more than iOS apps, so this
102102
// seems more appropriate here.
103-
QueueTimer.schedule(interval: .days(1), queue: .main) {
104-
self.checkSettings()
103+
QueueTimer.schedule(interval: .days(1), queue: .main) { [weak self] in
104+
self?.checkSettings()
105105
}
106106
}
107107
}

‎Sources/Segment/Timeline.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Sovran
1111

1212
// MARK: - Main Timeline
1313

14-
public class Timeline: Subscriber {
14+
public class Timeline {
1515
internal let plugins: [PluginType: Mediator]
1616

1717
public init() {

‎Sources/Segment/Utilities/HTTPClient.swift

+10-9
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public class HTTPClient {
2424
private var apiHost: String
2525
private var apiKey: String
2626
private var cdnHost: String
27-
private let analytics: Analytics
27+
28+
private weak var analytics: Analytics?
2829

2930
init(analytics: Analytics, apiKey: String? = nil, apiHost: String? = nil, cdnHost: String? = nil) {
3031
self.analytics = analytics
@@ -75,24 +76,24 @@ public class HTTPClient {
7576

7677
let dataTask = session.uploadTask(with: urlRequest, fromFile: batch) { [weak self] (data, response, error) in
7778
if let error = error {
78-
self?.analytics.log(message: "Error uploading request \(error.localizedDescription).")
79+
self?.analytics?.log(message: "Error uploading request \(error.localizedDescription).")
7980
completion(.failure(error))
8081
} else if let httpResponse = response as? HTTPURLResponse {
8182
switch (httpResponse.statusCode) {
8283
case 1..<300:
8384
completion(.success(true))
8485
return
8586
case 300..<400:
86-
self?.analytics.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
87+
self?.analytics?.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
8788
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
8889
case 429:
89-
self?.analytics.log(message: "Server limited client with response code \(httpResponse.statusCode).")
90+
self?.analytics?.log(message: "Server limited client with response code \(httpResponse.statusCode).")
9091
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
9192
case 400..<500:
92-
self?.analytics.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
93+
self?.analytics?.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
9394
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
9495
default: // All 500 codes
95-
self?.analytics.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
96+
self?.analytics?.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
9697
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
9798
}
9899
}
@@ -113,21 +114,21 @@ public class HTTPClient {
113114

114115
let dataTask = session.dataTask(with: urlRequest) { [weak self] (data, response, error) in
115116
if let error = error {
116-
self?.analytics.log(message: "Error fetching settings \(error.localizedDescription).")
117+
self?.analytics?.log(message: "Error fetching settings \(error.localizedDescription).")
117118
completion(false, nil)
118119
return
119120
}
120121

121122
if let httpResponse = response as? HTTPURLResponse {
122123
if httpResponse.statusCode > 300 {
123-
self?.analytics.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
124+
self?.analytics?.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
124125
completion(false, nil)
125126
return
126127
}
127128
}
128129

129130
guard let data = data, let responseJSON = try? JSONDecoder().decode(Settings.self, from: data) else {
130-
self?.analytics.log(message: "Error deserializing settings.")
131+
self?.analytics?.log(message: "Error deserializing settings.")
131132
completion(false, nil)
132133
return
133134
}

‎Sources/Segment/Utilities/Storage.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Foundation
99
import Sovran
1010

1111
internal class Storage: Subscriber {
12-
let store: Store
1312
let writeKey: String
1413
let userDefaults: UserDefaults?
1514
static let MAXFILESIZE = 475000 // Server accepts max 500k per batch
@@ -21,11 +20,14 @@ internal class Storage: Subscriber {
2120
private var fileHandle: FileHandle? = nil
2221

2322
init(store: Store, writeKey: String) {
24-
self.store = store
2523
self.writeKey = writeKey
2624
self.userDefaults = UserDefaults(suiteName: "com.segment.storage.\(writeKey)")
27-
store.subscribe(self, handler: userInfoUpdate)
28-
store.subscribe(self, handler: systemUpdate)
25+
store.subscribe(self) { [weak self] (state: UserInfo) in
26+
self?.userInfoUpdate(state: state)
27+
}
28+
store.subscribe(self) { [weak self] (state: System) in
29+
self?.systemUpdate(state: state)
30+
}
2931
}
3032

3133
func write<T: Codable>(_ key: Storage.Constants, value: T?) {
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//
2+
// MemoryLeak_Tests.swift
3+
//
4+
//
5+
// Created by Brandon Sneed on 10/17/22.
6+
//
7+
8+
import XCTest
9+
@testable import Segment
10+
11+
final class MemoryLeak_Tests: XCTestCase {
12+
13+
override func setUpWithError() throws {
14+
// Put setup code here. This method is called before the invocation of each test method in the class.
15+
}
16+
17+
override func tearDownWithError() throws {
18+
// Put teardown code here. This method is called after the invocation of each test method in the class.
19+
}
20+
21+
func testLeaksVerbose() throws {
22+
let analytics = Analytics(configuration: Configuration(writeKey: "1234"))
23+
24+
waitUntilStarted(analytics: analytics)
25+
analytics.track(name: "test")
26+
27+
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1))
28+
29+
let segmentDest = analytics.find(pluginType: SegmentDestination.self)!
30+
let destMetadata = segmentDest.timeline.find(pluginType: DestinationMetadataPlugin.self)!
31+
let startupQueue = analytics.find(pluginType: StartupQueue.self)!
32+
let segmentLog = analytics.find(pluginType: SegmentLog.self)!
33+
34+
let context = analytics.find(pluginType: Context.self)!
35+
36+
#if !os(Linux)
37+
let deviceToken = analytics.find(pluginType: DeviceToken.self)!
38+
#endif
39+
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
40+
let iosLifecycle = analytics.find(pluginType: iOSLifecycleEvents.self)!
41+
let iosMonitor = analytics.find(pluginType: iOSLifecycleMonitor.self)!
42+
#elseif os(watchOS)
43+
let watchLifecycle = analytics.find(pluginType: watchOSLifecycleEvents.self)!
44+
let watchMonitor = analytics.find(pluginType: watchOSLifecycleMonitor.self)!
45+
#elseif os(macOS)
46+
let macLifecycle = analytics.find(pluginType: macOSLifecycleEvents.self)!
47+
let macMonitor = analytics.find(pluginType: macOSLifecycleMonitor.self)!
48+
#endif
49+
50+
analytics.remove(plugin: startupQueue)
51+
analytics.remove(plugin: segmentLog)
52+
analytics.remove(plugin: segmentDest)
53+
segmentDest.remove(plugin: destMetadata)
54+
55+
analytics.remove(plugin: context)
56+
#if !os(Linux)
57+
analytics.remove(plugin: deviceToken)
58+
#endif
59+
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
60+
analytics.remove(plugin: iosLifecycle)
61+
analytics.remove(plugin: iosMonitor)
62+
#elseif os(watchOS)
63+
analytics.remove(plugin: watchLifecycle)
64+
analytics.remove(plugin: watchMonitor)
65+
#elseif os(macOS)
66+
analytics.remove(plugin: macLifecycle)
67+
analytics.remove(plugin: macMonitor)
68+
#endif
69+
70+
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1))
71+
72+
checkIfLeaked(segmentLog)
73+
checkIfLeaked(segmentDest)
74+
checkIfLeaked(destMetadata)
75+
checkIfLeaked(startupQueue)
76+
77+
checkIfLeaked(context)
78+
#if !os(Linux)
79+
checkIfLeaked(deviceToken)
80+
#endif
81+
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
82+
checkIfLeaked(iosLifecycle)
83+
checkIfLeaked(iosMonitor)
84+
#elseif os(watchOS)
85+
checkIfLeaked(watchLifecycle)
86+
checkIfLeaked(watchMonitor)
87+
#elseif os(macOS)
88+
checkIfLeaked(macLifecycle)
89+
checkIfLeaked(macMonitor)
90+
#endif
91+
92+
checkIfLeaked(analytics)
93+
}
94+
95+
func testLeaksSimple() throws {
96+
let analytics = Analytics(configuration: Configuration(writeKey: "1234"))
97+
98+
waitUntilStarted(analytics: analytics)
99+
analytics.track(name: "test")
100+
101+
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1))
102+
103+
checkIfLeaked(analytics)
104+
}
105+
106+
}

‎Tests/Segment-Tests/Support/TestUtilities.swift

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import XCTest
910
@testable import Segment
1011

1112
extension UUID{
@@ -133,3 +134,14 @@ func waitUntilStarted(analytics: Analytics?) {
133134
}
134135
}
135136
}
137+
138+
extension XCTestCase {
139+
func checkIfLeaked(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
140+
addTeardownBlock { [weak instance] in
141+
if instance != nil {
142+
print("Instance \(String(describing: instance)) is not nil")
143+
}
144+
XCTAssertNil(instance, "Instance should have been deallocated. Potential memory leak!", file: file, line: line)
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)
Please sign in to comment.