How I can clustering map annotation in swiftui? - swiftui

I searched for many different ways but didn't find any with swiftui.
I tried to do it through MKMapView. But I have custom points and a lot of functionality is tied to swiftui.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 43.64422936785126, longitude: 142.39329541313924),
span: MKCoordinateSpan(latitudeDelta: 1.5, longitudeDelta: 2)
)
var body: some View {
Map(coordinateRegion: $region, annotationItems: data) { annotation in
MapAnnotation(coordinate: annotation.coordinate) {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(Color.purple)
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct SampleData: Identifiable {
var id = UUID()
var latitude: Double
var longitude: Double
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
}
var data = [
SampleData(latitude: 43.70564024126748, longitude: 142.37968945214223),
SampleData(latitude: 43.81257464206404, longitude: 142.82112322464369),
SampleData(latitude: 43.38416585162576, longitude: 141.7252598737476),
SampleData(latitude: 45.29168643283501, longitude: 141.95286751470724),
SampleData(latitude: 45.49261392585982, longitude: 141.9343973160499),
SampleData(latitude: 44.69825427301145, longitude: 141.91227845284203)
]
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please help find a solution. I will be glad to any advice.

Related

How to add tooltip to Map Annotation in order to show the location name on the Map using MapKit (SwiftUI)

I'm trying to figure out how I can display the title/name of a MapAnnotation when I hover over the Annotation/Marker or when I simply tap on the annotation/Marker. Is there a simple way to do this?
I tried using .help(), but it doesn't display anything on the map...
Here is the relevant code...
Map(coordinateRegion: $viewModel.region, showsUserLocation: true, annotationItems: viewModel.locations){ location in
MapAnnotation(coordinate: location.coordinate) {
Image(systemName: "mappin.circle")
.help("\(location.name)")
}
}
Actually, .help() will work as long as you use it with a button.
This is a quick paste from a current project I’ve got.
Note:
This works with the Mac version and I have not tested anywhere else
import SwiftUI
import MapKit
struct SwiftUIMapViewTest: View {
#EnvironmentObject var modelData: ModelData
#State var region: MKCoordinateRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 29.548460, longitude: -98.481556),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
#Binding var selectedListing: Place?
#Binding var results: [Place]
#State var image: String = "mappin"
var body: some View {
Map(
coordinateRegion: $region,
annotationItems: results,
annotationContent: {
listing in
MapAnnotation(coordinate: listing.coordinate,
content: {Button(action: {
self.selectedListing = listing as Place?
},
label: {
VStack{
Image(systemName: "mappin.circle.fill")
.foregroundColor(.red)
.contentShape(Circle())
}
}).help("\(listing.company) \n\(listing.street) \n\(String(listing.zipCode))")
}
)})
}
}
struct SwiftUIMapViewTest_Previews: PreviewProvider {
static var modelData = [ModelData().places]
static var modelPlace = ModelData().places
static var previews: some View {
SwiftUIMapViewTest(
selectedListing: .constant(modelPlace[0]),
results: .constant(modelData[0])
)
}
}
Although not very pritty, this is the result.
Hope this helps someone.
You don't add a tooltip to a map annotation. You make your own custom view. You can display it however you want, and show and hide child views as desired. As an example:
import SwiftUI
import CoreLocation
import MapKit
struct MapAnnotationsView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.889499, longitude: -77.035230), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
let placeArray: [Place] = [Place(title: "Washington Monument", coordinate: CLLocationCoordinate2D(latitude: 38.889499, longitude: -77.035230))]
var body: some View {
Map(coordinateRegion: $region, annotationItems: placeArray) { annotation in
// This makes a generic annotation that takes a View
MapAnnotation(coordinate: annotation.coordinate) {
// This is your custom view
AnnotationView(placeName: annotation.title)
}
}
}
}
struct AnnotationView: View {
let placeName: String
#State private var showPlaceName = false
var body: some View {
VStack(spacing: 0) {
Text(placeName)
.font(.callout)
.padding(5)
.background(Color.white)
.cornerRadius(10)
// Prevents truncation of the Text
.fixedSize(horizontal: true, vertical: false)
// Displays and hides the place name
.opacity(showPlaceName ? 1 : 0)
// You can use whatever you want here. This is a custom annotation marker
// made to look like a standard annotation marker.
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundColor(.red)
Image(systemName: "arrowtriangle.down.fill")
.font(.caption)
.foregroundColor(.red)
.offset(x: 0, y: -5)
}
.onTapGesture {
withAnimation(.easeInOut) {
showPlaceName.toggle()
}
}
}
}
struct Place: Identifiable {
let id = UUID()
var title: String
var coordinate: CLLocationCoordinate2D
}

Clustering annotation with swiftUI

My goal is clustering annotation on map with show number of items in cluster, I have no experience in UIKit and try to avoid it. Is it possible to do it using swiftUI only? If not how to reduce intervention of UIKit?
This is how it should look like
import SwiftUI
import MapKit
struct ContentView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 43.64422936785126, longitude: 142.39329541313924),
span: MKCoordinateSpan(latitudeDelta: 1.5, longitudeDelta: 2)
)
var body: some View {
Map(coordinateRegion: $region, annotationItems: data) { annotation in
MapAnnotation(coordinate: annotation.coordinate) {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(Color.purple)
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct SampleData: Identifiable {
var id = UUID()
var latitude: Double
var longitude: Double
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
}
var data = [
SampleData(latitude: 43.70564024126748, longitude: 142.37968945214223),
SampleData(latitude: 43.81257464206404, longitude: 142.82112322464369),
SampleData(latitude: 43.38416585162576, longitude: 141.7252598737476),
SampleData(latitude: 45.29168643283501, longitude: 141.95286751470724),
SampleData(latitude: 45.49261392585982, longitude: 141.9343973160499),
SampleData(latitude: 44.69825427301145, longitude: 141.91227845284203)
]
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I find the way to cluster annotations with MapKit, but reuse map like a view for easy swiftUI. Looks like that https://i.stack.imgur.com/u3hKR.jpg
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var forDisplay = data
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 43.64422936785126, longitude: 142.39329541313924),
span: MKCoordinateSpan(latitudeDelta: 1.5, longitudeDelta: 2)
)
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
/// showing annotation on the map
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? LandmarkAnnotation else { return nil }
return AnnotationView(annotation: annotation, reuseIdentifier: AnnotationView.ReuseID)
}
}
func makeCoordinator() -> Coordinator {
MapView.Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
/// creating a map
let view = MKMapView()
/// connecting delegate with the map
view.delegate = context.coordinator
view.setRegion(region, animated: false)
view.mapType = .standard
for points in forDisplay {
let annotation = LandmarkAnnotation(coordinate: points.coordinate)
view.addAnnotation(annotation)
}
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}
struct SampleData: Identifiable {
var id = UUID()
var latitude: Double
var longitude: Double
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
}
var data = [
SampleData(latitude: 43.70564024126748, longitude: 142.37968945214223),
SampleData(latitude: 43.81257464206404, longitude: 142.82112322464369),
SampleData(latitude: 43.38416585162576, longitude: 141.7252598737476),
SampleData(latitude: 45.29168643283501, longitude: 141.95286751470724),
SampleData(latitude: 45.49261392585982, longitude: 141.9343973160499),
SampleData(latitude: 44.69825427301145, longitude: 141.91227845284203)
]
class LandmarkAnnotation: NSObject, MKAnnotation {
let coordinate: CLLocationCoordinate2D
init(
coordinate: CLLocationCoordinate2D
) {
self.coordinate = coordinate
super.init()
}
}
/// here posible to customize annotation view
let clusterID = "clustering"
class AnnotationView: MKMarkerAnnotationView {
static let ReuseID = "cultureAnnotation"
/// setting the key for clustering annotations
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = clusterID
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForDisplay() {
super.prepareForDisplay()
displayPriority = .defaultLow
}
}
And use that map like a default view
import SwiftUI
struct ContentView: View {
var body: some View {
MapView()
.edgesIgnoringSafeArea(.all)
}
}
For solving a problem I used next resources:
https://www.hackingwithswift.com/books/ios-swiftui/communicating-with-a-mapkit-coordinator
https://www.hackingwithswift.com/books/ios-swiftui/advanced-mkmapview-with-swiftui
https://developer.apple.com/videos/play/wwdc2017/237/
https://www.youtube.com/watch?v=QuYA7gQjTt4

Problem of resizing Map using SwiftUI Mapkit

I would like to zoom in my map on the application, I tried both using latitudinalMeters: 300, longitudinalMeters: 300 or spin with latitudeDelta: 0.001. Both of them did not work at all.
I also chose (0, 0) as my center, but every time I run on the simulator, I have (37.326010,-122.026056) as my center. Apparently, none of the default settings of center and region that I had set in my location manager works in ContentView.
Here is my code of LocationManager.swift:
import Foundation
import CoreLocation
import MapKit
class LocationManager: NSObject, ObservableObject{
let locationManager = CLLocationManager()
#Published var location: CLLocation?
#Published var region: MKCoordinateRegion
override init(){
self.region = MKCoordinateRegion(center: CLLocationCoordinate2D.init(latitude: 0,longitude: 0),latitudinalMeters: 300, longitudinalMeters: 300)
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]){
guard let location = locations.last else { return }
self.region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 300, longitudinalMeters: 300)
self.location = location
}
}
Here is my ContentView:
struct ContentView: View {
var body: some View {
MapView2()
}
}
struct MapView2: View {
#ObservedObject var locationManager = LocationManager()
var body: some View {
let coord = locationManager.location?.coordinate
let lat = coord?.latitude ?? 0
let lon = coord?.longitude ?? 0
return VStack {
Map(coordinateRegion: $locationManager.region,
interactionModes: .all,
showsUserLocation: true, userTrackingMode: .constant(.follow))
}
}
}
As for SwiftUI using MapKit, I would not use the CoreLocation framework. You can use the .onChange modifier to perform zoom changes to your View. You can use the #State var zoom with a SwiftUI gesture to perform them if you want, or anything that can do those changes live. I added two buttons within a slider to zoom in or out for the example.
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
MapsView()
}
}
struct MapsView: View {
#State var zoom: CGFloat = 15
#State var mapCoordinate = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 38.989202809314854,
longitude: -76.93626224283602),
span: MKCoordinateSpan(
latitudeDelta: .zero,
longitudeDelta: .zero))
var body: some View {
VStack(spacing: 16) {
Map(coordinateRegion: $mapCoordinate)
.ignoresSafeArea(edges: .all)
// You can see the changes being operating by the .onChange modifier.
Slider(value: $zoom,
in: 0.01...50,
minimumValueLabel: Image(systemName: "plus.circle"),
maximumValueLabel: Image(systemName: "minus.circle"), label: {})
.padding(.horizontal)
.onChange(of: zoom) { value in
mapCoordinate.span.latitudeDelta = CLLocationDegrees(value)
mapCoordinate.span.longitudeDelta = CLLocationDegrees(value)
}
}
.font(.title)
}
}

Swiftui warning Modifying state during view update, this will cause undefined behavior

When I run my swiftui app I am getting the following warning runtime: SwiftUI: Modifying state during view update, this will cause undefined behavior. . I get that warning implementing Map and I know why I am getting it . Is there a way to fix the issue so that I do not get that warning . I basically initialize the MKCoordinateRegion then I have a function with a closure that gets the latitude and longitude and I update the MKCoordinateRegion . I call this method inside onappear
PlaceViewModel().CreatedBy(PlaceID) { x in
showMap = true
region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: x?.latitude ?? 0.0, longitude: x?.longitude ?? 0.0), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)) // warning comes here
}
rest of code , any suggestions on getting rid of that warning would be great
import SwiftUI
import MapKit
struct PlaceView: View {
#State var region: MKCoordinateRegion = MKCoordinateRegion()
#State var mainModel = [MainModel]()
#State var model = PlaceModel()
#State var mainModelValue = MainModelValue()
#Binding var PlaceID: Int
#State var IsMainView = MainViewEnum(viewType: .PlaceView)
#State var triggerView = false
#Environment(\.presentationMode) var presentationMode
#State var ProfileState = false
#State var showMap = false
var body: some View {
VStack(spacing: 0) {
ZStack
{
Color(UIColor(hexString: "#E0EAEA"))
.ignoresSafeArea()
HStack {
Image("back-arrow")
.resizable()
.frame(width: 24, height: 24)
.padding(.leading, 15)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
Text(model.Name ?? "Places")
.fontWeight(.semibold)
.font(.system(size: 18))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
}.padding(0)
}.frame(height: 35)
if showMap == true {
Map(coordinateRegion: $region, showsUserLocation: true, userTrackingMode: .constant(.follow))
.frame(height: 152)
}
HStack {
Text("Added By: ")
Text(model.createdby ?? "")
.onTapGesture {
ProfileState.toggle()
}.fullScreenCover(isPresented: $ProfileState,content: {
ProfileHeaderView(Name: model.fullname ?? "", UserID: model.userid ?? 0)
})
}.padding([.top], 15)
if triggerView == true {
TimeLineView(model: mainModel, isMainView: $IsMainView, newValue: mainModelValue,UserID: MYID)
.padding(.top,0)
}
}.padding(0)
.onAppear {
IsMainView.PlaceID = PlaceID
triggerView = true
PlaceViewModel().CreatedBy(PlaceID) { x in
showMap = true
region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: x?.latitude ?? 0.0, longitude: x?.longitude ?? 0.0), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)) // The warning happens here
}
}
}
}
struct PlaceView_Previews: PreviewProvider {
static var previews: some View {
PlaceView(PlaceID: .constant(0))
}
}
You can reproduce the issue with this .
import SwiftUI
import MapKit
struct TesterView: View {
#State var PlaceID: Int?
#State var isTapped = false
var body: some View {
VStack(alignment: .leading, spacing: 1) {
Text("test")
.onTapGesture {
isTapped.toggle()
}.fullScreenCover(isPresented: $isTapped,content: {
ReproduceView(PlaceID: $PlaceID)
})
}
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
TesterView()
}
}
}
struct ReproduceView: View {
#State var region: MKCoordinateRegion = MKCoordinateRegion()
#State var showMap = false
#Binding var PlaceID: Int?
var body: some View {
VStack(spacing: 0) {
if showMap == true {
Map(coordinateRegion: $region, showsUserLocation: true, userTrackingMode: .constant(.follow))
.frame(height: 152)
}
}.padding(0)
.onAppear {
showMap = true
region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 33.7490, longitude: -84.3880), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
}
}
}
struct Reproduce_Previews: PreviewProvider {
static var previews: some View {
ReproduceView(PlaceID: .constant(0))
}
}

Transferring Coordinates to a MapView with Pin

I am trying to display a map pin on a map. Upon entry of a transaction the details are saved along with the location coordinates. In a list of transaction entries, the user may click on an entry for more detail information including a small map showing the transaction location.
Based on Asperi's suggestions at adding a MapMarker to MapKit in swiftUI 2 it appears that I need to declare an identifiable structure in order to use a map pin.
In the DetailView the latitude and longitude are copied to a coordinate parameter before transmission to MapView.
struct DetailView: View {
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: item.entryLat,
longitude: item.entryLong)
}
var body: some View {
VStack {
MapView(coordinate: coordinate)
.ignoresSafeArea(edges: .all)
.frame(height: 400)
.padding(.vertical, 10)
}
}
}
MapView is where I'm having trouble. I'm not sure how to pass in my coordinates for the region and the marker (xxxxx). Copying` coordinate to the #State region and the marker produces the error "Argument passed to call that takes no arguments".
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
struct MapView: View {
var coordinate: CLLocationCoordinate2D
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(xxxxxxx), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
let markers = [Marker(location: MapMarker(coordinate: CLLocationCoordinate2D(xxxxxxx), tint: .red))]
var body: some View {
Map(coordinateRegion: $region, showsUserLocation: true,
annotationItems: markers) { marker in
marker.location
}.edgesIgnoringSafeArea(.all)
}
}
Sounds like for your application, declaring the region as a constant will work. The code would look like this:
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
struct MapView: View {
var coordinate: CLLocationCoordinate2D
var body: some View {
Map(coordinateRegion: .constant(MKCoordinateRegion(center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))),
showsUserLocation: true,
annotationItems: [Marker(location: MapMarker(coordinate: coordinate))]) { marker in
marker.location
}.edgesIgnoringSafeArea(.all)
}
}
If you still wanted to use it as a #State variable, you could use a custom init to set the value:
struct MapView: View {
var coordinate: CLLocationCoordinate2D
#State private var region : MKCoordinateRegion
init(coordinate : CLLocationCoordinate2D) {
self.coordinate = coordinate
_region = State(initialValue: MKCoordinateRegion(center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)))
}
var body: some View {
Map(coordinateRegion: $region,
showsUserLocation: true,
annotationItems: [Marker(location: MapMarker(coordinate: coordinate))]) { marker in
marker.location
}
.edgesIgnoringSafeArea(.all)
}
}
Lastly, I'm defining the markers array inline, but you could split it out into a computed property:
var markers : [Marker] {
[Marker(location: MapMarker(coordinate: coordinate))]
}
var body: some View {
Map(coordinateRegion: $region,
showsUserLocation: true,
annotationItems: markers) { marker in
marker.location
}
.edgesIgnoringSafeArea(.all)
}