Clustering annotation with swiftUI - 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

Related

How I can clustering map annotation in 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.

MapView does not show user current location

I was trying to display my current location into a Swiftui MapView. To do so, I created the following class:
import SwiftUI
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var locationManager = CLLocationManager()
#Published var locationStatus: CLAuthorizationStatus?
#Published var lastLocation: CLLocation?
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
private var statusString: String {
guard let status = locationStatus else {
return "unknown"
}
switch status {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationStatus = status
print(#function, statusString)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
lastLocation = location
// fetchLocation(location: lastLocation)
self.locationManager.stopUpdatingLocation()
print(#function, location)
}
}
and the following view:
var body: some View {
VStack {
Text("Latitude: \(locationManager.lastLocation?.coordinate.latitude ?? 0), Longitude: \(locationManager.lastLocation?.coordinate.longitude ?? 0)")
.onAppear{
print("DEBUG: status 1 : \(locationManager.lastLocation?.coordinate.latitude ?? 0)")
}
MapView(lat: (locationManager.lastLocation?.coordinate.latitude ?? 0), lon: locationManager.lastLocation?.coordinate.longitude ?? 0, latDelta: 0.05, lonDelta: 0.05)
.frame(width: UIScreen.screenWidth - 36, height: UIScreen.screenWidth / 2)
.cornerRadius(10)
.onAppear{
print("DEBUG: status 2 : \(locationManager.locationStatus)")
print("DEBUG: lat: \(locationManager.lastLocation?.coordinate.latitude), lon: \(locationManager.lastLocation?.coordinate.longitude)")
}
}
}
So far, the Textfield does show the correct coordinates, but my Mapview shows NIL as the coordinates.
Adding my MapView here as well for completeness:
struct MapView: UIViewRepresentable {
#State var lat = 0.0
#State var lon = 0.0
#State var latDelta = 0.05
#State var lonDelta = 0.05
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon )
let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}
Hope you could have a look and see if I have missed anything.
The answer is:
Remove all the #State from the MapView():
struct MapView: UIViewRepresentable {
var lat = 0.0
var lon = 0.0
var latDelta = 0.05
var lonDelta = 0.05
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon )
let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}

Can I hide default points of interests in MapKit with SwiftUI like parks, restaurants etc?

I want to use my own map annotations and I am trying to hide default annotations from Map.
I found this to remove every default annotation from the map view
let configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
But how do I apply this configuration to my map view in SwiftUI?
import SwiftUI
import MapKit
#available(iOS 16.0, *)
struct MyMapView: View {
init (){
let configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
}
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 41.59710,
longitude: -74.14976),
span: MKCoordinateSpan(
latitudeDelta: 0.0125,
longitudeDelta: 0.0125)
)
var body: some View {
Map(coordinateRegion: $region)
.edgesIgnoringSafeArea(.all)
.disabled(true)
}
}
The SwiftUI Map view doesn't support this functionality. To get a map that can do this you'd need to use an MKMapView which is in UIKit. Here's an example of how to use it in SwiftUI
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
MapView()
.edgesIgnoringSafeArea(.all)
.disabled(true)
}
}
struct MapView: UIViewRepresentable {
let configuration: MKStandardMapConfiguration
private var center: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: 41.59710, longitude: -74.14976)
}
private var coordinateSpan: MKCoordinateSpan {
MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125)
}
init() {
configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.region = MKCoordinateRegion(center: center, span: coordinateSpan)
mapView.preferredConfiguration = configuration
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Passing Map Coordinates to a UIKit Map and Map Pin

I have an app that displays expense entries in a list. Clicking on any entry will display additional information and a map of the transaction location. Each entry contains map coordinates for that particular entry.
I would like to use a UIKit map with the options standard, hybrid, or satellite views. Below is some sample map code that will display the three map types but I need help passing in the coordinates and handling the map pin.
Let me know if you need to see any additional code or have questions about my code. Thanks
struct MapViewUIKit: UIViewRepresentable {
let region: MKCoordinateRegion
let mapType : MKMapType
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
struct ContentView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 36.15035, longitude: -115.91304) , span: MKCoordinateSpan(latitudeDelta: 0.0005, longitudeDelta: 0.0005))
#State private var mapType: MKMapType = .standard
var body: some View {
ZStack {
MapViewUIKit(region: region, mapType: mapType)
.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
.offset(y: -40)
.font(.largeTitle)
}
}
}
}
Below is my current swiftui code with map and map pin showing how the coordinates are passed down to maps. ShowRow is part of logic to display core data entries. Clicking on any entry will bring up additional data and a map.
struct DetailView: View {
var item: CurrTrans // this contains core data entries with coordinates
var coordinate: CLLocationCoordinate2D
var g: GeometryProxy
var body: some View {
VStack {
ShowMap(item: item, coordinate: coordinate)
.frame(width: g.size.width, height: g.size.height * 0.65)
ShowDetail(item: item, g: g) // additional entry info
.padding(.top, g.size.height * 0.05)
}
}
}
struct ShowMap: View {
var coordinate: CLLocationCoordinate2D
var item: CurrTrans
var body: some View {
HStack {
Spacer()
MapView(coordinate: coordinate)
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
}
struct Marker: Identifiable {
let id = UUID()
let location: MapPin // or MapMarker
}
Here in MapView typical UIKit examples hardcode the coordinates in a state parameter. I need to pass in the coordinates dynamically.
struct MapView: View {
var coordinate: CLLocationCoordinate2D
var body: some View {
Map(coordinateRegion: .constant(MKCoordinateRegion(center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))),
showsUserLocation: false,
annotationItems: [Marker(location: MapPin(coordinate: coordinate))]) { marker in
marker.location
}
}
}
I'm not showing all the details here. The map type segmented picker is shown slightly below the map so I'm not using a ZStack.
The map type state parameter is stored up a level because I have slightly different versions for portrait and landscape modes.
#State private var mapType: MKMapType = .standard
struct ShowMap: View {
var item: CurrTrans
var coordinate: CLLocationCoordinate2D
var g: GeometryProxy
var mapType: MKMapType
var body: some View {
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
let region = MKCoordinateRegion(center: coordinate, span: span)
MapView(region: region, mapType: mapType, coordinate: coordinate)
.edgesIgnoringSafeArea(.all)
}
}
}
struct MapView: UIViewRepresentable {
let region: MKCoordinateRegion
let mapType : MKMapType
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: true)
// display a map pin
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
mapView.addAnnotation(annotation)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
struct ShowDetail: View {
var item: CurrTrans
var g: GeometryProxy
#Binding var mapType: MKMapType
var body: some View {
Picker("", selection: $mapType) { // new to end
Text("Default").tag(MKMapType.standard)
Text("Transit").tag(MKMapType.hybrid)
Text("Satellite").tag(MKMapType.satellite)
}
.pickerStyle(SegmentedPickerStyle())
.offset(y: -35)
.font(.largeTitle)
VStack (alignment: .leading) {
ShowMoreDetails(item: item)
.navigationBarTitle("Transaction Details", displayMode: .inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

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