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!
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.
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)
}
}
}
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
}
}
Hello I’m really new to iOS and SwiftUi so don't hate me if my formatting is wrong haha. I’m having trouble converting the users location coordinates to a US address. I have gotten most of the code done but for some reason on line 109 I create the #Published var location and set it to some random location and then I try to update it on line l72 but for some reason it just uses the first location I pass in on line 109. Id love for this to update and use the users current location after they grant permission. Thanks you for any suggestions you have :)
//
// ContentView.swift
// Parked
//
// Created by Luke Jamison on 11/2/21.
//
import SwiftUI
import MapKit
import CoreLocation
import CoreLocationUI
struct ContentView: View {
static let sharedV = ContentView()
#StateObject private var viewModel = ContentViewModel()
#State var addressData: String = ""
#State private var showingImagePicker = false
#State private var image: Image?
#State private var inputImage: UIImage?
//#State var geo: Any = locationManager.location
var body: some View {
NavigationView {
VStack {
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.onAppear {
viewModel.checkIFLocationServicesisEnabled()
setData()
//viewModel.runSetDatafunc()
}.frame(maxWidth: .infinity, maxHeight: .infinity).cornerRadius(20).padding()
Form {
Section(header: Text("Add Location Details")) {
Text(addressData)
Menu {
Button(action: {
self.showingImagePicker = true
}) {
Label("Take Picture", systemImage: "camera.shutter.button")
}
image?.resizable()
.frame(width: 250, height: 250)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
} label: {
Label("Add Image", systemImage: "photo.on.rectangle.angled")
}
}
if image != nil {
Section(header: Text("Image Taken")) {
ZStack {
Rectangle().fill(Color.secondary)
if image != nil {
image?.resizable().scaledToFit()
} else {
}
}
Button(action: {
image = nil
}) {
Label("Remove Picture", systemImage: "trash.fill")
}
}
} else {
}
}.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
}.navigationBarTitle("Parked", displayMode: .automatic)
}
}.navigationViewStyle(.stack)
}
func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
func setData() {
ContentViewModel.shared.resolveLoactionName(with: viewModel.location) { [self] locationName in
self.addressData = locationName
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = ContentViewModel()
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 33.4, longitude: -117.4), span:
MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
#Published var address = ""
#Published var location = CLLocation.init(latitude: 33, longitude: -117)
var locationManager: CLLocationManager?
func checkIFLocationServicesisEnabled() {
if CLLocationManager.locationServicesEnabled() {
locationManager = CLLocationManager()
locationManager!.delegate = self
//locationManager?.desiredAccuracy = kCLLocationAccuracyBest
} else {
print("this is off turn in the settings...")
}
}
public func resolveLoactionName(with location: CLLocation, completion: #escaping ((String) -> Void)) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: .current) { placemarks, error in
guard let place = placemarks?.first, error == nil else {
completion("")
return
}
print(place)
var name = ""
if let locality = place.subThoroughfare {
name += locality
}
if let street = place.thoroughfare {
name += " \(street)"
}
if let city = place.locality {
name += " \(city)"
}
if let adminRegion = place.administrativeArea {
name += ", \(adminRegion)"
}
if let zipCode = place.postalCode {
name += " \(zipCode)"
}
completion(name)
}
}
private func checkLocationAuth() {
guard let locationManager = locationManager else { return }
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
print("location is restricted")
case .denied:
print("location is denied in settings for app")
case .authorizedAlways, .authorizedWhenInUse:
region = MKCoordinateRegion(center: locationManager.location!.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
ContentView.sharedV.setData()
self.location = CLLocation(latitude: region.self.center.latitude, longitude: region.self.center.longitude)
print(location)
//runSetDatafunc()
//address = "\(locationManager.location!.coordinate.latitude), \(locationManager.location!.coordinate.longitude)"
/*geocode(latitude: region.center.latitude, longitude: region.center.longitude) { placemark, error in
let placemark = placemark?.first
//print(placemark?.description)
//self.address = "\(placemark?.thoroughfare), \(placemark?.locality), \(placemark?.administrativeArea)"
}*/
#unknown default:
break
}
}
public func runSetDatafunc() {
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuth()
}
}
}
Unfortunately, due to the transient nature of SwiftUI views, you definitely won't have luck making a singleton out of a View. But, you don't need it because you're already publishing your region and location, which your View will update in reaction to.
Here's a simplified/refactored version of your code:
struct ContentView: View {
#StateObject private var viewModel = ContentViewModel()
var body: some View {
VStack {
Text(viewModel.address)
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.onAppear {
viewModel.checkIfLocationServicesisEnabled()
}
}
}
}
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = ContentViewModel()
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 33.4, longitude: -117.4), span:
MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
#Published var address = ""
#Published var location = CLLocation.init(latitude: 33, longitude: -117)
private var lastResolved : CLLocation = .init()
var locationManager: CLLocationManager?
func checkIfLocationServicesisEnabled() {
if CLLocationManager.locationServicesEnabled() {
locationManager = CLLocationManager()
locationManager?.startUpdatingLocation()
locationManager?.delegate = self
//locationManager?.desiredAccuracy = kCLLocationAccuracyBest
} else {
print("this is off turn in the settings...")
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let last = locations.last else { return }
if last.distance(from: lastResolved) > 200 {
resolveLocationName(with: last) { address in
self.address = address
self.lastResolved = last
}
}
self.location = last
}
public func resolveLocationName(with location: CLLocation, completion: #escaping ((String) -> Void)) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: .current) { placemarks, error in
guard let place = placemarks?.first, error == nil else {
completion("")
return
}
print(place)
var name = ""
if let locality = place.subThoroughfare {
name += locality
}
if let street = place.thoroughfare {
name += " \(street)"
}
if let city = place.locality {
name += " \(city)"
}
if let adminRegion = place.administrativeArea {
name += ", \(adminRegion)"
}
if let zipCode = place.postalCode {
name += " \(zipCode)"
}
completion(name)
}
}
private func checkLocationAuth() {
guard let locationManager = locationManager else { return }
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
print("location is restricted")
case .denied:
print("location is denied in settings for app")
case .authorizedAlways, .authorizedWhenInUse:
region = MKCoordinateRegion(center: locationManager.location!.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
self.location = CLLocation(latitude: region.self.center.latitude, longitude: region.self.center.longitude)
print(location)
#unknown default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuth()
}
}
Note that I'm calling startUpdatingLocation() and implementing didUpdateLocations -- these will allow you to get/update the user's location.
In the simulator, you can use the Features->Location menu to simulate different spots. You'll see it resolve different spots and show the address at the top of the screen.
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()
}
}