SwiftUI with Core Location as ObservableObject cra

2020-06-30 04:28发布

问题:

I am trying to use Core Location to get the CLRegionState to update elements in a SwiftUI app. I am using XCode 11 beta 6 and have iOS 13 beta 7 on my device.

There are two problems that I can see:

  1. The app crashes and the error Thread 1: EXC_BAD_ACCESS appears on line 147 (...ScrollView {... )

  2. The CLRegionState is never called or does not update.

I am basing this off of Paul Hudson's tutorial on SwiftUI Beacon Detector (which I have not been able to make work either), and modifying it to use CLRegionState instead of beacon proximity .

Here is the code:

import SwiftUI
import CoreLocation
import Combine

class MYLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {


    var locationManager: CLLocationManager?
    var willChange = PassthroughSubject<Void, Never>()
    var lastRegionState = CLRegionState.unknown


    override init() {
        super.init()
        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.requestWhenInUseAuthorization()
    }

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        checkLocationAuthorization()
    }

    func update(state: CLRegionState) {
        lastRegionState = state
        willChange.send(())
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        print("Your location is \(location)")
        update(state: .unknown)
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(error)
    }

    func startScanning() {

        // temporary coordinates
        var workCoordinates: CLLocationCoordinate2D {
            return CLLocationCoordinate2D(
                latitude: 43.486525,
                longitude: -11.912542)
        }

        var homeCoordinates = CLLocationCoordinate2D(
            latitude: 43.499541,
            longitude: -11.875079)

        let workRegion: CLCircularRegion = CLCircularRegion(center: workCoordinates, radius: 100, identifier: "Work")

        let homeRegion: CLCircularRegion = CLCircularRegion(center: homeCoordinates, radius: 100, identifier: "Home")

        locationManager!.startMonitoring(for: workRegion)
        locationManager!.startMonitoring(for: homeRegion)
        locationManager!.requestState(for: workRegion)
        locationManager!.requestState(for: homeRegion)
    }

    func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        switch state {
        case .inside:
            switch region.identifier {
            case "Work":
                print("You are at work")
            case "Home":
                print("You are at home")
            default:
                print("unknown")
            }
        default:
            break
        }
    }

    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {

        switch region.identifier {
        case "Work":
            print("Work**********")
        //self.taskTypeSegCtrl.selectedSegmentIndex = 0
        case "Home":
            print("Home*********8")
        //self.taskTypeSegCtrl.selectedSegmentIndex = 1
        default:
            break
        }
    }

    func checkLocationAuthorization() {
        switch CLLocationManager.authorizationStatus() {
        case .authorizedWhenInUse:
            startScanning()
            break
        case .authorizedAlways:
            startScanning()
            break
        case .denied:
            // show an alert instructing them howto turn on permissions
            break
        case .notDetermined:

            print("Location authorization is not determined.")
            locationManager!.requestAlwaysAuthorization()
            break
        case .restricted:
            break
        @unknown default:
            fatalError()
        }
    }
}


struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjectContext

    @FetchRequest(entity: Task.entity(),
                  sortDescriptors: [NSSortDescriptor(
                    keyPath: \Task.name, ascending: true)])
    var tasks: FetchedResults<Task>

    var locationManager = CLLocationManager()

    @ObservedObject var location: MYLocationManager = MYLocationManager()

    @State private var taskName = ""
    @State private var taskType = 0
    @State private var selectedTask = ""
    @State private var numberOfTaps = 0
    @State private var regionState = CLRegionState.unknown

    var body: some View {

        ScrollView {
            VStack {
                TextField("Enter a task name", text: $taskName)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                Picker(selection: $taskType, label: Text("Task type")) {
                    Text("Work").tag(1)
                    Text("Home").tag(2)
                }.pickerStyle(SegmentedPickerStyle())

                Text(selectedTask)

                Button(action: {
                    let task = Task(context: self.managedObjectContext)
                    task.name = self.taskName
                    task.type = Int16(self.taskType)
                    do {
                        try self.managedObjectContext.save()
                    } catch {
                        // handle the Core Data error
                    }
                    self.taskName = ""
                }) {
                    Text("Save Task")
                }.padding()

                Button(action: {
                    if self.numberOfTaps < self.tasks.count {
                        let task = self.tasks[self.numberOfTaps].name
                        self.selectedTask = task ?? "No task..."
                        self.numberOfTaps = self.numberOfTaps + 1
                    } else {
                        self.selectedTask = "No more tasks!  Have a wonderful day."
                    }
                }) {
                    Text("Next Task")
                }

                List {
                    ForEach(tasks, id: \.self) {
                        task in
                        VStack(alignment: .leading, spacing: 6) {
                            Text(task.name ?? "Unknown")
                                .font(.headline)
                            Text("Task type \(task.type)")
                                .font(.caption)
                        }
                    }.onDelete(perform: removeTask)

                }
            }   .frame(width: 300, height: 400, alignment: .top)
                .padding()
                .border(Color.black)

            if regionState == .inside {
                Text("inside")
            } else if regionState == .outside {
                Text("outside")
            } else {
                Text("unknown")
            }


            Spacer()
        }
    }


    func removeTask(at offsets: IndexSet) {
        for index in offsets {
            let task = tasks[index]
            managedObjectContext.delete(task)
            do {
                try managedObjectContext.save()
            } catch {
                // handle the Core Data error
            }
        }
    }

    func showTask(at offsets: IndexSet) {
        for index in offsets {
            let task = tasks[index]
            selectedTask = task.name ?? "No task..."
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

After implementing the changes made by Fabian, here is the content of the console log:

Granted: true 2019-08-22 14:30:07.051062-0600 AppName[4452:2089841] locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) 2019-08-22 14:30:07.052803-0600 New1Thing[4452:2089841] startScanning 2019-08-22 14:30:07.054319-0600 New1Thing[4452:2089841] Current location: <+.49945068,-*.87504490> +/- 65.00m (speed -1.00 mps / course -1.00) @ 8/22/19, 2:30:07 PM **** Daylight Time

回答1:

Here is a complete working example. There were several problems I fixed.

  1. ObservableObject does now work with objectWillChange instead of willChange.
  2. It should now update on every status change.

The updating part was not complete before (my opinion)

import SwiftUI
import CoreLocation
import Combine
import CoreData
import os

class MYLocationManager: NSObject, ObservableObject {
    var locationManager: CLLocationManager?
    var objectWillChange = PassthroughSubject<Void, Never>()
    @Published var lastRegionState = CLRegionState.unknown {
        willSet {
            objectWillChange.send()
        }
    }
    @Published var currentRegion: Region = .nowhereKnown {
        willSet {
            objectWillChange.send()
        }
    }

    override init() {
        super.init()
        locationManager = CLLocationManager()
        locationManager!.delegate = self
        locationManager!.requestWhenInUseAuthorization()
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        fatalError("error: \(error.localizedDescription)")
    }

    enum Region: String {
        case work = "Work"
        case home = "Home"
        case nowhereKnown = "Nowhere Known"
    }

    func startScanning() {
        os_log("startScanning")

        // temporary coordinates
        var workCoordinates: CLLocationCoordinate2D {
            return CLLocationCoordinate2D(
                latitude: 43.486525,
                longitude: -11.912542)
        }

        var homeCoordinates = CLLocationCoordinate2D(
            latitude: 43.499541,
            longitude: -11.875079)

        if let currentLocation = locationManager?.location {
            os_log("Current location: %@", currentLocation.description)
            homeCoordinates = currentLocation.coordinate
        } else {
            os_log("Current location: failed")
        }

        let workRegion: CLCircularRegion = CLCircularRegion(center: workCoordinates, radius: 100, identifier: Region.work.rawValue)
        let homeRegion: CLCircularRegion = CLCircularRegion(center: homeCoordinates, radius: 100, identifier: Region.home.rawValue)

        locationManager!.startMonitoring(for: workRegion)
        locationManager!.startMonitoring(for: homeRegion)
        locationManager!.requestState(for: workRegion)
        locationManager!.requestState(for: homeRegion)
    }
}

// MARK: Authorization
extension MYLocationManager {
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        os_log("locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)")
        checkLocationAuthorization()
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        os_log("locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])")
        guard let location = locations.last else { return }
        print("Your location is \(location)")
        update(state: .unknown)
    }

    func checkLocationAuthorization() {
        switch CLLocationManager.authorizationStatus() {
        case .authorizedWhenInUse:
            startScanning()
            break
        case .authorizedAlways:
            startScanning()
            break
        case .denied:
            // show an alert instructing them howto turn on permissions
            break
        case .notDetermined:

            print("Location authorization is not determined.")
            locationManager!.requestAlwaysAuthorization()
            break
        case .restricted:
            break
        @unknown default:
            fatalError()
        }
    }
}

// MARK: UI Updates
extension MYLocationManager: CLLocationManagerDelegate {
    func updateCurrentRegion(region: CLRegion) {
        guard let region = Region(rawValue: region.identifier) else {
            currentRegion = .nowhereKnown
            return
        }
        currentRegion = region
    }

    func update(state: CLRegionState) {
        lastRegionState = state
    }

    func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        self.lastRegionState = state
        updateCurrentRegion(region: region)
    }

    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        updateCurrentRegion(region: region)
    }

    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        updateCurrentRegion(region: region)
    }
}

struct CoreLocationView: View {
    private static func makeContainer() -> NSPersistentContainer {
        let store = NSPersistentContainer(name: "CoreLocationView")
        store.loadPersistentStores { (desc, err) in
            if let err = err {
                fatalError("core data error: \(err)")
            }
        }
        return store
    }

    let container: NSPersistentContainer

    init() {
        self.container = CoreLocationView.makeContainer()
    }

    var body: some View {
        CoreLocationView_NeedsEnv().environment(\.managedObjectContext, container.viewContext)
    }
}

struct CoreLocationView_NeedsEnv: View {
    @Environment(\.managedObjectContext) var managedObjectContext

    @FetchRequest(entity: Task.entity(),
                  sortDescriptors: [NSSortDescriptor(
                    keyPath: \Task.name, ascending: true)])
    var tasks: FetchedResults<Task>

    var locationManager = CLLocationManager()

    @ObservedObject var location: MYLocationManager = MYLocationManager()

    @State private var taskName = ""
    @State private var taskType = 0
    @State private var selectedTask = ""
    @State private var numberOfTaps = 0
    //@State private var regionState = CLRegionState.unknown

    var body: some View {
        ScrollView {
            VStack {
                TextField("Enter a task name", text: $taskName)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                Picker(selection: $taskType, label: Text("Task type")) {
                    Text("Work").tag(1)
                    Text("Home").tag(2)
                }.pickerStyle(SegmentedPickerStyle())

                Text(selectedTask)

                Button(action: {
                    let task = Task(context: self.managedObjectContext)
                    task.name = self.taskName
                    task.type = Int16(self.taskType)
                    do {
                        try self.managedObjectContext.save()
                    } catch {
                        // handle the Core Data error
                    }
                    self.taskName = ""
                }) {
                    Text("Save Task")
                }.padding()

                Button(action: {
                    if self.numberOfTaps < self.tasks.count {
                        let task = self.tasks[self.numberOfTaps].name
                        self.selectedTask = task ?? "No task..."
                        self.numberOfTaps = self.numberOfTaps + 1
                    } else {
                        self.selectedTask = "No more tasks!  Have a wonderful day."
                    }
                }) {
                    Text("Next Task")
                }

                List {
                    ForEach(tasks, id: \.self) {
                        task in
                        VStack(alignment: .leading, spacing: 6) {
                            Text(task.name ?? "Unknown")
                                .font(.headline)
                            Text("Task type \(task.type)")
                                .font(.caption)
                        }
                    }.onDelete(perform: removeTask)

                }
            }   .frame(width: 300, height: 400, alignment: .top)
                .padding()
                .border(Color.black)

            if location.lastRegionState == .inside {
                Text("inside")
            } else if location.lastRegionState == .outside {
                Text("outside")
            } else {
                Text("unknown")
            }

            Text("Where am I: \(location.currentRegion.rawValue)")


            Spacer()
        }
    }


    func removeTask(at offsets: IndexSet) {
        for index in offsets {
            let task = tasks[index]
            managedObjectContext.delete(task)
            do {
                try managedObjectContext.save()
            } catch {
                // handle the Core Data error
            }
        }
    }

    func showTask(at offsets: IndexSet) {
        for index in offsets {
            let task = tasks[index]
            selectedTask = task.name ?? "No task..."
        }
    }
}


回答2:

First off I want to thank Fabian and graycampbell for their help.

Secondly, as for as I can tell @ObservableObject still does not work in iOS 13 beta 8 using XCode 11 beta 6.

Here is what worked for me: 1. I changed

@ObservedObject var location: MYLocationManager = MYLocationManager()

to:

@EnvironmentObject var location: MYLocationManager

2. In the SceneDelegate I added:

let myLocationManager = MYLocationManager()

and:

 window.rootViewController = UIHostingController(rootView: CoreLocationView_NeedsEnv()
            .environmentObject(myLocationManager)

No more crash!!

P.S. I am using Fabian's updated code. Thanks again!