Я пытался найти способ вызвать requestLocation corelocation в основном потоке (что, по-видимому, необходимо).
Рассмотрим это 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 работает так, как ожидалось, поскольку Task из представления наследуется как главный актер.
Test2 работает по той же причине, по которой я предполагаю
Test3 не работает, не знаю, почему, когда test2 работал? Думаю, если он перейдет в другой класс, ты потеряешь актера?
Test4 работает так, как ожидалось, потому что вы заставляете его быть главным актером.
Test5 загадочным образом не работает, даже если вы снова заставляете его быть главным актером Итак, каковы правила для основного потока в быстром параллелизме?
Я пытаюсь заставить Test5 работать, но другие описанные тестовые примеры помогут понять, как заставить Test5 работать.
Измените APIService на структуру. В SwiftUI обычно мы создаем эти ключи среды, чтобы их можно было переключать с помощью макетов для предварительного просмотра.
Я пытался найти способ вызвать 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()
Что касается того, какой поток вызывает requestLocation
(не имеет отношения к проблеме — просто для вашей информации), основной поток всегда будет вызывать вызов currentLocation2
, поскольку он изолирован от основного актера, а неосновной поток всегда будет вызывать вызов. в currentLocation
, потому что он не изолирован. Вы можете проверить это, нажав MainActor.shared.assertIsolated()
. Если он вылетает, вы не на главном актере.
Обратите внимание, что ваш код содержит множество предупреждений, связанных с параллелизмом. Попробуйте включить полную проверку параллелизма и убедитесь в этом сами.
Я бы сделал 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
}
}
// also consider making this a struct
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)
}
}
Теперь при запуске приложения вам просто нужно получить доступ к APIService.shared
в основном потоке, это приведет к созданию CLLocationManager
в основном потоке, и любые последующие доступы можно будет осуществлять из любого потока.
В код SwiftUI также необходимо внести несколько улучшений, например создание неструктурированного Task
верхнего уровня, когда вместо этого можно использовать модификатор .task(id:)
, но это выходит за рамки данного вопроса.
спасибо за ответ. Возможно, у него есть работающее решение, но оно не помогает нам понять, как использовать быстрый параллелизм. Почему иногда использование mainactor исправляет проблему (case4), но использование mainactor в другом месте иногда не помогает (case5). И еще один вопрос: в вашем решении даже не используется главный актер, а только сам актер. Кажется, волшебство связано с актерами (любыми актерами), и это, возможно, создает цикл выполнения? Наконец, последний вопрос: сам метод withCheckedThrowingContinuation является асинхронным, так что не сводит ли это на нет всю работу актера, которую выполняют вызывающие объекты?
@erotsppa Важнейшим моментом является «в каком потоке CLLocationManager()
выполняется». CLLocationManager
должен быть создан в потоке, имеющем цикл выполнения. В случае 4 CLLocationManager()
запускается для главного актера, потому что @MainActor
отмечает test2
. В случае 5 CLLocationManager()
не запускается для главного актера, поскольку test3
не отмечен @MainActor
. Совершенно неважно, чем изолирован актер currentLocation2
. На каком актере выполняются другие части кода, также не имеет значения, вызываются ли методы делегата.
@erotsppa В своем коде я предполагал, что первый доступ к APIService.shared
происходит у главного актера, и это все, что имеет значение для этого вопроса. Первый доступ к APIService.shared
приведет к запуску let locationManager = LocationManager()
и, следовательно, к инициализации CLLocationManager
. Обратите внимание, что в этом конкретном случае инициализатор LocationManager
не изолирован от LocationManager
.
@erotsppa Что касается циклов выполнения, цикл выполнения создается для потока при первом доступе RunLoop.current
к этому потоку. Актеры Swift не связаны между собой. Поскольку вы не можете работать с другими потоками напрямую с помощью Swift Concurrency (в том смысле, что вы не можете контролировать, в каком именно фоновом потоке выполняется ваш код), создайте цикл выполнения в любом другом потоке, а затем попробуйте создать CLLocationManager
в эта нить не будет надежной. Вам следует просто использовать цикл выполнения основного потока. Кажется, у вас есть другие путаницы относительно Swift Concurrency. Я бы предложил опубликовать новые вопросы по этому поводу.
@erotsppa Честно говоря, то, как вы написали свой вопрос, не дает понять, что вы хотите «понять, как использовать быстрый параллелизм» (в любом случае это был бы слишком широкий вопрос для Stack Overflow). Вопрос в том, что CLLocationManager
не работает в определенных случаях, поэтому я объяснил почему (а именно, вы должны создать CLLocationManager
в основном потоке), и что «нужно вызвать requestLocation
в основном потоке» — это ложная предпосылка. Если у вас есть другие вопросы, пожалуйста, задайте их отдельным постом.
ваши комментарии помогли прояснить ситуацию. Теперь я это понимаю, наверное, я был слишком сосредоточен на том, где вызывается запрос, и не внимательно прочитал ваш ответ, хотя на самом деле все дело в том, где создается CLLocationManager. Я не знал ничего подобного в SwiftUI, думаю, возможно, это особенность UIKit.
Поскольку это SwiftUI, вам не следует инициализировать объекты как свойства в вашей структуре View, это утечка памяти. Вам необходимо запустить диспетчер местоположения в вашем .task и передать его асинхронным функциям, определенным в структуре, которые сохраняют его во время работы. Также авторизация должна выполняться в другой структуре View.