Looped value is showing the same result for tap - swiftui

I have imported JSON for countries:
Countries.json (sample)
[
{
display_name: "France",
timezone: "placeholder",
longitude: 13.33,
latitude: 15.34
},
{
display_name: "California",
timezone: "EST",
longitude: 33.33,
latitude: 12.34
},
]
I have a function getAnnotated that iterates through the countries to make an array of AnnotatedItem. That is used in Map and gets looped through as item to actually create the MapAnnotation. Then item is passed into a helper function getCountry. I filter through countries to get the country that has the same display_name field as item.
The desired behavior is to have an annotation/marker over each country and tapping on that annotation will pop up a modal/sheet that gives info on the country.
My issue is that if I am zoomed in and on screen is only a single annotation/marker, the proper country is displayed when clicking on it.
If I zoom out on the map and there are multiples annotations, every annotation I tap pops up the same country info for each one. I assume there is something wrong with the way I am looping.
var countries = Bundle.main.decode("Countries.json")
struct AnnotatedItem: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}
struct MapView: View {
#State var showSheet = false
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 25.7617,
longitude: 80.1918
),
span: MKCoordinateSpan(
latitudeDelta: 10,
longitudeDelta: 10
)
)
func getAnnotated() -> [AnnotatedItem] {
var pointsOfInterest = [AnnotatedItem]()
for i in countries {
pointsOfInterest.append(AnnotatedItem(name: i.display_name, coordinate: .init(latitude: i.latitude, longitude: i.longitude)))
}
return pointsOfInterest
}
func getCountry(newItem: AnnotatedItem) -> Country {
let country = countries.filter{ $0.display_name == newItem.name }
return country[0]
}
var body: some View {
Map(coordinateRegion: $region, annotationItems: getAnnotated()) { item in
MapAnnotation(coordinate: item.coordinate) {
Button(action: {
showSheet.toggle()
}){
Image(systemName: "airplane")
.foregroundColor(.white)
.padding()
}
.background(Circle())
.foregroundColor(Color.green)
.sheet(isPresented: $showSheet) {
SheetView(country: getCountry(newItem: item))
}
}
}
}
}

I would try something like this to achieve the desired behaviour:
class SelectedCountry: ObservableObject {
#Published var item: AnnotatedItem = AnnotatedItem(name: "no name", coordinate: CLLocationCoordinate2D())
}
struct MapView: View {
#ObservedObject var selected = SelectedCountry() // <--- here
#State var showSheet = false
...
var body: some View {
Map(coordinateRegion: $region, annotationItems: getAnnotated()) { item in
MapAnnotation(coordinate: item.coordinate) {
Button(action: {
selected.item = item // <--- here
showSheet.toggle()
}){
Image(systemName: "airplane")
.foregroundColor(.white)
.padding()
}
.background(Circle())
.foregroundColor(Color.green)
}
}
// ---> put the sheet here
.sheet(isPresented: $showSheet) {
SheetView(country: getCountry(newItem: selected.item))
}
}

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?

Touch events seemingly not registering at top of screen

I'm seeing very strange behavior within a view. Here's my layout:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
#available(iOS 15.0, *)
struct EventDetailView: View {
#Binding var event: EventRecord
#Binding var editing: Bool
#FocusState var textIsFocused: Bool
var body: some View {
VStack {
TextField(
"Event text",
text: $event.text
)
.focused($textIsFocused)
.disabled(!editing)
.padding()
DatePicker("Event Date:", selection: $event.date)
.disabled(!editing)
.padding()
Toggle("Goal is Reached?", isOn: $event.achievesKR)
.disabled(!editing)
.padding()
HStack {
Text("Notes:")
Spacer()
}
.padding()
TextEditor(text: $event.notes)
.disabled(!editing)
.padding()
Spacer()
}
}
}
struct EventRecord: Identifiable, Equatable {
typealias ID = Identifier
struct Identifier: Identifiable, Equatable, Hashable {
typealias ID = UUID
let id: UUID = UUID()
}
let id: ID
var keyResults: [KeyResult.ID]
var date: Date
var text: String
var notes: String
var achievesKR: Bool
init(
id: ID = ID(),
keyResults: [KeyResult.ID],
date: Date = Date(),
text: String,
notes: String = "",
achievesKR: Bool
) {
self.id = id
self.keyResults = keyResults
self.date = date
self.text = text
self.notes = notes
self.achievesKR = achievesKR
}
}
So this works perfectly when I run it as an iPad app, but when I run it on the simulator, the the top toggle doesn't respond to text input.
The strange thing is, when I simply duplicate the toggle, the top one doesn't work and the bottom one works perfectly:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
It seems like this should be totally unrelated to the touch behavior of the other views.
Btw this is being displayed in the context of a navigation view.
Is there anything that can explain this? And how can I get it working without adding this extra view on top?
edit: Here's a gif of this behavior being demonstrated. The two controls are exactly the same, but the lower one responds to touch and the upper one does not.

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
}

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

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