简体   繁体   English

将核心位置作为 ObservableObject 的 SwiftUI 崩溃

[英]SwiftUI with Core Location as ObservableObject crashes

I am trying to use Core Location to get the CLRegionState to update elements in a SwiftUI app.我正在尝试使用 Core Location 来获取 CLRegionState 以更新 SwiftUI 应用程序中的元素。 I am using XCode 11 beta 6 and have iOS 13 beta 7 on my device.我正在使用 XCode 11 beta 6 并在我的设备上安装 iOS 13 beta 7。

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 {... )应用程序崩溃,错误Thread 1: EXC_BAD_ACCESS出现在第147行 (...ScrollView {... )

  2. The CLRegionState is never called or does not update. CLRegionState 永远不会被调用或不会更新。

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 .我基于 Paul Hudson 的关于 SwiftUI Beacon Detector 的教程(我也无法完成工作),并将其修改为使用 CLRegionState 而不是 Beacon Similarity 。

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:执行 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

Here is a complete working example.这是一个完整的工作示例。 There were several problems I fixed.我修复了几个问题。

  1. ObservableObject does now work with objectWillChange instead of willChange . ObservableObject现在可以使用objectWillChange而不是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..."
        }
    }
}

First off I want to thank Fabian and graycampbell for their help.首先,我要感谢 Fabian 和 graycampbell 的帮助。

Secondly, as for as I can tell @ObservableObject still does not work in iOS 13 beta 8 using XCode 11 beta 6.其次,据我所知,@ObservableObject 在使用 XCode 11 beta 6 的 iOS 13 beta 8 中仍然不起作用。

Here is what worked for me: 1. I changed这对我有用: 1. 我改变了

@ObservedObject var location: MYLocationManager = MYLocationManager()

to:到:

@EnvironmentObject var location: MYLocationManager

2. In the SceneDelegate I added: 2. 在 SceneDelegate 我添加:

let myLocationManager = MYLocationManager()

and:和:

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

No more crash!!不要再崩溃了!!

PS I am using Fabian's updated code. PS 我正在使用 Fabian 的更新代码。 Thanks again!再次感谢!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM