How to add gestures to MapView in SwiftUI - ios

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!

Related

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

Marking makeUIView func makes view NOT conform to UIViewRepresentable?

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
}

How to get MKMapView Directions in SwiftUI

In my application I'm using MKMapView SwiftUI implementation. My map is working good but I want to get directions when tapped a button from ContentView. I've explained in more detail below...
Here is my ContentView:
struct ContentView: View {
var body: some View {
VStack {
AppleMapView(coordinate: CLLocationCoordinate2D(latitude: 40.7127, longitude: -74.0059))
.frame(height: 400)
Button(action: {
**// !!! I want to call AppleMapView/getDirections() method here !!!**
}) {
Text("Get Directions")
}
}
}
}
Here is my MapView:
struct AppleMapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
// some codes //
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// some codes //
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func getDirections() {
// some codes //
}
class Coordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = .blue
renderer.lineWidth = 4
return renderer
}
}
}
Thanks.
Create a #Binding property on your MapView and then set the directions from your ContentView inside the Button action.
This way your updateUIView will get called accordingly with the updated values.
struct ContentView: View {
#State var directions: [CLLocation] = []
var body: some View {
VStack {
MapView(directions: $directions)
Button(action: {
// Directions are Nepal to India
self.directions = [CLLocation(latitude: 27.2041206, longitude: 84.6093928), CLLocation(latitude: 20.7712763, longitude: 73.7317739)]
}) {
Text("Get Directions")
}
}
}
}
struct MapView: UIViewRepresentable {
#Binding var directions: [CLLocation]
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = .blue
renderer.lineWidth = 4
return renderer
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ mapView: MKMapView, context: Context) {
var coordinates = self.directions.map({(location: CLLocation!) -> CLLocationCoordinate2D in return location.coordinate})
let polyline = MKPolyline(coordinates: &coordinates, count: self.directions.count)
mapView.delegate = context.coordinator
mapView.addOverlay(polyline)
}
}

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

Using SpriteKit inside SwiftUI

I am having an issue when creating a SpriteKit scene within SwiftUI. I created this project initially as a SwiftUI project.
Here is the code I have so far:
ContentView.swift:
/// Where the UI content from SwiftUI originates from.
struct ContentView : View {
var body: some View {
// Scene
SceneView().edgesIgnoringSafeArea(.all)
}
}
SceneView.swift:
/// Creates an SKView to contain the GameScene. This conforms to UIViewRepresentable, and so can be used within SwiftUI.
final class SceneView : SKView, UIViewRepresentable {
// Conformance to UIViewRepresentable
func makeUIView(context: Context) -> SKView {
print("Make UIView")
return SceneView(frame: UIScreen.main.bounds)
}
func updateUIView(_ uiView: SKView, context: Context) {
print("Update UIView")
}
// Creating scene
override init(frame: CGRect) {
super.init(frame: frame)
let scene = Scene(size: UIScreen.main.bounds.size)
presentScene(scene)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Scene.swift:
/// The scene for the game in SpriteKit.
final class Scene : SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
print("Scene didMove:")
}
}
Problem
The problem is that the scene is reloading multiple times, as shown by the logs (because there are prints in the code):
Scene didMove:
Make UIView
Scene didMove:
Update UIView
As you can see, Scene didMove: is printed twice. I only want this to be called once, as I want to create my sprites here. Any ideas?
SwiftUI 2
There is now a native view responsible for displaying a SKScene - it's called SpriteView.
Assuming we have a simple SKScene:
class Scene: SKScene {
override func didMove(to view: SKView) {
...
}
}
we can use a SpriteView to display it directly in a SwiftUI view:
struct ContentView: View {
var scene: SKScene {
let scene = Scene()
scene.size = CGSize(width: 300, height: 400)
scene.scaleMode = .fill
return scene
}
var body: some View {
SpriteView(scene: scene)
.frame(width: 300, height: 400)
.edgesIgnoringSafeArea(.all)
}
}
You can find more information here:
How to integrate SpriteKit using SpriteView.
Here's a SpriteKit container View which can be used this way:
SpriteKitContainer(sceneName: "MainScene")
struct SpriteKitContainer : UIViewRepresentable {
let sceneName: String
class Coordinator: NSObject {
var scene: SKScene?
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> SKView {
let view = SKView(frame: .zero)
view.preferredFramesPerSecond = 60
view.showsFPS = true
view.showsNodeCount = true
//load SpriteKit Scene
guard let aScene = SKScene(fileNamed: sceneName)
else {
view.backgroundColor = UIColor.red
return view
}
aScene.scaleMode = .resizeFill
context.coordinator.scene = aScene
return view
}
func updateUIView(_ view: SKView, context: Context) {
view.presentScene(context.coordinator.scene)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
// Replace "MainScene" with your SpriteKit scene file name
SpriteKitContainer(sceneName: "MainScene")
.edgesIgnoringSafeArea(.all)
.previewLayout(.sizeThatFits)
}
}
#endif
This is how i solved it:
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
SpriteKitContainer(scene: SpriteScene())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SpriteKitContainer.swift
import SwiftUI
import SpriteKit
struct SpriteKitContainer: UIViewRepresentable {
typealias UIViewType = SKView
var skScene: SKScene!
init(scene: SKScene) {
skScene = scene
self.skScene.scaleMode = .resizeFill
}
class Coordinator: NSObject {
var scene: SKScene?
}
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator()
coordinator.scene = self.skScene
return coordinator
}
func makeUIView(context: Context) -> SKView {
let view = SKView(frame: .zero)
view.preferredFramesPerSecond = 60
view.showsFPS = true
view.showsNodeCount = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
view.presentScene(context.coordinator.scene)
}
}
struct SpriteKitContainer_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}
SpriteKitScene.swift
import UIKit
import SpriteKit
class SpriteScene: SKScene {
//change the code below to whatever you want to happen on skscene
override func didMove(to view: SKView) {
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
let box = SKSpriteNode(color: .red, size: CGSize(width: 50, height: 50))
box.position = location
box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
addChild(box)
}
}
PS: you'll only see the spritekitscene working in the simulator, it won't work in the preview
A Coordinator is not necessary to present a scene; the Context is sufficient. Here is what I use to load a Scene.sks file:
struct ContentView : View {
var body: some View {
SKViewContainer()
}
}
struct SKViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> SKView {
let view = SKView()
guard let scene = SKScene(fileNamed: "Scene")
else {
view.backgroundColor = UIColor.red
return view
}
view.presentScene(scene)
return view
}
func updateUIView(_ uiView: SKView, context: Context) {}
}

Resources