Implementing Location Protocol - swift

I Liked the concept of Protocol Oriented Programming and I am trying to start writing protocols by Protocol Extensions in swift.
I am trying to bring device location using Protocol Extensions.
However I am facing problems on creating that as the following:
import Foundation
import CoreLocation
protocol Locator:CLLocationManagerDelegate {
var locationManager:CLLocationManager!
{ get set }
var locationHandler: ((CLLocation)->())?
{ get set }
func getLocation(completionHandler:#escaping (CLLocation)->())
}
extension Locator {
private var _locationManager:CLLocationManager {
get {return self.locationManager} set {self.locationManager = newValue}
}
private var _locationHandler:((CLLocation)->())? {
get {return self.locationHandler} set {self.locationHandler = newValue}
}
func getLocation(completionHandler:#escaping (CLLocation)->()) {
self.locationManager = CLLocationManager()
self.locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
let status = CLLocationManager.authorizationStatus()
switch status {
case .notDetermined:
self.locationManager.requestAlwaysAuthorization()
return
case .denied, .restricted:
return
case .authorizedAlways, .authorizedWhenInUse:
self.locationManager.startUpdatingLocation()
#unknown default:
break
}
self.locationHandler = completionHandler
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse || status == .authorizedAlways {
self.locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("Hey this is a location")
if let currentLocation = locations.last {
self._locationHandler?(currentLocation)
}
}
}
However the completion handler is not working properly.
what is my problem and How can I proceed creating this in Protocols concept.

Related

SwiftUI refresh the app when user change the location setting in device setting option

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)
}

Swift func to provide current CLLocation (with closures NOT delegates)

I would like to create an Swift func like that:
static func queryLocation(desiredAccuracy: CLLocationAccuracy, completion: #escaping (Result<CLLocation, Error>)->())
This func would be as simple as it can get. As caller, you don't need to hold on to your location manager and you don't need to implement any delegate methods. Just ask for the location and implement the completion handler.
(original source code removed and added as answer, because it actually worked - I just had issues calling it properly)
Sorry everyone. There was a problem with the calling method. The OneTimeLocation class is actually working as expected. (also available as Swift Package: https://github.com/hoereth/CoreLocationUtils/blob/main/Sources/CoreLocationUtils/OneTimeLocation.swift)
import Foundation
import CoreLocation
public class OneTimeLocation : NSObject, CLLocationManagerDelegate {
public enum LocationError : Error {
case denied
case restricted
case timeout
case unknown
}
let manager: CLLocationManager
let completion: (Result<CLLocation, Error>)->()
let timeout: TimeInterval
fileprivate static let instancesQueue = DispatchQueue(label: "OneTimeLocation.instances")
fileprivate static var instances = Set<OneTimeLocation>()
/// Will either find you the current location or produce an error.
/// - Parameters:
/// - desiredAccuracy: see CLLocationManager.desiredAccuracy
/// - timeout: Applies to actual finding a location. Dialogs are presented without timeout.
public static func queryLocation(desiredAccuracy: CLLocationAccuracy, timeout: TimeInterval, completion: #escaping (Result<CLLocation, Error>)->()) {
let oneTimeLocation = OneTimeLocation(desiredAccuracy: desiredAccuracy, completion: completion, timeout: timeout)
oneTimeLocation.manager.delegate = oneTimeLocation
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways, .authorizedWhenInUse:
instancesQueue.sync {
_ = instances.insert(oneTimeLocation)
oneTimeLocation.manager.startUpdatingLocation()
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
oneTimeLocation.manager.stopUpdatingLocation()
completion(Result.failure(LocationError.timeout))
oneTimeLocation.removeInstance()
}
}
case .notDetermined:
instancesQueue.sync {
_ = instances.insert(oneTimeLocation)
oneTimeLocation.manager.requestWhenInUseAuthorization()
}
case .denied:
completion(Result.failure(LocationError.denied))
case .restricted:
completion(Result.failure(LocationError.restricted))
#unknown default:
completion(Result.failure(LocationError.unknown))
}
}
fileprivate init(desiredAccuracy: CLLocationAccuracy, completion: #escaping (Result<CLLocation, Error>)->(), timeout: TimeInterval) {
self.manager = CLLocationManager()
self.manager.desiredAccuracy = desiredAccuracy
self.completion = completion
self.timeout = timeout
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if (locations.count > 0) {
self.completion(Result.success(locations[0]))
} else {
self.completion(Result.failure(LocationError.unknown))
}
self.manager.stopUpdatingLocation()
self.removeInstance()
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
self.completion(Result.failure(error))
self.manager.stopUpdatingLocation()
self.removeInstance()
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways, .authorizedWhenInUse:
self.manager.startUpdatingLocation()
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
self.manager.stopUpdatingLocation()
self.completion(Result.failure(LocationError.timeout))
self.removeInstance()
}
case .notDetermined:
break;
case .denied:
completion(Result.failure(LocationError.denied))
self.removeInstance()
case .restricted:
completion(Result.failure(LocationError.restricted))
self.removeInstance()
#unknown default:
completion(Result.failure(LocationError.unknown))
self.removeInstance()
}
}
fileprivate func removeInstance() {
Self.instancesQueue.sync {
_ = OneTimeLocation.instances.remove(self)
}
}
}

Swift: Get Current User Coordinates and Store them into a Variable

I am currently trying to get the current coordinates of the user and ultimately store those values into variables.
I have created the following class to define the users current location and set up functions to pull data.
import Foundation
import CoreLocation
class MyCurrentCoordinate: NSObject {
private var currentLocation: CLLocation!
var myLatitude = 0.0
var myLongitude = 0.0
var myAltitude = 0.0
override init() {
super.init()
}
func getLat() {
myLatitude = currentLocation.coordinate.latitude
}
func getLong() {
myLongitude = currentLocation.coordinate.longitude
}
func getAlt() {
myAltitude = currentLocation.altitude
}
}
This does not show any errors. However, when I go to call any function (getLat, getLong, or getAlt) to pull a piece of the users location data, the app crashes due the value being nil. Does anyone have any insight as to why the actual user lat, long, or altitude is not being passed?
I have the location permission and info.plist updated to allow the user to give location tracking permission.
import Foundation
import CoreLocation
import UIKit
public protocol LocalizationHelperDelegate: class {
func didUpdateLocation(_ sender: CLLocation)
}
public class LocalizationHelper: NSObject {
public weak var delegate: LocalizationHelperDelegate?
public static var shared = LocalizationHelper()
private lazy var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.desiredAccuracy = kCLLocationAccuracyBest
return locationManager
}()
private var currentLocation: CLLocationCoordinate2D?
public func startUpdatingLocation() {
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
public func stopUpdatingLocation() {
locationManager.stopUpdatingLocation()
}
public func getCurrentLocation() -> CLLocationCoordinate2D? {
return currentLocation
}
public func getLat() -> Double{
return currentLocation?.latitude ?? 0.0
}
public func getLon() -> Double{
return currentLocation?.longitude ?? 0.0
}
}
extension LocalizationHelper: CLLocationManagerDelegate {
public func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let location = locations.first else { return }
currentLocation = location.coordinate
print("[Update location at - \(Date())] with - lat: \(currentLocation!.latitude), lng: \(currentLocation!.longitude)")
delegate?.didUpdateLocation(location)
}
}
How to use
LocalizationHelper.shared.Start()
...
let lat = LocalizationHelper.shared.getLat()
let lon = LocalizationHelper.shared.getLon()
...
LocalizationHelper.shared.Stop()
You can use CLLocationManager
Add app capability, you can open your info.plist like source code and add:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>App requires allways tracking</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>App requires background tracking</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>App requires tracking when be in use</string>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>location</string>
<string>remote-notification</string>
</array>
Ask for authorization like locationManager.requestAlwaysAuthorization() and manage if have the correct access... CLLocationManager.authorizationStatus() == .authorizedAlways
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
var locationManager: CLLocationManager?
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.requestAlwaysAuthorization()
if CLLocationManager.authorizationStatus() == .authorizedAlways {
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
locationManager.allowsBackgroundLocationUpdates = false
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
}
extension ViewController: CLLocationManagerDelegate {
public func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let location = locations.first else { return }
print(location)
}
public func locationManager(_: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
print("Autorization status did change \(status)")
case .authorizedWhenInUse:
print("Autorization status did change \(status)")
case .authorizedAlways:
print("Autorization status did change \(status)")
case .restricted:
print("Autorization status did change \(status)")
case .denied:
print("Autorization status did change \(status)")
#unknown default:
fatalError()
}
}
}
Don't forget to stop it in some place locationManager.stopUpdatingLocation()

Why is this Singleton Location Manager class returning a nil Location?

I am trying to use the below LocationSingleton Class from this blog in my project. I like the simplicity of its usage.
You start updating location by simply calling:
LocationSingleton.sharedInstance.startUpdatingLocation()
Get the last location by simply calling:
LocationSingleton.sharedInstance.lastLocation
My intention is to start location services, get the last location so that I can then fetch users from Firebase using the location returned.
The issue is that if I call lastLocation straight after startUpdatingLocation it returns nil.
After some debugging I've found the reason is because location services are slow to start on the device and therefore when lastLocation is called the devices hasn't acquired the location yet. I would like to execute the next command as soon as the lastLocation has been recorded. How can I achieve that?
I would like to understand how the Protocol is being used?
import UIKit
import CoreLocation
protocol LocationServiceDelegate {
func locationDidUpdateToLocation(currentLocation: CLLocation)
func locationUpdateDidFailWithError(error: NSError)
}
class LocationSingleton: NSObject,CLLocationManagerDelegate {
var locationManager: CLLocationManager?
var lastLocation: CLLocation?
var delegate: LocationServiceDelegate?
static let sharedInstance:LocationSingleton = {
let instance = LocationSingleton()
return instance
}()
override init() {
super.init()
self.locationManager = CLLocationManager()
guard let locationManagers=self.locationManager else {
return
}
if CLLocationManager.authorizationStatus() == .notDetermined {
//locationManagers.requestAlwaysAuthorization()
locationManagers.requestWhenInUseAuthorization()
}
if #available(iOS 9.0, *) {
// locationManagers.allowsBackgroundLocationUpdates = true
} else {
// Fallback on earlier versions
}
locationManagers.desiredAccuracy = kCLLocationAccuracyBest
locationManagers.pausesLocationUpdatesAutomatically = false
locationManagers.distanceFilter = 0.1
locationManagers.delegate = self
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
self.lastLocation = location
updateLocation(currentLocation: location)
}
#nonobjc func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
locationManager?.requestWhenInUseAuthorization()
break
case .authorizedWhenInUse:
locationManager?.startUpdatingLocation()
break
case .authorizedAlways:
locationManager?.startUpdatingLocation()
break
case .restricted:
// restricted by e.g. parental controls. User can't enable Location Services
break
case .denied:
// user denied your app access to Location Services, but can grant access from Settings.app
break
default:
break
}
}
// Private function
private func updateLocation(currentLocation: CLLocation){
guard let delegate = self.delegate else {
return
}
delegate.locationDidUpdateToLocation(currentLocation: currentLocation)
}
private func updateLocationDidFailWithError(error: NSError) {
guard let delegate = self.delegate else {
return
}
delegate.locationUpdateDidFailWithError(error: error)
}
func startUpdatingLocation() {
print("Starting Location Updates")
self.locationManager?.startUpdatingLocation()
// self.locationManager?.startMonitoringSignificantLocationChanges()
}
func stopUpdatingLocation() {
print("Stop Location Updates")
self.locationManager?.stopUpdatingLocation()
}
func startMonitoringSignificantLocationChanges() {
self.locationManager?.startMonitoringSignificantLocationChanges()
}
}
The location manager works asynchronously and provides delegate methods to get the result.
In your class adopt LocationServiceDelegate, implement the delegate methods and set the delegate for example in viewDidLoad
func locationDidUpdateToLocation(currentLocation: CLLocation)
{
print(LocationSingleton.sharedInstance.lastLocation)
}
func locationUpdateDidFailWithError(error: NSError)
{
print(error)
}
func viewDidLoad()
super viewDidLoad()
let locationSingleton = LocationSingleton.sharedInstance
locationSingleton.delegate = self
locationSingleton.startUpdatingLocation()
}
When a location is detected, one of the delegate methods is called

Google map current location Swift

I'm trying to render a Google map with the user current location but it only renders the default location(around Europe area) no matter what I do. Since I am new to Swift, I am not sure how to debug this and want to ask here to figure out what I need to do.
Swift code
import UIKit
import GoogleMaps
class ViewController: UIViewController {
#IBOutlet weak var mapView: GMSMapView!
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
}
}
extension ViewController: CLLocationManagerDelegate {
private func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
}
}
private func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
}
}
}
Use did change autorization delegate of location manager where you should use
start updating location then
Remove private key before delegate methods and put below properties in did update location delegate methods. Remove stop update location method.
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedAlways, .authorizedWhenInUse:
self.locationManager.startUpdatingLocation()
break
case .denied, .restricted:
self.disableLocationRelatedFeatures()
break
// Do nothing otherwise.
default: break
}
}
self.mapView.isMyLocationEnabled = true
self.mapView.settings.myLocationButton = true