In the following code, I have a LocationManager class which provides the city name of the current location via the #Published property wrapper lastSearchedCity.
Then I have a SearchManagerViewModel class that should be in charge of presenting the city name on SwiftUI views based on some conditions (not currently shown in the code below) via the #Published property wrapper cityName. It properly shows the city name when I call the searchAndSetCity() method from ContentView.swift inside an onAppear modifier.
My issue is that if the user turned Location Services off and turns it back On while he/she is in the ContentView.swift the Text view doesn't update, which is understandable since the searchAndSetCity() method would need to be called again.
How can I call the searchAndSetCity() method located inside the SearchManagerViewModel class every time the locationManagerDidChangeAuthorization(_ manager: CLLocationManager) method is called? I believed this method is called every time the authorization status changes.
LocationManager Class
final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
#Published var lastSearchedCity = ""
var hasFoundOnePlacemark:Bool = false
func checkIfLocationServicesIsEnabled(){
DispatchQueue.global().async {
if CLLocationManager.locationServicesEnabled(){
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest/// kCLLocationAccuracyBest is the default
self.checkLocationAuthorization()
}else{
// show message: Services desabled!
}
}
}
private func checkLocationAuthorization(){
switch locationManager.authorizationStatus{
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
// show message
case .denied:
// show message
case .authorizedWhenInUse, .authorizedAlways:
/// app is authorized
locationManager.startUpdatingLocation()
default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
hasFoundOnePlacemark = false
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
if error != nil {
self.locationManager.stopUpdatingLocation()
// show error message
}
if placemarks!.count > 0 {
if !self.hasFoundOnePlacemark{
self.hasFoundOnePlacemark = true
let placemark = placemarks![0]
self.lastSearchedCity = placemark.locality ?? ""
}
self.locationManager.stopUpdatingLocation()
}else{
// no places found
}
})
}
}
SearchManagerViewModel Class
class SearchManagerViewModel: ObservableObject{
#Published var cityName = "" // use this directly in SwifUI views
#ObservedObject private var locationManager = LocationManager()
// Call this directly fron onAppear in SwiftUI views
// This method is more complex than what is shown here. It handles other things like HTTP requests etc.
func searchAndSetCity(){
locationManager.checkIfLocationServicesIsEnabled()
self.cityName = locationManager.lastSearchedCity
}
}
ContentView.swift
struct ContentView: View {
#StateObject private var searchManager = SearchManagerViewModel()
var body: some View {
VStack {
Text(searchManager.cityName)
.font(.callout)
}
.onAppear{
searchManager.searchAndSetCity()
}
}
}
Related
I am trying to get the users current location in swifti and then upload it to firebase, but nothing seems to be uploaded
I tried adding print commands to the code to check whether I was getting the location but nothing was printed to the terminal, here is my code:
import MapKit
import FirebaseFirestore
struct Maps: View {
#State private var location = CLLocationCoordinate2D()
var body: some View {
Text("Hello World")
.onAppear {
let manager = LocationManager()
manager.getLocation { location in
self.location = location
print("Latitude: \(location.latitude), Longitude: \(location.longitude)")
}
}
}
}
class LocationManager: NSObject, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
func getLocation(completion: #escaping (CLLocationCoordinate2D) -> ()) {
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
self.completion = completion
}
private var completion: ((CLLocationCoordinate2D) -> ())?
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let db = Firestore.firestore()
let locationRef = db.collection("locations").document()
locationRef.setData([
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"identifier": UUID().uuidString
])
locationManager.stopUpdatingLocation()
completion?(location.coordinate)
}
}
struct Maps_Previews: PreviewProvider {
static var previews: some View {
Maps()
}
}
I added Privacy - Request when in use.. to info, so I don’t know what the problem is, I am not getting any errors
This code
.onAppear {
let manager = LocationManager()
// ...
}
creates and then immediately discards a LocationManager, which means that the object does not live long enough in memory to even receive one callback to the delegate method.
Move manager up to be a propery of Maps to let it stay in memory as long as the view itself.
In the following code, I'm trying to call a method located inside the LocationManagerViewModel class when the locationManagerDidChangeAuthorization is called from inside the LocationManager class via delegation, but nothing happens when it gets called, I only see the debug message Calling method locationManagerDidChangeAuthorization but the method locationManagerDelegateComunicationMethod inside the LocationManagerViewModel never gets called.
Any idea what could be wrong?
Location Manger Class; Sender
protocol LocationManagerDelegate {
func locationManagerDelegateComunicationMethod()
}
final class LocationManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
var delegate: LocationManagerDelegate?
var hasFoundOnePlacemark:Bool = false
func checkIfLocationServicesIsEnabled(){
DispatchQueue.global().async {
if CLLocationManager.locationServicesEnabled(){
self.locationManager.delegate = self
self.checkLocationAuthorization()
}else{
print("You have Location Services DESABLED!")
}
}
}
private func checkLocationAuthorization(){
switch locationManager.authorizationStatus{
case .notDetermined:
print("notDetermined")
case .restricted:
print("restricted")
case .denied:
print("denied")
case .authorizedWhenInUse, .authorizedAlways:
print("authorizedWhenInUse")
default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
print("Calling method locationManagerDidChangeAuthorization")
var delegate: LocationManagerDelegate? = LocationManagerViewModel()// I tried with and without this line
delegate?.locationManagerDelegateComunicationMethod()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
hasFoundOnePlacemark = false
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
if error != nil {
self.locationManager.stopUpdatingLocation()
}
if placemarks!.count > 0 {
if !self.hasFoundOnePlacemark{
self.hasFoundOnePlacemark = true
self.locationManager.stopUpdatingLocation()
}else{
self.noPlacesFoundAlertView()
}
})
}
}
LocationManagerViewModel Class; Receiver
class LocationManagerViewModel: ObservableObject, LocationManagerDelegate{
// some #Published properties here
private var locationManager = LocationManager()
func locationManagerDelegateComunicationMethod() {
print("Received message from the LocationManager class!")
// do something...
}
}
Thanks
The delegate is never assigned in LocationManagerViewModel. You probably need to assign it in an init:
class LocationManagerViewModel: ObservableObject, LocationManagerDelegate{
// some #Published properties here
private var locationManager = LocationManager()
init(){
locationManager.delegate = self
}
func locationManagerDelegateComunicationMethod() {
print("Received message from the LocationManager class!")
// do something...
}
}
I have an app built using SwiftUI that works with user location. Using online tutorials, I have come up with a class LocationManager that handles the request using the delegate method and has an attribute #Published that sets the location as soon as it receives it.
I also have a ViewModel that has a function getData(location: CLLocation) that will properly update my view after an async call to a different API.
My question is, what is the best way to connect the LocationManager with the ViewModel, so that as soon as the LocationManager gets the location using the delegate it automatically calls the getData() function with that value?
I've tried to come up with a few solutions on my own, such as passing a closure to the LocationManager to call viewModel.getData() when the delegate is updated, but I got an issue with the "closure capturing a mutating self parameter". Any help would be greatly appreciated!!
Here is the code in question:
final class LocationManager: NSObject, ObservableObject {
#Published var location: CLLocation?
private let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
DispatchQueue.main.async {
self.location = location
}
}
}
class ViewModel: ObservableObject {
#Published dataArray = [Model]()
func getData(location: CLLocation) {
// async api call
// update dataArray for view in completion handler
}
}
struct ShowData: View {
// initialize LocationManager
#StateObject var locationManager = LocationManager()
// initialize ViewModel
#StateObject var viewModel = ViewModel()
var body: some View {
// show dataArray
}
}
You can own the LocationManager in your view model:
class ViewModel: ObservableObject {
#Published dataArray = [Model]()
var lm = LocationManager()
}
Then, you could architect the LocationManager to take a separate delegate (which could be the view model), or, you could use Combine to listen for changes on the #Published property on the LocationManager:
cancellable = lm.$location.sink { ... }
My requirement is when the location is disabled, In UI there is a feature "Enabled Location" when the user clicks Enabled location, it will go to setting screen. That is fine working. After changing the setting, and user comes back to app, it should refresh it. This is not working. Not refreshing the app again
My code :
struct MainView: View {
//MARK:- ObservedObject variable
#ObservedObject private var inStoreVM = InStoreViewModal()
#ObservedObject var locationManager = LocationManager()
//MARK: Location coordinate
var userLatitude: String {
return "\(locationManager.lastLocation?.coordinate.latitude ?? 0)"
}
var userLongitude: String {
return "\(locationManager.lastLocation?.coordinate.longitude ?? 0)"
}
var body: some View {
NavigationView {
ZStack {
TabView(selection: $selectedTab) {
HomeView()
.tabItem {
Image("home").renderingMode(.template)
Text("Home")
}.tag(0)
ExploreView(inStoreVM: inStoreVM)
.tabItem {
Image("search").renderingMode(.template)
Text("Explore")
}.tag(1)
.onAppear() {
print("----onAppear-----TAB")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
print("----Moving back to the foreground!")
locationManager.enableMyAlwaysFeatures()
}
}}}
This is my location manager class
class LocationManager: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
private var locationManager = CLLocationManager()
func registerForLocationUpdates() {
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
}
override init() {
super.init()
self.locationManager.delegate = self
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled(){
locationManager.startUpdatingLocation()
}
}
#Published var locationStatus: CLAuthorizationStatus? {
willSet {
objectWillChange.send()
}
}
#Published var lastLocation: CLLocation? {
willSet {
objectWillChange.send()
}
}
var statusString: String {
guard let status = locationStatus else {
return "unknown"
}
switch status {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
guard let location = locations.first else { return }
lastLocation = location
self.lastLocation = location
print(#function, location)
print("location = \(location.coordinate.latitude) \(location.coordinate.longitude)")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
enableMyAlwaysFeatures()
self.locationStatus = status
print(#function, statusString)
}
func enableMyAlwaysFeatures() {
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
}
Any idea, how can I refresh/reload the app, when the user comes from setting screen
I appreciate any advice or help.
As already mentioned, property observers didSet and willSet don't "work" on #Published, since you are simply observing changes to the Published<...> object defining the property wrapper, not the wrappedValue ... itself.
There are two possible approaches.
First
You can use Combine to subscribe to changes of your #Published properties.
Example:
private var cancellables = Set<AnyCancellable>()
init() {
$locationStatus
.sink { _ in objectWillChange.send() }
.store(in: &cancellables)
$lastLocation
.sink { _ in objectWillChange.send() }
.store(in: &cancellables)
}
This would be equivalent to willSet.
Don't forget to import Combine.
Second
You call objectWillChange.send() manually right after you change the properties.
For example in your locationManager(_ , didChangeAuthorization):
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
enableMyAlwaysFeatures()
self.locationStatus = status
self.objectWillChange.send() // <- Here
print(#function, statusString)
}
Trying to get current location with using swiftUI. Below code, couldn't initialize with didUpdateLocations delegate.
class GetLocation : BindableObject {
var didChange = PassthroughSubject<GetLocation,Never>()
var location : CLLocation {
didSet {
didChange.send(self)
}
}
init() {}
}
This code below works (Not production ready). Implementing the CLLocationManagerDelegate works fine and the lastKnownLocation is updated accordingly.
Don't forget to set the NSLocationWhenInUseUsageDescription in your Info.plist
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var lastKnownLocation: CLLocation?
func startUpdating() {
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
lastKnownLocation = locations.last
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
manager.startUpdatingLocation()
}
}
}
I have written a one-file swift package with usage instructions on https://github.com/himbeles/LocationProvider. It provides a ObservableObject-type wrapper class for CLLocationManager and its delegate.
There is a #Published property location which can directly be used in SwiftUI, as well as a PassthroughSubject<CLLocation, Never> called locationWillChange that you can subscribe to via Combine. Both update on every didUpdateLocations event of the CLLocationManager.
It also handles the case where location access has previously been denied: The default behavior is to present the user with a request to enable access in the app settings and a link to go there.
In SwiftUI (> iOS 14, > macOS 11), use as
import SwiftUI
import LocationProvider
struct ContentView: View {
#StateObject var locationProvider = LocationProvider()
var body: some View {
VStack{
Text("latitude \(locationProvider.location?.coordinate.latitude ?? 0)")
Text("longitude \(locationProvider.location?.coordinate.longitude ?? 0)")
}
.onAppear {
do {try locationProvider.start()}
catch {
print("No location access.")
locationProvider.requestAuthorization()
}
}
}
}
As of Xcode 11 beta 4, you will need to change didChange to willChange:
var willChange = PassthroughSubject<LocationManager, Never>()
var lastKnownLocation: CLLocation? {
willSet {
willChange.send(self)
}
}