SwiftUI: Trigger view update after user pans the map - swiftui

I'm new to Swift/SwiftUI, and so please forgive me if this is trivial.
In an app I am attempting to retrieve the user's location, and fetch nearby websites from Wikipedia to display. A simple example is below:
//
// ContentView.swift
// MRE
//
// Created by Philipp Maier on 2/25/22.
//
import SwiftUI
import CoreLocationUI
import MapKit
import Foundation
struct ContentView: View {
enum ViewState {
case waiting, fetching
}
#State private var viewState = ViewState.waiting
#StateObject private var viewModel = LocationViewModel()
// some values to initialize
#State var listEntries = [
Geosearch(pageid: 45348219,
title: "Addison Apartments",
lat: 35.21388888888889,
lon: -80.84472222222222,
dist: 363.7 ),
Geosearch(pageid: 35914731,
title: "Midtown Park (Charlotte, North Carolina)",
lat: 35.2108,
lon: -80.8363,
dist: 1034.5 )
]
var body: some View {
NavigationView{
VStack{
ZStack(alignment: .leading){
Map(coordinateRegion: $viewModel.mapRegion, showsUserLocation: true)
.frame(height: 300)
LocationButton(.currentLocation, action: {
viewModel.requestAllowLocationPermission()
viewState = .fetching
print("Button Press: Latitude: \(viewModel.mapRegion.center.latitude), Longitude: \(viewModel.mapRegion.center.longitude)")
})
}
HStack{
Text("Latitude: \(viewModel.mapRegion.center.latitude), Longitude: \(viewModel.mapRegion.center.longitude)")
}
switch viewState {
case .waiting:
Text("Waiting for your location")
Spacer()
case .fetching:
List{
ForEach(listEntries) {location in
NavigationLink(destination: Text("Target") ) {
HStack(alignment: .top){
Text(location.title)
}
}
.task{
await fetchNearbyLandmarks(lat: viewModel.mapRegion.center.latitude, lon: viewModel.mapRegion.center.longitude)
}
}
}
}
}
}
}
func fetchNearbyLandmarks(lat: Double, lon: Double) async {
let urlString = "https://en.wikipedia.org/w/api.php?action=query&list=geosearch&gscoord=\(lat)%7C\(lon)&gsradius=5000&gslimit=25&format=json"
print(urlString)
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let items = try JSONDecoder().decode(wikipediaResult.self, from: data)
listEntries = items.query.geosearch
print ("Loadingstate: Loaded")
} catch {
print ("Loadingstate: Failed")
}
}
}
final class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: -80.5), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowLocationPermission() {
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let latestLocation = locations.first else {
print("Location Error #1")
return
}
DispatchQueue.main.async {
self.mapRegion = MKCoordinateRegion(center: latestLocation.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location Error #2:")
print(error.localizedDescription)
}
}
struct wikipediaResult: Codable {
let batchcomplete: String
let query: Query
}
struct Query: Codable {
let geosearch: [ Geosearch ]
}
struct Geosearch: Codable, Identifiable {
var id: Int { pageid }
let pageid: Int
let title: String
let lat: Double
let lon: Double
let dist: Double
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The core function of the app works, in that once the user pushes the "Location" button, the list of Wikipedia pages underneath updates. I've also added to Text fields to track the center of the map as the user pans around.
Here's my issue: If I pan the map, the list of locations underneath does not update. However, once I click on a link to display the page, and hit the "back" button, the updated list is built "correctly", i.e. with the new coordinates.
I'm sure that I'm overlooking something simple, but how can I track panning of the user and dynamically adjust the list underneath "in real time"?
Thanks, Philipp

For being new to Swift/SwiftUI this is quite cool!
You want to refetch when the coordinates of your Map change. So you could use .onChange, e.g.on the ZStack of the Map:
.onChange(of: viewModel.mapRegion.center.latitude) { _ in
Task {
await fetchNearbyLandmarks(lat: viewModel.mapRegion.center.latitude, lon: viewModel.mapRegion.center.longitude)
}
}
Generally this works, but it might be fetching too often (on every change of latitude). So you might want to add some kind of delay, or try to figure out when the drag ended.

Related

SwiftUI - showing Annotation with dynamic colors on Map with MapKit

hopefully someone can help me with my problem. I have an app where I show a Map with the current location of the user. Then I can tap on a button and it shows all gas stations around the location based on a certain radius. The stations are showed by little map pins. Now I want that the cheapest station has another color than the rest (yellow instead of red). The function for this is already written, but the problem is, that sometimes there is no map pin yellow or it is the wrong one which is yellow. The first tap on the button after the app starts is always good, but the following can be sporadic right or wrong. Here is my code.
Part of my MapView:
#ObservedObject var locationManager = LocationManager.shared
#EnvironmentObject var dataViewModel:DataViewModel
#EnvironmentObject var carViewModel:CarViewModel
#State private var radius: String = ""
#State private var showInput: Bool = false
var body: some View {
ZStack {
Map(coordinateRegion: $locationManager.region, interactionModes: .all, showsUserLocation: true, annotationItems: dataViewModel.annotations, annotationContent: { station in
MapAnnotation(coordinate: station.coordinate) {
if dataViewModel.annotations.count > 0 {
MapAnnotationView(dataViewModel: dataViewModel, station: station)
.onTapGesture {
dataViewModel.currentAnnotation = station
dataViewModel.showStationSheet = true
}
}
}
} )
.accentColor(Color.blue)
.ignoresSafeArea()
This is the function where I get my data from:
guard let url = URL(string: "https://creativecommons.tankerkoenig.de/json/list.php?lat=\(latitude)&lng=\(longitude)&rad=\(radius)&sort=dist&type=all&apikey=\(apiKey)") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if error == nil {
if let data = data {
do {
let decodedResponse = try JSONDecoder().decode(HeadData.self, from: data)
DispatchQueue.main.async {
self.gasData.append(decodedResponse)
self.bestAnnotations = [Annotation]()
for bestStation in self.gasData[0].stations {
var id = UUID()
self.annoIDs.append(id)
var tempAnnotation = Annotation(id: id, name: bestStation.name, brand: bestStation.brand, street: bestStation.street, houseNumber: bestStation.houseNumber, postCode: bestStation.postCode, place: bestStation.place, distance: bestStation.dist, diesel: bestStation.diesel ?? 9.999, e5: bestStation.e5 ?? 9.999, e10: bestStation.e10 ?? 9.999, isOpen: bestStation.isOpen, address: bestStation.street, coordinate: CLLocationCoordinate2D(latitude: bestStation.lat, longitude: bestStation.lng))
self.bestAnnotations.append(tempAnnotation)
}
self.calculateBestAnnotation(activeCar: activeCar)
var i = 0
for station in self.gasData[0].stations {
var tempAnnotation = Annotation(id: self.annoIDs[i], name: station.name, brand: station.brand, street: station.street, houseNumber: station.houseNumber, postCode: station.postCode, place: station.place, distance: station.dist, diesel: station.diesel ?? 9.999, e5: station.e5 ?? 9.999, e10: station.e10 ?? 9.999, isOpen: station.isOpen, address: station.street, coordinate: CLLocationCoordinate2D(latitude: station.lat, longitude: station.lng))
i += 1
self.copiedAnnotations.append(tempAnnotation)
}
self.annotations = self.copiedAnnotations
}
} catch let jsonError as NSError {
DispatchQueue.main.async {
self.searchToastError = "Es konnten keine Daten gefunden werden."
self.presentSearchToast = true
}
}
return
}
}
}
.resume()
At first I am saving the decoded json response into an array and I calculate the cheapest gas station so that there is one element left in the array bestAnnotations. After that I append the data to the Annotation Array which is the data source of the Annotations on the MapView.
And then my MapAnnotationView looks like this:
#ObservedObject var locationManager = LocationManager.shared
#ObservedObject var dataViewModel:DataViewModel
#State var station: Annotation
var body: some View {
ZStack {
Circle()
.frame(width: 35, height: 35)
.foregroundColor(station.id == dataViewModel.bestAnnotations[0].id ? .yellow : .red)
Image(systemName: "mappin")
.resizable()
.scaledToFit()
.foregroundColor(.white)
.frame(width: 25, height: 25)
}
}
Hopefully someone can help me with the problem. Maybe there is there something wring with the Dispatch function?

swiftUI Mapkit and Corelocation causing problems

I'm reposting my question of yesterday and now adding a clean code example to demonstrate the problem
I have a MyCustomMapView, embedding a MKMApView and it starts at a fixed location. I have a function called gotoCoordinate, which accepts a coordinate and then navigates the mapview's center to that coordinate.
In the sample code that can be simulated by clicking on the red button labelleing "Click here to change map position".
This all works great. Until....
in the app I'm working on I also need to have a user location so I have a LocationViewModel handling the request. Once you have given request to access your location, click the button no longer moves the center of the map to that new coordinate.
Once you comment the #StateObject var locationViewModel = LocationViewModel() it is working again.
So it seems that once you are using a location manager with a delegate the map no longer moves when changing it's region
Is this a bug or am I doing something wrong?
import SwiftUI
struct ContentView: View {
#StateObject var locationViewModel = LocationViewModel()
var body: some View {
switch locationViewModel.authorizationStatus {
case .notDetermined:
AnyView(RequestLocationView())
.environmentObject(locationViewModel)
case .restricted:
ErrorView(errorText: "Location use is restricted.")
case .denied:
ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
default:
EmptyView()
}
GeometryReader { geometry in
DisplayMapView(size:geometry.size)
}
}
}
import SwiftUI
import CoreLocation
import MapKit
struct MyCustomMapView: UIViewRepresentable {
var map = MKMapView() // << constructor contract !!
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.showsUserLocation = true
map.showsCompass = true
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
map.setRegion(region, animated: true)
return map
}
func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
map.setRegion(region, animated: true)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MyCustomMapView.Coordinator {
return MyCustomMapView.Coordinator(parent1: self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent:MyCustomMapView
init(parent1:MyCustomMapView){
parent = parent1
}
}//class Coordinator
}
import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
#Environment(\.presentationMode) var presentationMode
var size: CGSize
var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
var map = MyCustomMapView()
var body: some View {
ZStack(alignment:.top){
map
VStack(alignment:.leading){
HStack {
HStack {
Text("Click here to change map position")
.onTapGesture(){
map.gotoCoordinate(startCoordinate)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.black)
.background(Color(.red))
.cornerRadius(10.0)
}
}.padding(.top,50).padding(.leading,20).padding(.trailing,20)
}.ignoresSafeArea()
}
}
import Foundation
import SwiftUI
import CoreLocation
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var authorizationStatus: CLAuthorizationStatus
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
private let locationManager: CLLocationManager
static let shared = LocationViewModel()
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 0.4
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastSeenLocation = locations.first
}
}
struct RequestLocationView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
var body: some View {
VStack(spacing:50) {
Image(systemName: "location.circle")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
Button(action: {
locationViewModel.requestPermission()
}, label: {
Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
})
.padding(10)
.foregroundColor(.white)
.background(.green)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("We need your permission to give you the best experience.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
struct ErrorView: View {
var errorText: String
var body: some View {
VStack {
Image(systemName: "xmark.octagon")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
Text(errorText)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Declare your coordinates as a stateful variable, either as #State or as #Published within an observable object:
struct DisplayMapView: View {
#State var coordinates = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
Then pass the coordinates in as an argument to your view - no need to store your view as a variable:
ZStack(alignment: .top) {
MyMapView(coordinates: coordinates)
VStack(alignment: .leading) {
// etc.
Then you’ll need to do some rejigging in your UIViewRepresentable. You mustn't retain map as a separate instance outside makeUIView and updateUIView - SwiftUI structs can be recreated at will, so that would release your MKMapView instance and create a new one. Instead, the object returned by makeUIView is retained for you by the system. You do need to declare a variable that will accept the coordinates argument above, and then respond to any changes in it in updateUIView.
struct MyMapView: UIViewRepresentable {
var coordinates: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
// etc.
return map
}
func updateUIView(_ uiView: MKMapView, context: Coordinator) {
let region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
uiView.setRegion(region, animated: true)
}
}
Now, when the user taps, instead of calling a function inside your view, you update the DisplayMapView’s coordinates variable and the UIViewRepresentable’s update logic should redraw the map in the correct position.

How to transfer data from map annotation to fullscreencover popup

I'm trying to transfer data after user click MapAnnotation. Currently after user click MapAnnotation it add data to selectedCourse and prints it before going if let course = selectedCourse. But some some reason selectedCourse is empty inside .fullScreenCover if statement
import SwiftUI
import MapKit
struct CourseMapView: View {
#ObservedObject var viewModel: CourseSearchViewModel
#State var isShowSheet = false
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 60.480960,
longitude: 22.239808),
span: MKCoordinateSpan(latitudeDelta: 0.1,
longitudeDelta: 0.1))
#State var selectedCourse: Course? = nil
func setCurrentLocation() {
region = MKCoordinateRegion(center: viewModel.location?.coordinate ?? CLLocationCoordinate2D(latitude: 60.480960, longitude: 22.239808), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
}
var body: some View {
ZStack {
if viewModel.location != nil {
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: nil, annotationItems: viewModel.courses) { course in
MapAnnotation(coordinate: .init(latitude: course.location.latitude, longitude: course.location.longitude)) {
Image(systemName: "person")
.frame(width: 44, height: 44)
.onTapGesture(count: 1, perform: {
print("PRINT: \(course.name)")
selectedCourse = course
if selectedCourse != nil {
isShowSheet.toggle()
}
print("\(selectedCourse)")
})
}
}
.ignoresSafeArea()
} else {
Text("locating user location")
}
}
.fullScreenCover(isPresented: $isShowSheet, content: {
if let course = selectedCourse {
LocationInfoView(viewModel: LocationInfoViewModel(course: course))
.environment(\.showingSheet, self.$isShowSheet)
}
})
.alert(item: $viewModel.alertItem, content: { alertItem in
Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
})
.onAppear {
setCurrentLocation()
}
}
}
You should use an other signature of .fullScreenCover :
func fullScreenCover<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, #ViewBuilder content: #escaping (Item) -> Content)
In this way :
.fullScreenCover(item: $selectedCourse) { course in
// content
}
In your example, item: will be a Course?. When you pass a Course the fullScreenCover is presented ; nil and it's closed.
So you could use a #Binding in your LocationInfoView to dismiss the fullScreenCover. But it looks like you prefer to use an EnvironmentKey. You must therefore modify it:
private struct SelectedCourseKey: EnvironmentKey {
static let defaultValue: Binding<Course?> = .constant(nil)
}
extension EnvironmentValues {
var selectedCourse: Binding<Course?> {
get { self[SelectedCourseKey.self] }
set { self[SelectedCourseKey.self] = newValue }
}
}
In your LocationInfoView:
struct LocationInfoView: View {
#Environment(\.selectedCourse) var selectedCourse: Binding<Course?>
var viewModel: LocationInfoViewModel
var body: some View {
VStack {
Button("close") {
selectedCourse.wrappedValue = nil
}
// some code
}
}
}
And finally, in your CourseMapView :
.fullScreenCover(item: $selectedCourse) { course in
if let course = course {
LocationInfoView(viewModel: LocationInfoViewModel(course: course))
.environment(\.selectedCourse, self.$selectedCourse)
}
}

SwiftUI - Location in API call with CLLocationManager and CLGeocoder

I'm struggling with this for a long time without finding where I'm wrong (I know I'm wrong).
I have one API call with the location of the phone (this one is working), but I want the same API call with a manual location entered by a textfield (using Geocoding for retrieving Lat/Long). The geocoding part is ok and updated but not passed in the API call.
I also want this API call to be triggered when the TextField is cleared by the dedicated button back with the phone location.
Please, what am I missing? Thanks for your help.
UPDATE: This works on Xcode 12.2 beta 2 and should work on Xcode 12.0.1
This is the code:
My Model
import Foundation
struct MyModel: Codable {
let value: Double
}
My ViewModel
import Foundation
import SwiftUI
import Combine
final class MyViewModel: ObservableObject {
#Published var state = State.ready
#Published var value: MyModel = MyModel(value: 0.0)
#Published var manualLocation: String {
didSet {
UserDefaults.standard.set(manualLocation, forKey: "manualLocation")
}
}
#EnvironmentObject var coordinates: Coordinates
init() {
manualLocation = UserDefaults.standard.string(forKey: "manualLocation") ?? ""
}
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
private var url: URL {
get {
return URL(string: "https://myapi.com&lat=\(coordinates.latitude)&lon=\(coordinates.longitude)")!
}
}
let urlSession = URLSession.shared
var dataTask: AnyPublisher<MyModel, Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load(){
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("⚠️ API Call finished")
break
case let .failure(error):
print("❌ API Call failure")
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.value = value
print("👍 API Call loaded")
}
))
}
}
The Location Manager
import Foundation
import SwiftUI
import Combine
import CoreLocation
import MapKit
final class Coordinates: NSObject, ObservableObject {
#EnvironmentObject var myViewModel: MyViewModel
#Published var latitude: Double = 0.0
#Published var longitude: Double = 0.0
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
deinit {
locationManager.stopUpdatingLocation()
}
}
extension Coordinates: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
self.placemark = places?[0]
})
self.locationManager.stopUpdatingLocation()
}
}
extension Coordinates {
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
}
The View
import Foundation
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
#EnvironmentObject var coordinates: Coordinates
private var icon: Image { return Image(systemName: "location.fill") }
var body: some View {
VStack{
VStack{
Text("\(icon) \(coordinates.placemark?.locality ?? "Unknown location")")
Text("Latitude: \(coordinates.latitude)")
Text("Longitude: \(coordinates.longitude)")
}
VStack{
Text("UV Index: \(myViewModel.value.value)")
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
HStack{
TextField("Manual location", text: $myViewModel.manualLocation)
if !myViewModel.manualLocation.isEmpty{
Button(action: { clear() }) { Image(systemName: "xmark.circle.fill").foregroundColor(.gray) }
}
}
}.padding()
}
func commit() {
coordinates.getLocation(from: self.myViewModel.manualLocation) { places in
coordinates.latitude = places?.latitude ?? 0.0
coordinates.longitude = places?.longitude ?? 0.0
}
myViewModel.load()
}
func clear() {
myViewModel.manualLocation = ""
myViewModel.load()
}
}

How to put a button/view on top of the SwiftUI Map?

I can't find a way to get my buttonView (just a Button) on top of the map so I can tap it.
In another more complicated setup, the button somehow turns-up on top and I can tap it, but this is by luck not by design. How to get my buttonView on the map so I can tap it?
Note, I think the issue maybe that my buttonView is "under" some map layer, hence the map captures the tap events and does not pass them to my buttonView.
Xcode 12 beta-3, mac catalina, target ios 14.
import Foundation
import SwiftUI
import MapKit
import CoreLocation
#main
struct TestMapApp: App {
var body: some Scene {
WindowGroup {
MapViewer()
}
}
}
struct MapViewer: View {
#State var cityAnno = [CityMapLocation(title: "Tokyo", subtitle: "Japan", lat: 35.685, lon: 139.7514)]
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 35.685, longitude: 139.7514),span: MKCoordinateSpan(latitudeDelta: 1.0, longitudeDelta: 1.0))
var body: some View {
Map(coordinateRegion: $region, annotationItems: cityAnno) { city in
MapAnnotation(coordinate: city.coordinate) {
buttonView(cityName: city.title!)
// tried this, does not work
// Image(systemName:"dot.circle.and.cursorarrow").foregroundColor(.white).scaleEffect(2.2)
// .onTapGesture { print("----> onTapGesture") }
}
}
}
func buttonView(cityName: String) -> some View {
Button(action: {print("----> buttonView action")}) {
VStack {
Text(cityName)
Image(systemName: "dot.circle.and.cursorarrow")
}.foregroundColor(.red).scaleEffect(1.2)
}.frame(width: 111, height: 111)
// tried combinations of these, without success
// .background(Color.gray).opacity(0.8)
// .border(Color.white)
// .contentShape(Rectangle())
// .clipShape(Rectangle())
// .zIndex(1)
// .buttonStyle(PlainButtonStyle())
// .layoutPriority(1)
// .allowsHitTesting(true)
// .onTapGesture {
// print("----> onTapGesture")
// }
}
}
class CityMapLocation: NSObject, MKAnnotation, Identifiable {
var id = UUID().uuidString
var title: String?
var subtitle: String?
dynamic var coordinate: CLLocationCoordinate2D
init(title: String?, subtitle: String?, lat: Double, lon: Double) {
self.id = UUID().uuidString
self.title = title
self.subtitle = subtitle
self.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
}
Just wrap them in ZStack:
ZStack {
Map(coordinateRegion: $region, annotationItems: cityAnno){...}
Button(action: {print("----> buttonView action")}) {...}
}
You could get the onTapGesture on Just Vstack. Try bellow code by replacing body of MapViewer.
var body: some View {
Map(coordinateRegion: $region, annotationItems: cityAnno) { city in
MapAnnotation(coordinate: city.coordinate) {
VStack {
Text(city.title ?? "")
Image(systemName: "dot.circle.and.cursorarrow")
}
.foregroundColor(.red).scaleEffect(1.2)
.frame(width: 111, height: 111)
.onTapGesture {
print("Clicked")
}
}
}
}