核心位置作为 ObservableObject 的 SwiftUI 崩溃

Posted

技术标签:

【中文标题】核心位置作为 ObservableObject 的 SwiftUI 崩溃【英文标题】:SwiftUI with Core Location as ObservableObject crashes 【发布时间】:2019-08-22 13:13:55 【问题描述】:

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

我可以看到两个问题:

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

    CLRegionState 永远不会被调用或不会更新。

我基于 Paul Hudson 的关于 SwiftUI Beacon Detector 的教程(我也无法进行工作),并将其修改为使用 CLRegionState 而不是信标邻近度。

代码如下:

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

实现F***所做的更改后,控制台日志的内容如下:

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。 它现在应该在每次状态更改时更新。

之前的更新部分不完整(我的看法)

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

【讨论】:

感谢您的工作,但应用程序仍然在同一个地方崩溃。它适用于模拟器,但不适用于设备。 我直接在我的 ipad pro 上运行它。您是否添加了为什么需要位置的描述?另外还需要添加定位功能。最好添加带有先前打印错误的完整错误。 我在问题的末尾添加了控制台日志信息。应用程序的某些功能需要位置才能正常工作。我的设备上允许此应用的位置权限。 请从那里复制到并包括崩溃。没有它,我看不到崩溃的确切位置:( 我相信 iOS 和 iPadOS 现在是两种不同的动物。我必须是一个错误。再次感谢代码建议。我是一个相当缺乏经验的程序员,所以查看您的代码非常有帮助。我可以给你投票并在错误解决后将其标记为已解决。【参考方案2】:

首先,我要感谢 F*** 和 graycampbell 的帮助。

其次,据我所知,@ObservableObject 在使用 XCode 11 beta 6 的 iOS 13 beta 8 中仍然无法正常工作。

这对我有用: 1.我变了

@ObservedObject var location: MYLocationManager = MYLocationManager()

到:

@EnvironmentObject var location: MYLocationManager

2。在我添加的 SceneDelegate 中:

let myLocationManager = MYLocationManager()

和:

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

不再崩溃!!

附:我正在使用 F*** 的更新代码。再次感谢!

【讨论】:

以上是关于核心位置作为 ObservableObject 的 SwiftUI 崩溃的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI = ObservableObject 作为类的选择

SwiftUI 致命错误:未找到“”类型的 ObservableObject

ObservableObject 不更新视图

View.environmentObject(_:) for 可能作为该视图的祖先而丢失

ObservableObject 的问题

来自 ObservableObject 的绑定值