Skip to content

Commit 5bf87c9

Browse files
committed
Keep strong reference to temporary delegate directly on location manager
This removes the strong reference cycle, which should make this a bit more robust
1 parent e962781 commit 5bf87c9

File tree

1 file changed

+105
-31
lines changed

1 file changed

+105
-31
lines changed
Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,72 @@
11
import CoreLocation
22

33
extension CLLocationManager {
4-
/// Requests the user’s permission to use location services while the app is
5-
/// in use.
4+
/// Requests the user’s permission to use location services while the app is in use.
65
///
7-
/// This is a wrapper around
8-
/// `CLLocationManager.requestWhenInUseAuthorization()`, providing an async
9-
/// API.
6+
/// This is a wrapper around `CLLocationManager.requestWhenInUseAuthorization()`, providing an
7+
/// async API.
108
///
11-
/// While the user is making a selection the `delegate` will be set to a
12-
/// custom delegate used for handling the
13-
/// `locationManagerDidChangeAuthorization(_:)` function. When the user has
14-
/// made a selection the `delegate` will be set back to the original value.
9+
/// While the user is making a selection the `delegate` will be set to a custom delegate used
10+
/// for handling the `locationManagerDidChangeAuthorization(_:)` function. This delegate will
11+
/// forward all calls to the current delegate. When the user has made a selection the
12+
/// `delegate` will be set back to the original value.
1513
public func requestWhenInUseAuthorization() async -> CLAuthorizationStatus {
14+
#if os(macOS)
15+
switch authorizationStatus {
16+
case .authorizedAlways, .denied, .restricted:
17+
return authorizationStatus
18+
case .notDetermined:
19+
break
20+
@unknown default:
21+
break
22+
}
23+
#endif
24+
1625
return await withCheckedContinuation { continuation in
17-
let previousDelegate = delegate
18-
let ownDelegate = LocationDelegate { [self] status in
19-
self.delegate = previousDelegate
26+
let ownDelegate = LocationDelegate { status in
2027
continuation.resume(returning: status)
2128
}
22-
delegate = ownDelegate
29+
ownDelegate.attach(to: self)
2330
requestWhenInUseAuthorization()
2431
}
2532
}
2633

27-
/// Requests the user’s permission to use location services regardless of
28-
/// whether the app is in use.
34+
/// Requests the user’s permission to use location services regardless of whether the app is in
35+
/// use.
2936
///
30-
/// This is a wrapper around
31-
/// `CLLocationManager.requestAlwaysAuthorization()`, providing an async
32-
/// API.
37+
/// This is a wrapper around `CLLocationManager.requestAlwaysAuthorization()`, providing an
38+
/// async API.
3339
///
34-
/// While the user is making a selection the `delegate` will be set to a
35-
/// custom delegate used for handling the
36-
/// `locationManagerDidChangeAuthorization(_:)` function. When the user has
37-
/// made a selection the `delegate` will be set back to the original value.
40+
/// While the user is making a selection the `delegate` will be set to a custom delegate used
41+
/// for handling the `locationManagerDidChangeAuthorization(_:)` function. This delegate will
42+
/// forward all calls to the current delegate. When the user has made a selection the
43+
/// `delegate` will be set back to the original value.
3844
public func requestAlwaysAuthorization() async -> CLAuthorizationStatus {
45+
#if os(macOS)
46+
switch authorizationStatus {
47+
case .authorizedAlways, .denied, .restricted:
48+
return authorizationStatus
49+
case .notDetermined:
50+
break
51+
@unknown default:
52+
break
53+
}
54+
#else
55+
switch authorizationStatus {
56+
case .authorizedAlways, .authorizedWhenInUse, .denied, .restricted:
57+
return authorizationStatus
58+
case .notDetermined:
59+
break
60+
@unknown default:
61+
break
62+
}
63+
#endif
64+
3965
return await withCheckedContinuation { continuation in
40-
let previousDelegate = delegate
41-
let ownDelegate = LocationDelegate { [self] status in
42-
self.delegate = previousDelegate
66+
let ownDelegate = LocationDelegate { status in
4367
continuation.resume(returning: status)
4468
}
45-
delegate = ownDelegate
69+
ownDelegate.attach(to: self)
4670
requestAlwaysAuthorization()
4771
}
4872
}
@@ -51,20 +75,70 @@ extension CLLocationManager {
5175
private final class LocationDelegate: NSObject, CLLocationManagerDelegate {
5276
private let authorizationDidChangeHandler: (_ status: CLAuthorizationStatus) -> Void
5377

54-
/// A reference to `self`, used to prevent the delegate for being
55-
/// deallocated while the user is making a selection.
56-
private var strongSelf: LocationDelegate?
78+
/// The original delegate to forward calls to while this delegate is installed.
79+
private weak var originalDelegate: CLLocationManagerDelegate?
5780

5881
init(authorizationDidChangeHandler: @escaping (_ status: CLAuthorizationStatus) -> Void) {
5982
self.authorizationDidChangeHandler = authorizationDidChangeHandler
6083

6184
super.init()
85+
}
6286

63-
strongSelf = self
87+
func attach(to locationManager: CLLocationManager) {
88+
originalDelegate = locationManager.delegate
89+
locationManager.delegate = self
90+
// Keep a strong reference via the location manager to enable it to be deinitialised.
91+
locationManager._gatheredKitLocationDelegate = self
6492
}
6593

94+
// MARK: - Authorization changes
95+
6696
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
6797
authorizationDidChangeHandler(manager.authorizationStatus)
68-
strongSelf = nil
98+
// Forward to original delegate if it implements this method
99+
originalDelegate?.locationManagerDidChangeAuthorization?(manager)
100+
manager.delegate = originalDelegate
101+
// This will release `self`.
102+
manager._gatheredKitLocationDelegate = nil
103+
}
104+
105+
// MARK: - Forwarding other delegate methods
106+
107+
override func responds(to aSelector: Selector!) -> Bool {
108+
if originalDelegate?.responds(to: aSelector) == true {
109+
return true
110+
}
111+
return super.responds(to: aSelector)
112+
}
113+
114+
override func forwardingTarget(for aSelector: Selector!) -> Any? {
115+
if originalDelegate?.responds(to: aSelector) == true {
116+
return originalDelegate
117+
}
118+
return super.forwardingTarget(for: aSelector)
119+
}
120+
}
121+
122+
extension CLLocationManager {
123+
@inline(never) private static var __associated_gatheredKitLocationDelegateKey: UnsafeRawPointer {
124+
let closure: @convention(c) () -> Void = {}
125+
return unsafeBitCast(closure, to: UnsafeRawPointer.self)
126+
}
127+
128+
fileprivate var _gatheredKitLocationDelegate: LocationDelegate? {
129+
get {
130+
objc_getAssociatedObject(
131+
self,
132+
Self.__associated_gatheredKitLocationDelegateKey
133+
) as? LocationDelegate
134+
}
135+
set {
136+
objc_setAssociatedObject(
137+
self,
138+
Self.__associated_gatheredKitLocationDelegateKey,
139+
newValue,
140+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
141+
)
142+
}
69143
}
70144
}

0 commit comments

Comments
 (0)