How to get access to underlying GMSMapView from UIRepresentable MapView - swift

In my app I have a ViewModel(MapViewModel) class, a UIRepresentable class and ContentView. I am looking for a way to get access to GMSMapView view in the ViewModel that is created as a UIRepresentable class.
ContentView.swift:
import SwiftUI
import Combine
struct ContentView: View {
#State private var selection = 0
#State private var dragOffset = CGSize.zero
#ObservedObject var mapViewModel : MapViewModel = MapViewModel()
var body: some View {
GeometryReader { geo in
TabView(selection: self.$selection) {
MapView()
.edgesIgnoringSafeArea(.all)
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}
.tag(0)
Text("Second Page")
.tabItem {
VStack {
Image(systemName: "gear")
Text("Settings")
}
}
.tag(1)
Text("Third Page")
.tabItem {
VStack {
Image(systemName: "gear")
Text("Third Page")
}
}
.tag(2)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
MapViewModel.swift:
import Foundation
import Combine
import GoogleMaps
import os
class MapViewModel: NSObject, ObservableObject {
let lm = CLLocationManager()
var myLocations = [CLLocation]()
override init() {
super.init()
lm.delegate = self
lm.desiredAccuracy = kCLLocationAccuracyBest
lm.requestWhenInUseAuthorization()
lm.pausesLocationUpdatesAutomatically = false
lm.allowsBackgroundLocationUpdates = true
lm.startUpdatingLocation()
}
}
extension MapViewModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
//self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
os_log("locationManager:didUpdateLocations: received location",log: Log.general, type: .debug)
}
}
MapView.swift:
import UIKit
import SwiftUI
import GoogleMaps
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(withLatitude: 30.267153, longitude: -97.7430608, zoom: 6.0)
let gmsMapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
gmsMapView.delegate = context.coordinator
return gmsMapView
}
func updateUIView(_ mapView: GMSMapView, context: Self.Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, GMSMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
}
}
struct GoogleMapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
So any idea on how I can access the gmsMapView object in MapViewModel. I need access to draw lines on the map...Thanks!

I was able to get past this issue by defining the polyline overly value as #Published and accessing it via viewmodel in updateUI. So instead of trying to access the mapview from the viewmodel, I let the view add the polyline overlay. Hope this helps someone else. Thanks
ContentView.swift:
struct ContentView: View {
#State private var selection = 0
#State private var dragOffset = CGSize.zero
#ObservedObject var mapViewModel: MapViewModel
var body: some View {
GeometryReader { geo in
TabView(selection: self.$selection) {
MapView()
.edgesIgnoringSafeArea(.all)
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}
.tag(0)
Text("Second Page")
.tabItem {
VStack {
Image(systemName: "gear")
Text("Settings")
}
}
.tag(1)
Text("Third Page")
.tabItem {
VStack {
Image(systemName: "gear")
Text("Third Page")
}
}
.tag(2)
}
}
}
}
MapView:
struct MapView: UIViewRepresentable {
#ObservedObject var mapViewModel = MapViewModel()
func makeUIView(context: Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(withLatitude: 30.5986015, longitude: -97.8210401, zoom: 20.0)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.isMyLocationEnabled = true
mapView.animate(toViewingAngle: 45)
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Self.Context) {
if (mapViewModel.polyline != nil) {
print("updateUIView: Polyline = \(mapViewModel.polyline!.description)")
os_log("updateUIView: Polyline = %{Public}s",log: Log.general, type: .debug, mapViewModel.polyline!.description)
mapViewModel.polyline!.strokeColor = UIColor.red
mapViewModel.polyline!.strokeWidth = 5.0
mapViewModel.polyline!.map = mapView
}
if (mapViewModel.locChanged && mapViewModel.myLocations.count > 0) {
print("updateUIView: Refocus camera on last location")
os_log("updateUIView: Refocus camera on last location",log: Log.general, type: .debug, mapViewModel.polyline!.description)
let camera = GMSCameraPosition.camera(withLatitude: (mapViewModel.myLocations.last?.coordinate.latitude)!, longitude: (mapViewModel.myLocations.last?.coordinate.longitude)!, zoom: 20.0)
let _ = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, GMSMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
}
}
MapViewModel:
class MapViewModel: NSObject, ObservableObject {
.
.
.
#Published var polyline: GMSPolyline?
#Published var locChanged: Bool = false
override init() {
super.init()
.
.
.
polyline = nil
}
.
.
.
func draw(myLocations: [CLLocation], color: UIColor) {
os_log("MapViewController: Drawing Track for last two Locations",log: Log.general, type: .info)
print("MapViewController: Drawing Track for last two Locations")
let path = GMSMutablePath()
let c1 = myLocations[myLocations.count - 1].coordinate
let c2 = myLocations[myLocations.count - 2].coordinate
path.add(c1)
path.add(c2)
polyline = GMSPolyline(path: path)
print("draw: Polyline = \(polyline!.description)")
polyline!.strokeColor = color
polyline!.strokeWidth = 5.0
}
.
.
.
extension MapViewModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
os_log("locationManager:didUpdateLocations: received location",log: Log.general, type: .debug)
guard let lastLocation: CLLocation = locations.last else {
os_log("locationManager:didUpdateLocations: received null location",log: Log.general, type: .debug)
return
}
if (myLocations.count >= 2) {
os_log("MapViewController: updateLocation: Calling draw method with count = %{Public}s",log: Log.general, type: .debug, myLocations.count.description)
self.draw(myLocations: self.myLocations, color:.red)
}
.
.
.
} else {
os_log("LocationManager: Bad Location...",log: Log.general, type: .error)
badLocationCount += 1
locChanged = false
}
}
}

Related

How can I add the beacon id?

I would like to show on the screen the distance (Unknown, far, near or right here) and the ID of the found beacon. This is my code
struct ContentView: View{
#ObservedObject var detector = BeaconDetector()
var body: some View {
if detector.lastDistance == .immediate {
return Text("RIGHT HERE")
// id here
.modifier(BigText())
.background(Color.green)
.edgesIgnoringSafeArea(.all)
} else if detector.lastDistance == .near {
return Text("NEAR")
.modifier(BigText())
.background(Color.orange)
.edgesIgnoringSafeArea(.all)
} else if detector.lastDistance == .far {
return Text("FAR")
.modifier(BigText())
.background(Color.red)
.edgesIgnoringSafeArea(.all)
} else {
return Text("UNKNOWN")
.modifier(BigText())
.background(Color.gray)
.edgesIgnoringSafeArea(.all)
}
}
}
import Combine
import CoreLocation
import SwiftUI
class BeaconDetector: NSObject, ObservableObject,
CLLocationManagerDelegate{
var didChange = PassthroughSubject<Void, Never>()
var locationManager: CLLocationManager?
var beaconID = UUID().self
#Published var lastDistance = CLProximity.unknown
override init() {
super.init()
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
if CLLocationManager.isMonitoringAvailable(for:
CLBeaconRegion.self){
if CLLocationManager.isRangingAvailable(){
startScanning()
}
}
}
}
func startScanning() {
let uuid = UUID(uuidString: "D3D6736B-4C7C-412D-865B-EE61ACF88C61")!
let constraint = CLBeaconIdentityConstraint(uuid: uuid, major: 123,
minor: 456)
let beconRegion = CLBeaconRegion(beaconIdentityConstraint:
constraint, identifier: "MyBeacon")
locationManager?.startMonitoring(for: beconRegion)
locationManager?.startRangingBeacons(satisfying: constraint)
}
func locationManager(_ manager: CLLocationManager, didRange beacons:
[CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
if let beacon = beacons.first {
update(distance: beacon.proximity)
} else {
update(distance: .unknown)
}
}
func update(distance: CLProximity) {
lastDistance = distance
didChange.send(())
}
}
struct BigText: ViewModifier {
func body(content: Content) -> some View {
content
.font(Font.system(size: 72, design: .rounded))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0,
maxHeight: .infinity)
}
}
Try something like this:
Text("Beacon with major: \(detector.lastMajor) minor: \(detector.lastMinor) is RIGHT HERE")
.modifier(BigText())
.background(Color.green)
.edgesIgnoringSafeArea(.all)
If the BeaconDetector class has a field for lastMajor and lastMinor you can reference these inside your string to be shown on the screen by escaping the expression inside the string with \\().
Since the clarified code shows that the major and minor are not fields on BeaconDetector you will need to add them like this:
#Published var lastMajor: NSNumber? = nil
#Published var lastMinor: NSNumber? = nil
And then you will need to update them when you update the proximity like this:
update(distance: beacon.proximity, major: beacon.major, minor: beacon.minor)
And add these to your update function like this:
func update(distance: CLProximity, major: NSNumber, minor: NSNumber) {
lastDistance = distance
lastMajor = major
lastMinor = minor
didChange.send(())
}

SwiftUI: How to put NavigationLink in MapPin or MapMarker

I have already decode my JSON API and successfully display the location on the map with MapAnnotations and put NavigationLink to see the detail on it. But somehow, when I zoomed out the map to see all marked locations, suddenly my view becomes very laggy with simulator and real iPhone 8 (maybe because I have 100+ annotations on the map?). And then I tried to use MapMarker and the view becomes more smoother, but the problem is now I can't put NavigationLink on MapMarker as well as MapPin. Is there a proper way to display marker/annotations on the map and NavigationLink without making the view lag??
Here is my LocationManager Code to track user's location
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var location: CLLocation?
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 else { return }
locationManager.stopUpdatingLocation()
DispatchQueue.main.async {
self.location = location
}
}
}
My ContentView to display the Map and Show the annotations
import SwiftUI
import MapKit
import Combine
struct ContentView: View {
var body: some View {
NavigationView{
ServiceLocation()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension MKCoordinateRegion {
static var defaultRegion: MKCoordinateRegion {
MKCoordinateRegion(center: CLLocationCoordinate2D.init(latitude: -0.789275, longitude: 113.921327), latitudinalMeters: 5000, longitudinalMeters: 5000)
}
}
//MARK: MAP VIEW
struct ServiceLocation: View{
#State var serviceLocations: [ServiceLocationJSON] = []
#ObservedObject private var locationManager = LocationManager()
#State private var region = MKCoordinateRegion.defaultRegion
#State private var cancellable: AnyCancellable?
private func setCurrentLocation() {
cancellable = locationManager.$location.sink { location in
region = MKCoordinateRegion(center: location?.coordinate ?? CLLocationCoordinate2D(), latitudinalMeters: 20000, longitudinalMeters: 20000)
}
}
var body: some View{
GeometryReader{ geometry in
VStack{
if locationManager.location != nil {
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: .none, annotationItems: serviceLocations) { location in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: location.LATITUDE, longitude: location.LONGITUDE)){
NavigationLink(destination: serviceLocationDetail(serviceLocations: location)){
Image(systemName: "mappin")
.resizable()
.scaledToFit()
.frame(width: geometry.size.width / 15, height: geometry.size.height / 15)
}
}
}
} else {
VStack{
Spacer()
ProgressView()
Spacer()
}
}
}.onAppear{
setCurrentLocation()
getServiceLocation(url: "https://my.api.mockaroo.com/latlong.json?key=e57d0e40"){ (serviceLocations) in
self.serviceLocations = serviceLocations
}
}
.navigationTitle("Service")
.navigationBarTitleDisplayMode(.inline)
}
}
}
//MARK: DETAIL VIEW
struct serviceLocationDetail: View{
var serviceLocations: ServiceLocationJSON
var body: some View{
VStack{
if serviceLocations.DEALER_NAME.isEmpty{
VStack{
Spacer()
ProgressView()
Spacer()
}
}else{
VStack(alignment: .leading, spacing: 10){
Text(serviceLocations.DEALER_NAME)
.fontWeight(.medium)
.padding(.leading, 10)
Text(serviceLocations.DEALER_ADDRESS)
.padding(.leading, 10)
HStack(spacing: 5){
Image(systemName: "phone.fill")
Text(serviceLocations.PHONE)
}.padding(.leading, 10)
Spacer()
}.navigationBarTitle(serviceLocations.DEALER_NAME)
}
}
Spacer()
}
}
//MARK: JSON MODEL
struct ServiceLocationJSON: Identifiable, Decodable{
var id: Int
var LATITUDE: Double
var LONGITUDE: Double
var DEALER_NAME: String
var DEALER_ADDRESS: String
var DEALER_PICTURE: String
var PHONE: String
}
//MARK: DECODE JSON MODEL
func getServiceLocation(url: String, completion: #escaping ([ServiceLocationJSON])->()){
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!){ (data, _, err) in
if err != nil{
print(err!.localizedDescription)
return
}
do{
let serviceLocations = try
JSONDecoder().decode([ServiceLocationJSON].self, from: data!)
completion(serviceLocations)
}
catch{
print(error)
}
}.resume()
}
Build with Xcode 12 and Swift 5
Nevermind, I just solved it. I just need to change coordinateRegion: $region to coordinateRegion: .constant(region)

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

LazyVGrid, List, LazyStacks don't release views from memory?

I'm playing around with the new photo picker in SwiftUI 2 and I made a simple app to show the imported images in a LazyVGrid but when scrolling down, if I imported around 150 images the app finish all the memory and it crashes (Terminated due to memory issue).
I tried the same with a LazyVStack and List but they have the same problem, I was expecting lazy items to release all the cells that goes off screen from the memory but it doesn't look like it's working.
Is this a bug or am I doing something wrong?
Here's my code:
import SwiftUI
struct Media: Identifiable {
var id = UUID()
var image: Image
}
struct ContentView: View {
#State var itemProviders: [NSItemProvider] = []
#State private var showingPhotoPicker: Bool = false
let columns = [
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 8)
]
#State var medias: [Media] = []
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(medias) { media in
media.image
.resizable()
.scaledToFill()
.frame(width: 100, height: 100, alignment: .center)
.clipped()
}
}
}
.navigationBarTitle("Images \(medias.count)")
.navigationBarItems(leading: Button(action: {
loadImages()
}, label: {
Text("Import \(itemProviders.count) images")
}), trailing: Button(action: {
showingPhotoPicker.toggle()
}, label: {
Image(systemName: "photo.on.rectangle.angled")
}))
.sheet(isPresented: $showingPhotoPicker) {
MultiPHPickerView(itemProviders: $itemProviders)
}
}
}
func loadImages() {
for item in itemProviders {
if item.canLoadObject(ofClass: UIImage.self) {
item.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
guard let image = image as? UIImage else {
return
}
medias.append(Media(image: Image(uiImage: image)))
}
}
}
}
}
}
And the PhotoPickerView:
import SwiftUI
import PhotosUI
struct MultiPHPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) private var presentationMode
#Binding var itemProviders: [NSItemProvider]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 0
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController( _ uiViewController: PHPickerViewController, context: Context) {}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
#Environment(\.presentationMode) private var presentationMode
var parent: MultiPHPickerView
init( _ parent: MultiPHPickerView ) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss( animated: true )
self.parent.itemProviders = results.map(\.itemProvider)
}
}
}

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