我一直在尝试找出一种在主线程上调用 corelocation 的 requestLocation 的方法(这显然是必需的)。
考虑这个 MRE
import CoreLocation
import MapKit
import SwiftUI
struct ContentView: View {
var locationManager = LocationManager()
var body: some View {
Button {
Task {
let location = try await locationManager.currentLocation // works
print(location)
let location2 = try await locationManager.work() // works, no mainactor needed
print(location2)
let location3 = try await APIService.shared.test() // doesnt work
print(location3)
let location4 = try await APIService.shared.test2() // works, mainactor needed
print(location4)
let location5 = try await APIService.shared.test3() // doesnt work even with mainactor
print(location5)
}
} label: {
Text("Get Location")
}.task {
// 1. Check if the app is authorized to access the location services of the device
locationManager.checkAuthorization()
}
}
}
class LocationManager: NSObject, CLLocationManagerDelegate {
// MARK: Object to Access Location Services
private let locationManager = CLLocationManager()
// MARK: Set up the Location Manager Delegate
override init() {
super.init()
locationManager.delegate = self
}
// MARK: Request Authorization to access the User Location
func checkAuthorization() {
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
default:
return
}
}
// MARK: Continuation Object for the User Location
private var continuation: CheckedContinuation<CLLocation, Error>?
// MARK: Async Request the Current Location
var currentLocation: CLLocation {
get async throws {
return try await withCheckedThrowingContinuation { continuation in
// 1. Set up the continuation object
self.continuation = continuation
// 2. Triggers the update of the current location
locationManager.requestLocation()
}
}
}
@MainActor
var currentLocation2: CLLocation {
get async throws {
return try await withCheckedThrowingContinuation { continuation in
// 1. Set up the continuation object
self.continuation = continuation
// 2. Triggers the update of the current location
locationManager.requestLocation()
}
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 4. If there is a location available
if let lastLocation = locations.last {
// 5. Resumes the continuation object with the user location as result
continuation?.resume(returning: lastLocation)
// Resets the continuation object
continuation = nil
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// 6. If not possible to retrieve a location, resumes with an error
continuation?.resume(throwing: error)
// Resets the continuation object
continuation = nil
}
func work() async throws -> CLLocation {
return try await currentLocation
}
}
class APIService {
static let shared = APIService()
// Private initializer to prevent the creation of additional instances
private init() {
}
func test() async throws -> String {
return try await String(describing: LocationManager().currentLocation)
}
@MainActor
func test2() async throws -> String {
return try await String(describing: LocationManager().currentLocation)
}
func test3() async throws -> String {
return try await String(describing: LocationManager().currentLocation2)
}
}
Test1 按预期工作,因为视图中的任务继承为 mainactor
Test2 的工作原理与我假设的相同
Test3 不工作,不知道为什么 test2 工作?我猜如果转到另一个班级,你就会失去演员?
Test4 按预期工作,因为你强制它成为 mainactor
即使你强迫 Test5 再次成为 mainactor,它也不会神秘地工作 那么swift并发中主线程的规则是什么呢?
我正在尝试让 Test5 工作,但解释的其他测试用例将有助于理解如何让 Test5 工作。
我一直在尝试找出一种在主线程上调用 corelocation 的
的方法(这显然是必需的)。requestLocation
不需要在主线程上调用requestLocation
。您可以从任何线程调用它。这里的问题是您在错误的线程上
创建CLLocationManager
。来自文档,
Core Location 使用您初始化在失败的情况下,您将在非隔离异步方法中创建
RunLoop
对象的线程的CLLocationManager
来调用委托对象的方法。该线程本身必须有一个活动的RunLoop
,就像在应用程序主线程中找到的那样。
LocationManager
(进而创建
CLLocationManager
)。这将在协作线程池中的某个线程上运行,该线程池肯定没有
RunLoop
。因此不会调用委托方法。
// locationManager is initialised in the View initialiser, which is run on the main thread
// so these work
let location = try await locationManager.currentLocation
let location2 = try await locationManager.work()
// test2 is isolated to the main actor, so "LocationManager()" is run on the main thread too
let location4 = try await APIService.shared.test2()
// neither test nor test3 are isolated to the main actor, so LocationManager is not created on the main thread
// the fact that 'currentLocation2' is isolated to the main thread doesn't matter
let location3 = try await APIService.shared.test()
let location5 = try await APIService.shared.test3()
我会将
LocationManager
变成
actor
,这样它就是
Sendable
,因此可以安全地从任何地方调用它的属性/方法。我也会将
APIService
作为最后一堂课,这样它也可以成为
Sendable
。这使得
shared
实例安全。请注意,目前如果您在正在进行的延续尚未恢复时获得
currentLocation
,您将覆盖现有的延续,导致永远不会在该覆盖的延续上调用
resume
。这里我修复了代码以删除与并发相关的警告:
actor LocationManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func checkAuthorization() {
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
default:
return
}
}
private var continuation: CheckedContinuation<CLLocation, Error>?
enum LocationError: Error {
case locationInProgress
}
var currentLocation: CLLocation {
get async throws {
return try await withCheckedThrowingContinuation { continuation in
if self.continuation != nil {
continuation.resume(throwing: LocationError.locationInProgress)
} else {
self.continuation = continuation
locationManager.requestLocation()
}
}
}
}
func updateLocation(_ location: CLLocation) {
continuation?.resume(returning: location)
continuation = nil
}
func locationError(_ error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let lastLocation = locations.last {
Task {
await updateLocation(lastLocation)
}
}
}
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
Task {
await locationError(error)
}
}
func work() async throws -> CLLocation {
return try await currentLocation
}
}
final class APIService: Sendable {
static let shared = APIService()
let locationManager = LocationManager()
private init() {
}
func test() async throws -> String {
return try await String(describing: locationManager.currentLocation)
}
}