Marking makeUIView func makes view NOT conform to UIViewRepresentable? - ios

I am trying to update my map's style URL immediately when the device changes from light/dark mode in my SwiftUI app.
The error below shows on every line where "mapView" is declared inside func makeUIView...
"Cannot use mutating getter on immutable value: 'self' is immutable. Mark method 'mutating' to make 'self' mutable"
I was instructed to put mapView inside of makeUIView but there is another func called setCenter() that needs to modify the mapView.
However, when I mark the method mutating the view does not conform to UIViewRepresentable and this error shows every where "mapView" is inside of makeUIView
What can I do to fix this?
struct MapboxMapViewRepresentable: UIViewRepresentable {
#Environment(\.colorScheme) var colorScheme
func makeCoordinator() -> Coordinator {
print("Make coordinator")
return Coordinator(control: self)
}
var mapboxStyleUrl: String {
"mapbox://styles/" + (colorScheme == .dark ? "cl0318cec0sdfdsfsfsfnrshos" : "ckwih3sdfsdfdfsfdfiz7403d0h")
}
lazy var mapView: MGLMapView = {
return MGLMapView(frame: .zero, styleURL: URL(string: mapboxStyleUrl)!)
}()
// FUNC CANNOT BE MUTATING BECAUSE VIEW WILL NOT CONFORM TO UIVIEWREPRESENTABLE
func makeUIView(context: Context) -> MGLMapView {
mapView.showsUserLocation = true
mapView.delegate = context.coordinator
mapView.allowsRotating = false
mapView.direction = 0.0
return mapView
}
// NEEDS TO BE ABLE TO ACCESS mapView
func setCenterAt(coordinate : CLLocationCoordinate2D) {
mapView.setCenter(coordinate, zoomLevel: MapViewModel.startAtZoomLevel, animated: false)
}
func updateUIView(_ mapView: MGLMapView, context: Context) {
//print("updateUIView")
// clear spot data of other users only when zoomed out too far to see them
}
}

A view struct cannot be modified from within itself and makeUIView is exactly a place to create a view (life cycle is managed by SwiftUI), so the fix is to move creation inside, like
func makeUIView(context: Context) -> MGLMapView {
// HERE !!
let mapView = MGLMapView(frame: .zero, styleURL: URL(string: mapboxStyleUrl)!)
mapView.showsUserLocation = true
mapView.delegate = context.coordinator
mapView.allowsRotating = false
mapView.direction = 0.0
return mapView
}

Related

Why is the map automatically zooming in after I manually moved it?

I am having a problem with the map within my app. I set the starting location as the center of the users location, but when I go to move the map around, it doesn't allow me to and/or moves automatically back to the center.
I have userTrackingMode set to .follow. I can't really think of anything else that might be causing this to happen, although I am fairly new to Xcode and Swift.
Here is where I think the problem occurs:
import Foundation
import SwiftUI
import MapKit
struct UberMapViewRepresentable: UIViewRepresentable {
let mapView = MKMapView()
let locationManager = LocationManager()
#EnvironmentObject var locationViewModel: LocationSearchViewModel
func makeUIView(context: Context) -> some UIView {
mapView.delegate = context.coordinator
mapView.isRotateEnabled = false
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
return mapView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if let coordinate = locationViewModel.selectedLocationCoordinate {
context.coordinator.addAndSelectAnnotation(withCoordinate: coordinate)
}
}
func makeCoordinator() -> MapCoordinator {
return MapCoordinator(parent: self)
}
}
extension UberMapViewRepresentable {
class MapCoordinator: NSObject, MKMapViewDelegate {
// MARK: - Properties
let parent: UberMapViewRepresentable
// MARK: - Lifecycle
init(parent: UberMapViewRepresentable) {
self.parent = parent
super.init()
}
// MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
parent.mapView.setRegion(region, animated: true)
}
// MARK: - Helpers
func addAndSelectAnnotation(withCoordinate coordinate: CLLocationCoordinate2D) {
parent.mapView.removeAnnotations(parent.mapView.annotations)
let anno = MKPointAnnotation()
anno.coordinate = coordinate
parent.mapView.addAnnotation(anno)
parent.mapView.selectAnnotation(anno, animated: true)
parent.mapView.showAnnotations(parent.mapView.annotations, animated: true)
}
}
}

Map annotations only appear after map is moved

I have a map which loads annotations from the Google API, when the map initially loads all the annotations they are 'placed' as seen through the print in the console, however they won't show up on the map until I move the map once. Does anyone know if I need to call a method to update the map after placing the annotations?
struct ContentView: View {
var locationSearch = LocationSearch()
#State private var mapView = MapView()
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
#EnvironmentObject var sheetManager: SheetManager
var body: some View {
mapView
.popup(with: SheetManager())
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
}
}
struct MapView: UIViewRepresentable {
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
func updateUIView(_ uiView: MKMapView, context: Context) {
print("FLF: MapView updated")
uiView.setNeedsDisplay()
}
var locationManager = CLLocationManager()
let mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: UIScreen.screenWidth, height: UIScreen.screenHeight))
func setupManager() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()
}
func makeUIView(context: Context) -> MKMapView {
setupManager()
mapView.region = ContentView().region
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
mapView.delegate = context.coordinator // set the delegate to the coordinator
placeMarkersForRegion(region: region)
return mapView
}
func placeMarkersForRegion(region: MKCoordinateRegion) {
var locationSearch = LocationSearch()
locationSearch.performSearch(region: region) { venues in
print("FLF: Placing \(venues.count) marker(s)")
for marker in venues {
let annotation = MKPointAnnotation()
annotation.coordinate = marker.location
annotation.title = marker.name
mapView.addAnnotation(annotation)
}
}
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self) // pass self to the coordinator so it can call `regionDidChangeAnimated`
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Use the 'coordinate' property to get the current location of the map view
let currentRegion = mapView.region
print("FLF: Map has moved")
self.placeMarkersForRegion(region: currentRegion)
// Do something with the current region (e.g. update a state variable or perform a search)
}
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
var parent: MapView // add a property to hold a reference to the parent view
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Call the parent's implementation of this method
parent.mapView(mapView, regionDidChangeAnimated: animated)
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
// Get the tapped annotation
guard let annotation = view.annotation else { return }
// Print the title of the annotation
print(annotation.title ?? "Unknown")
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
print("FLF: Marker tapped")
}
}
The UIViewRepresentable and Coordinator aren't implemented correctly. E.g. makeUIView has to init it, but you are initing it as a property on the struct which is immediately lost. Also MapViewCoordinator(self) is a mistake because self, i.e. the struct, is immediately disgarded after SwiftUI has updated.
Another issue is the #State shouldn't hold a View like how your ContentView has a #State for the MapView.
Here is an example of how to use MKMapView with UIViewRepresentable:
struct MKMapViewRepresentable: UIViewRepresentable {
#Binding var userTrackingMode: MapUserTrackingMode
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
context.coordinator.mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// MKMapView has a strange design that the delegate is called when setting manually so we need to prevent an infinite loop
context.coordinator.userTrackingModeChanged = nil
uiView.userTrackingMode = userTrackingMode == .follow ? MKUserTrackingMode.follow : MKUserTrackingMode.none
context.coordinator.userTrackingModeChanged = { mode in
userTrackingMode = mode == .follow ? MapUserTrackingMode.follow : MapUserTrackingMode.none
}
}
class Coordinator: NSObject, MKMapViewDelegate {
lazy var mapView: MKMapView = {
let mv = MKMapView()
mv.delegate = self
return mv
}()
var userTrackingModeChanged: ((MKUserTrackingMode) -> Void)?
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
userTrackingModeChanged?(mode)
}
}
}

Xcode 13 Swiftui change dynamically value of a variable

I have a Boolean variables called points. The initial value of points is setted to false because I want a certain behaviour. Then in a function I want to change the value of the points variable to true.
I read some documentation about swift (because I'm new) but I'm not getting why the value of points is not changing.
import SwiftUI
import MapboxMaps
import MapboxCoreMaps
import UIKit
import CoreLocation
struct MapBoxView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MapViewController {
return MapViewController()
}
func updateUIViewController(_ uiViewController: MapViewController, context: Context) {}
}
#objc(MapViewController)
public class MapViewController: UIViewController {
internal var mapView: MapView!
private let customImage = UIImage(named: "location-pin-1")!
var n: Int = 0
override public func viewDidLoad() {
super.viewDidLoad()
let centerCoordinate = CLLocationCoordinate2D(latitude: 50, longitude: 10)
let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 2.4))
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
mapView.mapboxMap.onNext(.mapLoaded) { _ in
self.setupExample()
}
}
public func setupExample() {
addTapGesture(to: mapView)
}
public func addTapGesture(to mapView: MapView) {
self.n = self.n + 1
if (self.n < 2) {
let tapGesture = UILongPressGestureRecognizer(target: self, action: #selector(addPoint))
mapView.addGestureRecognizer(tapGesture)
}
}
#objc public func addPoint(_ sender: UITapGestureRecognizer) {
let tapPoint = sender.location(in: mapView)
let coordinate = mapView.mapboxMap.coordinate(for: tapPoint)
let pointAnnotationManager = mapView.annotations.makePointAnnotationManager()
var customPointAnnotation = PointAnnotation(coordinate: coordinate)
customPointAnnotation.image = .init(image: customImage, name: "my-custom-image-name")
pointAnnotationManager.annotations = [customPointAnnotation]
}
}
This all is included in a class that use UIViewController.
This is not SwiftUI, so remove the #State property wrapper, make it just var points: Bool = false
If you are using UITapGestureRecognizer and UIViewController then you are using UIKit not SwiftUI

How to add gestures to MapView in SwiftUI

I am trying to add gestures (tap, swipe, etc) to a Mapkit Map View in ContentView.swift to no avail. I started with googlemaps and then tried Mapkit. There are two ways I know of to add gestures to a view.
Add gestures to mapView like the following:
import SwiftUI
struct ContentView: View {
var body: some View {
MapView()
.onTapGesture(count: 2) {
print("Double tapped!")
}
.edgesIgnoringSafeArea(.all)
}
Add gestures in MapView view as follows:
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.doubleTapped))
tap.numberOfTapsRequired = 2
mapView.addGestureRecognizer(tap)
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
#objc func doubleTapped() {
print("Double Tapped...")
}
}
}
Neither of these methods seem to work. The preferrable way to me is #1 but I tried an alternate way as in #2. Any ideas on what I am doing wrong? I am using Xcode 11.4 (11E146).Thanks!

SwiftUI: avoid recreating/rerendering view in TabView with MKMapView+UIViewRepresentable

I have a TabView with three tabs, one of which contains a map view that's implemented like this:
struct MapView: UIViewRepresentable {
let region: MKCoordinateRegion
let animatedRegion: Bool
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView(frame: .zero)
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.setRegion(region, animated: animatedRegion)
return mapView
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIView(_ mapView: MKMapView, context: Context) {
}
class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
}
}
The tab view is implemented like this:
TabView(selection: $selection) {
MapView(/* params */)
.tabItem {
Image(systemName: "1.square.fill")
Text("map")
}.tag(1)
Text("Screen #2")
.tabItem {
Image(systemName: "2.square.fill")
Text("2")
}.tag(2)
Text("Screen #3")
.tabItem {
Image(systemName: "3.square.fill")
Text("3")
}.tag(3)
}
The problem is that the makeUIView(:context) method is executed every time I switch back to the map tab from one of the other two tabs. It appears that the underlying MKMapView instance is deallocated when I switch to another tab, then it's recreated when I switch back. In UIKit it doesn't rerender the whole view like that. Am I doing something wrong, or is there anything I can do to make sure that the underlying MKMapView instance is retained when I switch back so that it doesn't have to recreate it every single time?
You need to store instance of MKMapView for each tab and destroy before you leaving the view because SwiftUI will destroy MapView when it need to render for example the binding vars have changed.
struct MapView: UIViewRepresentable {
#Binding var currentTab: Int
static private var mapViews = [Int: MKMapView]()
let region: MKCoordinateRegion
let animatedRegion: Bool
func makeUIView(context: Context) -> MKMapView {
guard MapView.mapViews[currentTab] != nil else { return MapView.mapViews[currentTab]! }
let mapView = MKMapView(frame: .zero)
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.setRegion(region, animated: animatedRegion)
MapView.mapViews[currentTab] = mapView
return mapView
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIView(_ mapView: MKMapView, context: Context) {
}
class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
}
}
use EnvironmentObject. always update it and pass it in TabView {MapView(coordinates: Object.coordinates)}

Resources