After having asked this question it turned out I was not specific enough, to the point where the question and its answer did actually not solve my issue.
In short, I have a function that is called by a CLLocationManager
and takes a while to update things on the server.
In the UI a function fetch()
can be triggered. This should run after the update()
has finished. Thus, if there is no update()
running, right away.
Here is a MRE:
import SwiftUI
import CoreLocation
class ExampleManager: ObservableObject {
func fetch() {
print("these would be some results")
}
}
struct ContentView2: View {
@StateObject var locationManager = LocationManager()
@StateObject var example = ExampleManager()
var body: some View {
Button {
example.fetch()
} label: {
Text("fetch")
}
}
}
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
// .. necessary stuff of CLLocationManager
private func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) async {
guard let newLocation: CLLocation = locations.first else { return }
await update(newLocation: newLocation)
}
func update(newLocation: CLLocation) async {
print("1")
try! await Task.sleep(nanoseconds: 10_000_000_000)
print("2")
}
}
CodePudding user response:
You probably don't want to show the button unless there is something to fetch to begin with. I have a couple of scenarios here:
import UIKit
import SwiftUI
import CoreLocation
var greeting = "Hello, playground"
class ExampleManager: ObservableObject {
func fetch() {
}
}
struct ContentView: View {
@StateObject var locationManager = LocationManager()
@StateObject var example = ExampleManager()
var body: some View {
// ProgressView()
// .onReceive(locationManager.$updated) { updated in
// example.fetch()
// }
if locationManager.updated {
Button {
example.fetch()
} label: {
Text("fetch")
}
} else {
EmptyView()
}
}
}
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
@Published var updated = false
private func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) async {
guard let newLocation: CLLocation = locations.first else { return }
await update(newLocation: newLocation)
}
func update(newLocation: CLLocation) async {
print("1")
try! await Task.sleep(nanoseconds: 10_000_000_000)
print("2")
updated = true
}
}
CodePudding user response:
You should convert what you are calling now Managers into services.
//Make a service
class ExampleService {
func fetch() {
print("3 these would be some results")
}
}
Then add a var onReceive: ((CLLocation) async -> Void)?
that get called after your update.
//Change to service
class LocationService: NSObject, CLLocationManagerDelegate {
var count: Int = 0
private let mgr = CLLocationManager()
/// gets called @didUpdateLocations after update
var onReceive: ((CLLocation) async -> Void)?
override init() {
super.init()
//Mimics location remove for real code
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] timer in
count = 1
Task{
await locationManager(mgr, didUpdateLocations: [.init(latitude: Double((-90...90).randomElement()!), longitude: Double((-180...180).randomElement()!))])
}
if count >= 10{
timer.invalidate()
}
}
}
private func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) async {
guard let newLocation: CLLocation = locations.first else { return }
await update(newLocation: newLocation)
//Run this after update
await onReceive?(newLocation)
print("4 done")
}
func update(newLocation: CLLocation) async {
print("1")
try! await Task.sleep(nanoseconds: 10_000_000_000)
print("2")
}
}
Both of these services wold come together in a Manager
struct ExampleManager{
let exampleSvc: ExampleService = .init()
let locationSvc: LocationService = .init()
init(){
//Give onReceive a value
locationSvc.onReceive = { [self] location in
//Call fetch when this closure is called
exampleSvc.fetch()
}
}
func fetch(){
exampleSvc.fetch()
}
}
Then your View
will be simplified and unconcerned about anything that happens that doesn't directly involve UI.
struct ContentView2: View {
@State var examapleMgr = ExampleManager()
var body: some View {
Button {
examapleMgr.fetch()
} label: {
Text("fetch")
}
}
}
You can add variable to lock the Button
until update has run at least once.
@MainActor
class ExampleManager: ObservableObject{
let exampleSvc: ExampleService = .init()
let locationSvc: LocationService = .init()
//You can add a check if you want to disable the button if you dont want to run fetch until location has updated at least once
@Published var hasRunOnce: Bool = false
init(){
//Give onReceive a value
locationSvc.onReceive = { [self] location in
hasRunOnce = true
//Call fetch when this closure is called
fetch()
}
}
func fetch(){
exampleSvc.fetch()
}
}
struct ContentView2: View {
@StateObject var examapleMgr = ExampleManager()
var body: some View {
Button {
examapleMgr.fetch()
} label: {
Text("fetch")
}.disabled(!examapleMgr.hasRunOnce)
}
}