In my main view MainView() I load weather data from WeatherKit asynchronously via .task() which runs whenever the user's location changes:
.task(id: locationManager.currentLocation){
if let location = locationManager.currentLocation{
weather = try await weatherService.weather(for: location)
}
}
However, the user's location may not change for a long period of time or even never and I would like for the weather data to be at least updated once every hour.
How would I update WeatherKit data every hour AND update every single time their location changes.
Obviously I can’t rely on the WeatherKit weather object within the ‘.task(id:)’ to update as that is the object that I’d need to update.
for fetching location after every few mins what approach I have used is
class BackgroundLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = BackgroundLocationManager()
private var locationManager: CLLocationManager!
private var timer: DispatchSourceTimer?
private var counter: Int = 0
#Published var location: LocationModel? = nil
private override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestAlwaysAuthorization()
}
private func startTimer(delay: Int = 15) {
let queue = DispatchQueue(label: "com.example.timer", qos: .background)
timer = DispatchSource.makeTimerSource(queue: queue)
timer?.schedule(deadline: .now(), repeating: .seconds(delay))
timer?.setEventHandler { [weak self] in
self?.locationManager.startUpdatingLocation()
self?.counter = 0
}
timer?.resume()
}
func fetchLocation(interval: Int? = nil) {
if let interval = interval {
startTimer(delay: interval)
} else {
self.locationManager.startUpdatingLocation()
}
}
func stopFetchingLocation() {
timer?.cancel()
timer = nil
}
}
extension BackgroundLocationManager {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
break
case .restricted, .denied:
// do something when permission is denied
case .authorizedAlways, .authorizedWhenInUse, .authorized:
// do something when permission is given
break
#unknown default:
return
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if let error = error as? CLError, error.code == .denied {
// do something when unable to fetch location
return
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.counter += 1
locationManager.stopUpdatingLocation()
let currentLocation = locations.last?.coordinate
let long = String(currentLocation?.longitude ?? 0)
let lat = String(currentLocation?.latitude ?? 0)
let verticalAcc = String(locations.last?.verticalAccuracy ?? 0)
let horizontalAcc = String(locations.last?.horizontalAccuracy ?? 0)
let altitude = String(locations.last?.altitude ?? 0)
let location = LocationModel(longitude: long, latitude: lat,
verticalAccuracy: verticalAcc,
horizontalAccuracy: horizontalAcc, alitude: altitude)
if counter <= 1 {
self.location = location
}
}
}
To use it according to my need
struct MainView: View {
#StateObject var vm = MainViewModel()
init() {
BackgroundLocationManager.shared.fetchLocation() // when needed once
BackgroundLocationManager.shared.fetchLocation(interval: 15) // needed after how many seconds
}
var body: some View {
ZStack {
MyCustomeView()
}
.onReceive(BackgroundLocationManager.shared.$location) { location in
vm.doSomething(location: location)
}
}
}
Use a timer inside your view.
struct WeatherView: View {
let weatherService: WeatherService
let location: Location
#State var weather: Weather?
let timer = Timer.publish(every: 3600, on: .main, in: .common).autoconnect()
var body: some View {
Text(weather.description)
.onReceive(timer) { _ in
fetch()
}
}
func fetch() {
Task {
weather = try await weatherService.weather(for: location)
}
}
}
Related
So I have a widget for a weather app (the app currently doesn't use WeatherKit but I'd eventually like to move it over), but I want to implement a widget using WeatherKit beforehand. I made a practice app using WeatherKit and got all the components I'd want to use in the widget to work correctly.
Here's the entirety of the main app that works perfectly:
import SwiftUI
import CoreLocation
import WeatherKit
var userCity: String = ""
class LocationManager: NSObject, ObservableObject {
#Published var currentLocation: CLLocation?
#Published var userCity: String = ""
private let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, currentLocation == nil else { return }
DispatchQueue.main.async {
self.currentLocation = location
}
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
guard error == nil else {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
guard placemarks!.count > 0 else {
print("Problem with the data received from geocoder")
return
}
let pm = placemarks![0].locality
print(pm!)
let userLocation = (pm!)
print(userLocation)
DispatchQueue.main.async {
self.userCity = userLocation
}
})
}
}
struct ContentView: View {
let weatherService = WeatherService.shared
#StateObject private var locationManager = LocationManager()
#State private var weather: Weather?
var body: some View {
VStack {
if let weather{
let celsiusWeather = weather.currentWeather.temperature.converted(to: .celsius)
let celsiusFormatted = String(format: "%.0f", celsiusWeather.value)
let celsiusFinal = "\(celsiusFormatted)°C"
let fahrenheitWeather = weather.currentWeather.temperature.converted(to: .fahrenheit)
let fahrenheitFormatted = String(format: "%.0f", fahrenheitWeather.value)
let fahrenheitFinal = "\(fahrenheitFormatted)°F"
VStack{
Image(systemName: weather.currentWeather.symbolName)
.resizable()
.symbolRenderingMode(.palette)
.scaledToFit()
.frame(width: 80, height: 80)
Text(locationManager.userCity)
.font(.largeTitle)
Text(fahrenheitFinal)
Text(celsiusFinal)
}
}
}
.padding()
.task(id: locationManager.currentLocation) {
do{
if let location = locationManager.currentLocation{
self.weather = try await weatherService.weather(for: location)
print(weather!)
}
}catch{
print(error)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Basically, I've just been trying to move this code over to the widget, unsure if that would even work properly (clearly something doesn't).
And here is what I've been trying to do with the widget, but it comes up blank, so something is coming up nil and can't retrieve the weather data. But Xcode doesn't give me any errors.
import WidgetKit
import SwiftUI
import WeatherKit
import CoreLocation
var userCity: String?
class LocationManager: NSObject, ObservableObject {
#Published var currentLocation: CLLocation?
#Published var userCity: String = ""
private let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, currentLocation == nil else { return }
DispatchQueue.main.async {
self.currentLocation = location
}
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
guard error == nil else {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
guard placemarks!.count > 0 else {
print("Problem with the data received from geocoder")
return
}
let pm = placemarks![0].locality
print(pm!)
let userLocation = (pm!)
print(userLocation)
DispatchQueue.main.async {
self.userCity = userLocation
}
})
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), city: "", temp: "0") // placeholder humidity
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), city: "", temp: "0") // placeholder humidity
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let locationManager = LocationManager()
#State var currentWeather: Weather?
let nextUpdate = Date().addingTimeInterval(1800)
Task {
do{
if let location = locationManager.currentLocation{
let currentWeather = try await WeatherService.shared.weather(for: location)
print(currentWeather)
}
}catch{
print(error)
}
}
let entry = SimpleEntry(date: .now, city: userCity!, temp: (currentWeather?.currentWeather.temperature.description)!)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let city: String
let temp: String
}
struct WidgetWeatherTestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack{
Text(entry.city)
Text(entry.temp)
}
}
}
struct ShitsdagWidget: Widget {
let kind: String = "WidgetWeatherTest"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WidgetWeatherTestWidgetEntryView(entry: entry)
}
.configurationDisplayName("Widget Weather Test")
.description("Testing weatherkit in a Widget")
.supportedFamilies([.systemSmall])
}
}
Because widgets are set up slightly differently than just SwiftUI with the timeline and everything, I might just have things in the wrong spot to be triggered in the right order. Any help would be great.
I call an API and get a response with a longitude and latitude. Now, I'm trying to show and update the city name. I made a function ("place") to convert the longitude and latitude to a city name. Unfortunately is doesn't work. Please, who could give me a help on this one? Thanks in advance.
Below I will show the code. I filled the struct "APIcoord" with lat and lon example coordinates.
public final class WeatherService: NSObject {
private let locationManager = CLLocationManager()
private var completionHandler: ((Weather) -> Void)?
public override init() {
super.init()
locationManager.delegate = self
}
public func loadWeatherData(_ completionhandler: #escaping((Weather) -> Void)) {
self.completionHandler = completionhandler
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
private func makeDataRequest(forCoordinates coorodinates: CLLocationCoordinate2D) {
}
}
extension WeatherService: CLLocationManagerDelegate {
public func locationManager(
_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]
) {
guard let location = locations.first else {return}
makeDataRequest(forCoordinates: location.coordinate)
}
public func locationManager(
_ manager: CLLocationManager, didFailWithError error: Error
) {
print("Something went wrong: \(error.localizedDescription)")
}
}
struct APIResponse: Decodable {
let coord: APIcoord
}
struct APIcoord: Decodable{
let lon: Double = -122.031
let lat: Double = 37.33
}
public struct Weather {
let longtitude: Double
let latitude: Double
init(response: APIResponse) {
longtitude = response.coord.lon
latitude = response.coord.lat
}
}
public class WeaterViewModel: ObservableObject {
#Published var longtitude: Double = 0.0
#Published var latitude: Double = 0.0
public let weatherService: WeatherService
public init(weatherService: WeatherService) {
self.weatherService = weatherService
}
public func refresh() {
weatherService.loadWeatherData { weather in
DispatchQueue.main.sync {
self.longtitude = weather.longtitude
self.latitude = weather.latitude
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: WeaterViewModel
#State var placeFound: String?
var body: some View {
NavigationView{
ZStack{
VStack {
Text(placeFound ?? "No Place Found")
.onAppear(){
place(APIlatitude: viewModel.latitude, APIlongitude: viewModel.longtitude){(city) in
placeFound = city
print("Place: \(String(describing: city))" )
}
}
}
.onAppear{
viewModel.refresh()
}
}
.navigationBarHidden(true)
}
}
func place(APIlatitude : Double, APIlongitude : Double, completion: #escaping (String?) -> Void) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: APIlatitude, longitude: APIlongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, _) -> Void in
placemarks?.forEach { (placemark) in
if let city = placemark.locality {
print(city)
completion(city)
}
else{
print("Can't find place")
}
}
})
}
}
You could pair GLGeocoder's async API with SwiftUI's task like this:
struct ContentView: View {
#StateObject var locator = Locator()
var body: some View {
if let location = locator.location {
ContentView2(location: location)
}
else {
Text("Hello, world!")
.padding()
}
}
}
struct ContentView2Config {
let geocoder = CLGeocoder()
var text = Self.noCityText
static let noCityText = "No City"
mutating func reverseGeocode(location: Location) async {
text = Self.noCityText
let location = CLLocation(latitude: location.lat, longitude: location.lon)
if let city = try? await geocoder.reverseGeocodeLocation(location)
.first
.flatMap({ placemark in
placemark.locality
})
{
text = city
}
else {
text = Self.noCityText
}
}
}
struct ContentView2: View {
let location: Location
#State var config = ContentView2Config()
var body: some View {
Text(config.text)
.task(id: location.id) {
await config.reverseGeocode(location: location)
}
}
}
And in case it helps, here is the location manager:
struct Location: Identifiable {
let id = UUID()
let lat: Double
let lon: Double
}
class Locator: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var location: Location?
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
locationManager.startUpdatingLocation()
locationManager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let coordinate = locations.last?.coordinate else {
location = nil
return
}
location = Location(lat: coordinate.latitude, lon: coordinate.longitude)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
location = nil
}
}
I am learning to develop apps for iOS from scratch. And I chose SwiftUI to make an app that gets the location of the user, get with geocode the city where he is and with that information obtained, show a list of items that belong to that city from a API.
So, I learned on one hand how to get the location and on the other hand how to display a list. My problem now is that when you run .onAppear(perform: loadData) to display my list, the "city" result is still empty. Evidently the value of city is obtained after I try to display the list of the city.
Both the algorithm I have to get the location and the one I have to show the list work separately.
So my code is:
import SwiftUI
struct Response: Codable {
var cinemas: [Cinema]
}
struct Cinema: Codable {
var _id: String
var cinemaName: String
var cinemaCategory: String
}
struct HomeScreenView: View {
#State var results = [Cinema]()
#ObservedObject var lm = LocationManager()
var latitude: String {
return("\(lm.location?.latitude ?? 0)") }
var longitude: String { return("\(lm.location?.longitude ?? 0)") }
var placemark: String { return("\(lm.placemark?.description ?? "XXX")") }
var status: String { return("\(lm.status)") }
var city: String {
return("\(lm.placemark?.locality ?? "empty")")
}
var body: some View {
VStack {
List(results, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.cinemaName)
.font(.headline)
Text(item.cinemaCategory)
}
}.onAppear(perform: loadData)
}
}
func loadData() {
guard let url = URL(string: "https://mycinemasapi.com/cinemasbycity/\(self.city)") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.cinemas
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
UPDATE:
LocationManager class
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private func geocode() {
guard let location = self.location else { return }
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
} else {
self.placemark = nil
}
})
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
self.geocode()
}
}
As by your code just do load data on placemark received, like
List(results, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.cinemaName)
.font(.headline)
Text(item.cinemaCategory)
}
}.onReceive(lm.$placemark) {
if $0 != nil {
self.loadData()
}
}
I'm a beginner, have been working on a weather project, trying to get the lat and long from LocationManager in WeatherViewModel class and pass it the API so i can get the user location based data. but can't get the lattitude and longitude in the WeatherViewModel. the simulator shows a random temperature and city as globe.(SwiftUI and Xcode 11.6)
here's the LocationManager class
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
private let manager: CLLocationManager
#Published var lastKnownLocation: CLLocation?
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .denied{
print("denied")
}
else{
print("athorized")
manager.requestLocation()
}
}
func start() {
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
init(manager: CLLocationManager = CLLocationManager()) {
self.manager = manager
super.init()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func startUpdating() {
self.manager.delegate = self
self.manager.requestWhenInUseAuthorization()
self.manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastKnownLocation = locations.last
print("\((lastKnownLocation?.coordinate.latitude)!)")
print("\((lastKnownLocation?.coordinate.longitude)!)")
}
}
Here's the WeatherViewModel class
import SwiftUI
import Combine
import CoreLocation
class WeatherViewModel: ObservableObject {
#ObservedObject var location = LocationManager()
let client = OpenWeatherAPI()
var stateView: StateView = StateView.loading{
willSet{
objectWillChange.send() }
}
var currentWeather = CurrentWeather.emptyInit(){
willSet{
objectWillChange.send()
}
}
var todayWeather = ForecastWeather.emptyInit(){
willSet{
objectWillChange.send()
}
}
var hourlyWeathers: [ForecastWeather] = [] {
willSet{
objectWillChange.send()
}
}
var dailyWeathers: [ForecastWeather] = [] {
willSet{
objectWillChange.send()
}
}
var currentDescription = ""{
willSet{
objectWillChange.send()
}
}
private var stateCurrentWeather = StateView.loading
private var stateForecastWeather = StateView.loading
var latitude: String{
return "\(location.lastKnownLocation?.coordinate.latitude ?? 0.0)"
}
var longitude: String{
return "\(location.lastKnownLocation?.coordinate.longitude ?? 0.0)"
}
func printLatlon(){
print("WeatherViewModel")
print(latitude )
print(longitude )
}
init (){
getData()
printLatlon()
}
func retry(){
stateView = .loading
stateCurrentWeather = .loading
stateForecastWeather = .loading
}
private func getData() {
client.getCurrentWeather(at: latitude, at: longitude) { [weak self] currentWeather, error in
guard let ws = self else { return }
if let currentWeather = currentWeather {
ws.currentWeather = currentWeather
ws.todayWeather = currentWeather.getForecastWeather()
ws.currentDescription = currentWeather.description()
ws.stateCurrentWeather = .success
} else {
ws.stateCurrentWeather = .failed
}
ws.updateStateView()
}
client.getForecastWeather(at: latitude, at: longitude) { [weak self] forecastWeatherResponse, error in
guard let ws = self else { return }
if let forecastWeatherResponse = forecastWeatherResponse {
ws.hourlyWeathers = forecastWeatherResponse.list
ws.dailyWeathers = forecastWeatherResponse.dailyList
ws.stateForecastWeather = .success
} else {
ws.stateForecastWeather = .failed
}
ws.updateStateView()
}
}
private func updateStateView() {
if stateCurrentWeather == .success, stateForecastWeather == .success {
stateView = .success
}
if stateCurrentWeather == .failed, stateForecastWeather == .failed {
stateView = .failed
}
}
}
I'm passing the data in this
private func getData() {
client.getCurrentWeather(at: latitude, at: longitude) { }
client.getForecastWeather(at: latitude, at: longitude) { }
}
if i use this
var latitude: String{
return "\(location.lastKnownLocation?.coordinate.latitude ?? 0.0)"
}
var longitude: String{
return "\(location.lastKnownLocation?.coordinate.longitude ?? 0.0)"
}
Then i get this output in the simulator
the console shows this
the console shows that it didnt get the lat and lon
but if i give a string of lat and lon like this
var latitude = "36.778259"
var longitude = "-119.417931"
Then i get expected output in the simulator
console output
because I pass the string location and thats why showing my string values
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:
The app crashes and the error Thread 1: EXC_BAD_ACCESS appears on line 147 (...ScrollView {... )
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
Here is a complete working example. There were several problems I fixed.
ObservableObject does now work with objectWillChange instead of willChange.
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.
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!