具有核心位置的 SwiftUI 作为 ObservableObject 崩溃

SwiftUI with Core Location as ObservableObject crashes

我正在尝试使用 Core Location 获取 CLRegionState 以更新 SwiftUI 应用程序中的元素。我正在使用 XCode 11 beta 6 并在我的设备上安装了 iOS 13 beta 7。

我看到有两个问题:

  1. 应用程序崩溃,错误 Thread 1: EXC_BAD_ACCESS 出现在行 147 (...ScrollView {... )

  2. 从未调用或不更新 CLRegionState。

我基于 Paul Hudson 关于 SwiftUI Beacon Detector 的教程(我也无法完成),并将其修改为使用 CLRegionState 而不是 beacon proximity 。

代码如下:

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

执行 Fabian 所做的更改后,控制台日志的内容如下:

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. ObservableObject 现在可以使用 objectWillChange 而不是 willChange
  2. 它现在应该在每次状态变化时更新。

之前更新的部分没有完成(我的意见)

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..."
        }
    }
}

首先要感谢 Fabian 和 graycampbell 的帮助。

其次,据我所知,@ObservableObject 在 iOS 13 beta 8 中使用 XCode 11 beta 6 仍然不起作用。

以下是对我有用的方法: 1.我改了

@ObservedObject var location: MYLocationManager = MYLocationManager()

至:

@EnvironmentObject var location: MYLocationManager

2。在我添加的 SceneDelegate 中:

let myLocationManager = MYLocationManager()

和:

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

不再崩溃!!

P.S。我正在使用 Fabian 的更新代码。再次感谢!