currently I am playing around with swiftui, CoreData and CoreLocation.
My goal is to store waypoints and calculate their distances to the current Location while moving. These distances should be written to CoreData and shown in the UI.
I am getting location updates in the Location Managers "didUpdateLocations", but what is the smartest way to write to CoreData from here?
I thought, I could just "merge" the DataController and LocationManager into one class, but
1: I can not access CoreData in the DataController itself
2: I can not create an observed object of the LocationManager anymore, since it does not provide a fetchrequest.
I'd really appreciate a slight hint here, since I don't know what to google anymore ;)
My DataController looks like this:
import CoreData
import Foundation
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Positions")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
And my LocationManager looks like this:
import Foundation
import Combine
import CoreLocation
class LocManager: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var location: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)
#Published var altitude: Double = 0
#Published var degrees: Double = 0
private let locationManager: CLLocationManager
override init() {
//LocationManager INIT
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
self.setup()
}
private func setup() {
// set accuracy
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
//update filter
//self.locationManager.headingFilter = kCLHeadingFilterNone //High Power usage
self.locationManager.headingFilter = 0.5
self.locationManager.distanceFilter = 1
if CLLocationManager.headingAvailable() {
self.locationManager.startUpdatingLocation()
self.locationManager.startUpdatingHeading()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
self.degrees = -1 * newHeading.magneticHeading
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location = locations.first!.coordinate
altitude = locations.first!.altitude
/*
I cant access CoreData here, can I?
*/
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch locationManager.authorizationStatus {
case .authorizedWhenInUse:
locationManager.requestWhenInUseAuthorization()
break
case .restricted:
break
case .denied:
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
break
default:
break
}
}
}
Thanks for your help!
Related
Hello
I want to make an app to catch runrise and goldenhour in SwiftUI.
I don't have errors, but it doesn't work neither.
The result is saying the following:
Current Location Unknown
Time Zone Unknown
I use the SunKit package Dependency.
I don't get an error.
Dependency used is SunKit:
https://github.com/Sunlitt/SunKit
Help is appreciated. As it doesn't work
Normally I should have the core location to fill in the location and specify the hour.
But it aint happening.
I don't know what is happening.
import SwiftUI
import SunKit
import CoreLocation
class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
var contentView: ContentView?
var locationManager = CLLocationManager()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
if let contentView = contentView {
do {
try contentView.sun.setLocation(location)
contentView.location = location
} catch let error {
print(error)
}
}
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if let error = error {
print(error)
return
}
if let placemarks = placemarks, let placemark = placemarks.first, let timeZone = placemark.timeZone {
if let contentView = self.contentView {
contentView.timeZone = timeZone
print("TimeZone: \(timeZone.identifier) offset : \(timeZone.secondsFromGMT())")
do { try contentView.sun.setTimeZone(Double(timeZone.secondsFromGMT()) / 3600.0)
} catch let error {
print(error)
}
}
}
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
}
}
}
struct ContentView: View {
let sun: Sun = Sun.common
let timeFormatter: DateFormatter = {
let tf = DateFormatter()
tf.dateFormat = "HH:mm"
return tf
}()
#State var locationManager = CLLocationManager()
#State var locationManagerDelegate = LocationManagerDelegate()
#State public var location: CLLocation?
#State public var timeZone: TimeZone?
init() {
locationManagerDelegate.contentView = self
locationManager.delegate = locationManagerDelegate
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
}
var body: some View {
VStack {
Text("Azimuth: \(sun.azimuth)°")
Text("Elevation: \(sun.elevation.degrees)°")
Text("Sunrise time: \(timeFormatter.string(from: sun.sunrise))")
if let location = location {
Text("Current Location: \(location.coordinate.latitude), \(location.coordinate.longitude)")
} else {
Text("Current Location: Unknown")
}
if let timeZone = timeZone {
Text("Time Zone: \(timeZone.abbreviation() ?? "Unknown")")
} else {
Text("Time Zone: Unknown")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Stil more details needed ?
I am using Xcode 12.
I am able to show a map with a region, but with hard-coded values.
Instead, I want to set the region of the map based on the user's current location.
I have a LocationManager class, which gets the user's location and publish it.
I have a ShowMapView SwiftUI View that observes an object based on the LocationManager class to get the user's location.
But, I don't know how to use the data from the locationManager object to set the region used by the map.
Here is the LocationManager class, which gets the user's location and publishes it.
import Foundation
import MapKit
final class LocationManager: NSObject, ObservableObject {
#Published var location: CLLocation?
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLDistanceFilterNone
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
self.location = location
}
}
}
Here is the ShowMapView SwiftUI View, which needs to get the user's location that's published and set the region used by the map. As you can see, the values are hard-coded for now.
import Combine
import MapKit
import SwiftUI
struct AnnotationItem: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
struct ShowMapView: View {
#ObservedObject private var locationManager = LocationManager()
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 38.898150, longitude: -77.034340),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
var body: some View {
Map(coordinateRegion: $region, annotationItems: [AnnotationItem(name: "Home", coordinate: CLLocationCoordinate2D(latitude: self.locationManager.location!.coordinate.latitude, longitude: self.locationManager.location!.coordinate.longitude))]) {
MapPin(coordinate: $0.coordinate)
}
.frame(height: 300)
}
}
Here's one possible solution to this:
final class LocationManager: NSObject, ObservableObject {
#Published var location: CLLocation?
#Published var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 38.898150, longitude: -77.034340),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
private var hasSetRegion = false
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLDistanceFilterNone
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
self.location = location
if !hasSetRegion {
self.region = MKCoordinateRegion(center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
hasSetRegion = true
}
}
}
}
struct ShowMapView: View {
#ObservedObject private var locationManager = LocationManager()
var homeLocation : [AnnotationItem] {
guard let location = locationManager.location?.coordinate else {
return []
}
return [.init(name: "Home", coordinate: location)]
}
var body: some View {
Map(coordinateRegion: $locationManager.region, annotationItems: homeLocation) {
MapPin(coordinate: $0.coordinate)
}
.frame(height: 300)
}
}
In this solution, the region is published by the location manager. As soon as a location is received, the region is centered on that spot (in didUpdateLocations). Then, a boolean flag is set saying the region has been centered initially. After that boolean is set, it no longer updates the region. This will let the user still drag/zoom, etc.
I also changed your code for putting down the pin a little bit. You were force-unwrapping location, which is nil until the first location is set by the location manager, causing a crash. In my edit, it just returns an empty array of annotation items if there isn't a location yet.
I want to build a view with a map centered on the user location when loaded. I managed to build this, but sometimes the map loads with latitude 0, longitude: 0. This happens when I move too fast between views (there are other views in the project besides the map).
It feels like the user location is loaded too slow and the Map appears with default coordinates, but I really have no idea what I'm doing wrong. Any ideas?
Map view:
import SwiftUI
import MapKit
struct MapView: View {
#StateObject var locationManager = LocationManager()
#State var trackingMode: MapUserTrackingMode = .follow
var body: some View {
Map(coordinateRegion: $locationManager.region, interactionModes: .all, showsUserLocation: true, userTrackingMode: $trackingMode)
}
}
Location View Model:
import SwiftUI
import CoreLocation
import MapKit
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion()
private let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locations.last.map {
let center = CLLocationCoordinate2D(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude)
let span = MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
region = MKCoordinateRegion(center: center, span: span)
}
}
}
That is exactly your problem. Location data will ALWAYS lag, just like any other retrieved data. What you need to consider is a mechanism to update your views when you get updates.
The best way to do that is to import Combine in your LocationManager class and use a PassthroughSubject like this:
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var region = MKCoordinateRegion() {
willSet { objectWillChange.send() }
}
That allows you to subscribe to your publisher in the map and get updates. You will find many tutorials regarding this.
I am building a SwiftUI app that shows data based on user lat/long. I have based my code off of this sample provided by the framework dev.
With SwiftUI I have my LocationManager set as:
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate{
#Published var userLatitude: Double = 0.0
#Published var userLongitude: Double = 0.0
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.distanceFilter = 100.0
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
userLatitude = location.coordinate.latitude
userLongitude = location.coordinate.longitude
print("Hello I'm here! \(location)")
}
}
Whenever I go back to my ContentView and try to read the Lat/Long it just shows up as 0.0. but if I output them within the body the values show up correctly.
struct ContentView: View {
#State private var times = prayerTimes()
#ObservedObject var locationViewModel = LocationViewModel()
var body: some View {
NavigationView {
PrayerTimeView(times: $times)
.navigationBarTitle(Text("Prayer Times"))
}
}
static func prayerTimes() -> PrayerTimes? {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: locationViewMode.userLatitude, longitude: locationViewMode.userLongitude)
var params = CalculationMethod.moonsightingCommittee.params
params.madhab = .hanafi
return PrayerTimes(coordinates: coordinates, date: date, calculationParameters: params)
}
}
prayerTimes() only call once when you init the view. Why don't you make times as a #Published of your ViewModel. When location changes, just update that value.
PrayerTimeView(times: $viewmodel.times)
.navigationBarTitle(Text("Prayer Times"))
I'm struggling with this for a long time without finding where I'm wrong (I know I'm wrong).
I have one API call with the location of the phone (this one is working), but I want the same API call with a manual location entered by a textfield (using Geocoding for retrieving Lat/Long). The geocoding part is ok and updated but not passed in the API call.
I also want this API call to be triggered when the TextField is cleared by the dedicated button back with the phone location.
Please, what am I missing? Thanks for your help.
UPDATE: This works on Xcode 12.2 beta 2 and should work on Xcode 12.0.1
This is the code:
My Model
import Foundation
struct MyModel: Codable {
let value: Double
}
My ViewModel
import Foundation
import SwiftUI
import Combine
final class MyViewModel: ObservableObject {
#Published var state = State.ready
#Published var value: MyModel = MyModel(value: 0.0)
#Published var manualLocation: String {
didSet {
UserDefaults.standard.set(manualLocation, forKey: "manualLocation")
}
}
#EnvironmentObject var coordinates: Coordinates
init() {
manualLocation = UserDefaults.standard.string(forKey: "manualLocation") ?? ""
}
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
private var url: URL {
get {
return URL(string: "https://myapi.com&lat=\(coordinates.latitude)&lon=\(coordinates.longitude)")!
}
}
let urlSession = URLSession.shared
var dataTask: AnyPublisher<MyModel, Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load(){
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("⚠️ API Call finished")
break
case let .failure(error):
print("❌ API Call failure")
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.value = value
print("👍 API Call loaded")
}
))
}
}
The Location Manager
import Foundation
import SwiftUI
import Combine
import CoreLocation
import MapKit
final class Coordinates: NSObject, ObservableObject {
#EnvironmentObject var myViewModel: MyViewModel
#Published var latitude: Double = 0.0
#Published var longitude: Double = 0.0
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
deinit {
locationManager.stopUpdatingLocation()
}
}
extension Coordinates: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
self.placemark = places?[0]
})
self.locationManager.stopUpdatingLocation()
}
}
extension Coordinates {
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
}
The View
import Foundation
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
#EnvironmentObject var coordinates: Coordinates
private var icon: Image { return Image(systemName: "location.fill") }
var body: some View {
VStack{
VStack{
Text("\(icon) \(coordinates.placemark?.locality ?? "Unknown location")")
Text("Latitude: \(coordinates.latitude)")
Text("Longitude: \(coordinates.longitude)")
}
VStack{
Text("UV Index: \(myViewModel.value.value)")
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
HStack{
TextField("Manual location", text: $myViewModel.manualLocation)
if !myViewModel.manualLocation.isEmpty{
Button(action: { clear() }) { Image(systemName: "xmark.circle.fill").foregroundColor(.gray) }
}
}
}.padding()
}
func commit() {
coordinates.getLocation(from: self.myViewModel.manualLocation) { places in
coordinates.latitude = places?.latitude ?? 0.0
coordinates.longitude = places?.longitude ?? 0.0
}
myViewModel.load()
}
func clear() {
myViewModel.manualLocation = ""
myViewModel.load()
}
}