Handle SwiftUI and CoreLocation with MVVM-Pattern - swift

i am trying to implement SwiftUI and CoreLocation with the MVVM-Pattern. My LocationManager as Helper works fine. But how I can change the properties of my LocationViewModel? I am implemented my #ObservedObject of the LocationManager in LocationViewModel. Here is my problem.
I don't have a idea to implement properties they change on the fly. Nothing is changed in my LocationView. By pressing a Button anything works fine one time. But the LocationViewModel must change there properties on every change of the LocationManager.
In summary I would like to display the current user position.
// Location Manager as Helper
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
let locationManager = CLLocationManager()
let geoCoder = CLGeocoder()
#Published var location: CLLocation?
#Published var placemark: CLPlacemark?
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
func geoCode(with location: CLLocation) {
geoCoder.reverseGeocodeLocation(location) { (placemark, error) in
if error != nil {
print(error!.localizedDescription)
} else {
self.placemark = placemark?.first
}
}
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
DispatchQueue.main.async {
self.location = location
self.geoCode(with: location)
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
// TODO
}
}
// Location Model
import Foundation
import CoreLocation
struct Location {
var location: CLLocation = CLLocation()
var placemark: CLPlacemark = CLPlacemark()
}
// Location View Model
import SwiftUI
import CoreLocation
class LocationViewModel: ObservableObject {
#ObservedObject var locationManager: LocationManager = LocationManager()
#Published var location: Location
init() {
self.location = Location()
}
}
// Location View
import SwiftUI
struct LocationView: View {
#ObservedObject var locationViewModel: LocationViewModel = LocationViewModel()
var body: some View {
VStack(alignment: .leading) {
Text("Latitude: \(self.locationViewModel.location.location.coordinate.latitude.description)")
Text("Longitude: \(self.locationViewModel.location.location.coordinate.longitude.description)")
}
}
}
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
LocationView()
}
}
Update
Now, I have set up my MapView.
But how I can receive the data of my LocationManager? The didUpdateLocations method is working in LocationManager.
All what I am trying to do goes wrong. I would like to set the region on my MapView based on the current user location. In UIKit it was very simple, but in SwiftUI it is freaky.
// Map View
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
#ObservedObject var locationManager: LocationManager = LocationManager()
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ control: MapView) {
self.parent = control
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView(frame: .zero)
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.showsUserLocation = true
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}

SwiftUI 2
Use instead StateObject in this case
struct LocationView: View {
#StateObject var locationManager: LocationManager = LocationManager()
...
SwiftUI 1
Actually LocationViewModel is redundant here. As your LocationManager is a ObservableObject you can use it directly in your view, as below:
struct LocationView: View {
#ObservedObject var locationManager: LocationManager = LocationManager()
var body: some View {
VStack(alignment: .leading) {
Text("Latitude: \(locationManager.location.coordinate.latitude.description)")
Text("Longitude: \(locationManager.location.coordinate.longitude.description)")
}
}
}

Related

Get users current location and upload it to firebase

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.

Passing Data between Delegate and ViewModel ObservableObjects

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 { ... }

SwiftUI Showing Specific Landmarks on Opening the map

I was wondering to see if there is a way to show the nearest specific landmarks[ie railway stations] just to see where the user can go to? I am trying to also show the point to point of landmarks but should revolve first around the user. top 3 nearest landmarks should do, please any help or resources?
import MapKit
import SwiftUI
import CoreLocation
struct SearchView: View {
var body: some View {
Home()
}
}
struct SearchView_Previews: PreviewProvider {
static var previews: some View {
SearchView()
}
}
struct Home: View {
#State var map = MKMapView()
#State var manager = CLLocationManager()
#State var alert = false
#State var source : CLLocationCoordinate2D!
#State var destination : CLLocationCoordinate2D!
var body: some View{
ZStack(alignment: .bottom){
VStack(spacing:0){
HStack{
Text("Station Search").font(.title)
Spacer()
}
.padding()
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top).background(Color.white)
MapView(map: self.$map, manager: self.$manager, alert:
self.$alert, source: self.$source,
destination: self.$destination)
.onAppear {
self.manager.requestAlwaysAuthorization()
}
}
}.edgesIgnoringSafeArea(.all)
.alert(isPresented: self.$alert) { () -> Alert in
Alert(title: Text("Error"), message: Text("Please Enable Location In Setting !!!"), dismissButton: .destructive(Text("Ok")))
}
}
}
struct MapView: UIViewRepresentable {
#Binding var map: MKMapView
#Binding var manager : CLLocationManager
#Binding var alert : Bool
#Binding var source : CLLocationCoordinate2D!
#Binding var destination : CLLocationCoordinate2D!
func makeCoordinator() -> Coordinator {
return MapView.Coordinator(parent1: self)
}
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
manager.delegate = context.coordinator
map.showsUserLocation = true
return map
}
func updateUIView(_ uiView: MKMapView, context: Context){
}
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
var parent : MapView
init(parent1 : MapView) {
parent = parent1
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .denied {
self.parent.alert.toggle()
}
else{
self.parent.manager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
let region = MKCoordinateRegion(center: locations.last!.coordinate, latitudinalMeters: 1000, longitudinalMeters: 1000)
self.parent.source = locations.last!.coordinate
self.parent.map.region = region
}
}
}

SwfitUI navigationBarItems calls location permission twice

I created a button under navigationBarItems, and this button opens a new sheet, and the new sheet will pop up a window to ask user locations. However, in the new sheet, CLLocationManager() has been called twice, and location permission pop up window will disappear in a few seconds. When you create a regular button, the location pop up windown will be stay there until you select one of the options, and CLLocationManager() will only be called one time.
Code
ConentView.swift
import SwiftUI
struct ContentView: View {
#State var show = false
#State var showEditPage = false
var body: some View {
NavigationView {
List {
Text("Text")
Button("Location button") {
print("Location button tapped")
self.show.toggle()
}.sheet(isPresented: $show) {
NewPage()
}
}
.navigationBarItems(
trailing:
VStack {
Button(action: {
print("BarItemButton tapped")
self.showEditPage.toggle()
}) {
//Top right icon
Text("BarItemButton")
}.sheet(isPresented: $showEditPage) {
//Open sheet page
NewPage()
}
}//End of trailing VStack
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NewPage.swift
import SwiftUI
struct NewPage: View {
#ObservedObject var locationManager = LocationManager()
var body: some View {
Text("New Page")
}
}
struct NewPage_Previews: PreviewProvider {
static var previews: some View {
NewPage()
}
}
LocationManager.swift
import SwiftUI
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
let objectWillChange = PassthroughSubject<Void, Never>()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
print("In LocationManger.swift #initi, this is called")
}
#Published var locationStatus: CLAuthorizationStatus? {
willSet {
objectWillChange.send()
}
}
#Published var lastLocation: CLLocation? {
willSet { objectWillChange.send() }
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.locationStatus = status
print("In LocationManger.swift #Func locationManager, Status is updaing")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.lastLocation = location
print("Location is updating")
}
}
GitHub
You can feel free to download the project to try it on your laptop to see the issue:
Github Example Project
Screenshot
Here are changes of possible approach to make it work in your code:
1) Make LocationManager be only one
class LocationManager: NSObject, ObservableObject {
static var defaultManager: LocationManager = {
LocationManager()
}()
...
2) Use default manager instead of creating every time SwiftUI wants to create/copy view structure
struct NewPage: View {
#ObservedObject var locationManager = LocationManager.defaultManager
...

How to get Current Location with SwiftUI?

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