I am trying to build an app which loops through overlay tiles. The problem is the map tiles take forever to reload when the map displays. What is the best way around this issue? I'm not sure if it is a caching issue which I think MapKit does itself. My guess is it is a Swift redrawing issue. My code is below, and I appreciate your help.
// Copyright 2020 Oklahoma Weather Blog
//
import SwiftUI
import MapKit
import SwiftSoup
/*
struct RadarMapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
var tileRenderer = MKOverlayRenderer()
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
guard let tileOverlay = overlay as? MKTileOverlay else {
return MKOverlayRenderer()
}
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
}
func makeUIView(context: Context) -> MKMapView {
var template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
template = "https://tilecache.rainviewer.com/v2/ radar/1600575600/512/{z}/{x}/{y}/6/0_1.png"
let overlay = MKTileOverlay(urlTemplate:template)
overlay.canReplaceMapContent = false
let mapView = MKMapView(frame: .zero)
var renderedOverlay = MKTileOverlayRenderer(tileOverlay: overlay)
mapView.addOverlay(overlay, level: .aboveLabels)
mapView.setNeedsDisplay()
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.3, longitudeDelta: 0.3)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
}
*/
func getTimesURL() -> [String] {
let myURLString = "https://api.rainviewer.com/public/maps.json"
guard let myURL = URL(string: myURLString) else {
printToConsole("Error: \(myURLString) doesn't seem to be a valid URL")
return []
}
do {
let myHTMLString = try String(contentsOf: myURL)
do {
let doc: Document = try SwiftSoup.parse(myHTMLString)
let text = try doc.text()
let resultArray = text.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
.components(separatedBy:",")
return resultArray
} catch Exception.Error( _, let message) {
printToConsole(message)
} catch {
printToConsole("error")
}
} catch let error as NSError {
printToConsole("Error: \(error)")
}
return []
}
func getOverlays() -> [MKTileOverlay] {
var overlays: [MKTileOverlay] = []
for time in getTimesURL() {
let template = "https://tilecache.rainviewer.com/v2/radar/\(time)/256/{z}/{x}/{y}/7/1_1.png"
let overlay = MKTileOverlay(urlTemplate:template)
overlays.append(overlay)
}
return overlays
}
struct RadarView: View {
private static let mapStyles: [MKMapType] = [.hybrid, .hybridFlyover, .mutedStandard, .satellite, .satelliteFlyover, .standard]
#State var mapTime = "1600585200"
let cache = NSCache<NSString, MKTileOverlay>()
#AppStorage("selectedMapStyle") var selectedMapStyle = 0
#State private var showingSheet = false
private static var overlayArray: [MKTileOverlay] {
getOverlays()
}
private static var timeArray: [String] {
getTimesURL()
}
func dateToString(_ epoch: String) -> String{
let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "MM/dd hh:mm a"
let date = Date(timeIntervalSince1970: TimeInterval(Int(epoch)!))
return dateFormatterPrint.string(from: date)
}
#State private var timeIndex: Double = 0
#State private var loopMap: Bool = false
#State var radarTimer: Timer?
var body: some View {
VStack{
ZStack(alignment: .top) {
RadarMapView(mapStyle: RadarView.mapStyles[selectedMapStyle], overlay: RadarView.overlayArray[Int(timeIndex)]).edgesIgnoringSafeArea(.bottom)
HStack{
VStack{
Slider(value: $timeIndex.animation(.linear), in: 0...9, step: 1)
Text("\(dateToString(RadarView.timeArray[Int(timeIndex)]))")
}
Button(action: {
loopMap.toggle()
if loopMap {
radarTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
// do something here
if RadarView.overlayArray.count > 0 {
withAnimation{
timeIndex = Double((Int(timeIndex) + 1) % RadarView.overlayArray.count )
}
}
}
} else {
radarTimer?.invalidate()
}
}, label: {
if !loopMap { Text("Loop") }
else { Text("Pause") }
})
}.padding(.horizontal).padding(.horizontal).background(Color.init(UIColor.systemBackground).opacity(0.75))
}
HStack(){
Spacer()
Button(action: {
/*
selectedMapStyle = (selectedMapStyle + 1) % mapStyles.count
*/
showingSheet.toggle()
}, label: {
Image(systemName: "map.fill").resizable()
.renderingMode(.template)
.font(.title)
.foregroundColor(Color.primary)
.frame(width: 20, height: 20)
}).padding()
}.padding(.horizontal).padding(.bottom).background(Color.init(UIColor.systemBackground).opacity(0.75))
}.actionSheet(isPresented: $showingSheet) {
ActionSheet(title: Text("What map style do you want?"), message: Text("Please select one option below"), buttons: [
.default(Text("Muted")) { self.selectedMapStyle = 2 },
.default(Text("Satellite")) { self.selectedMapStyle = 3 },
.default(Text("Satellite w/ Roads")) { self.selectedMapStyle = 0 },
.default(Text("Satellite 3-D")) { self.selectedMapStyle = 4 },
.default(Text("3-D Satellite w/ Roads")) { self.selectedMapStyle = 1 },
.default(Text("Standard")) { self.selectedMapStyle = 5 },
.cancel(Text("Dismiss"))
])
}.edgesIgnoringSafeArea(.bottom).navigationBarTitle("Radar")
}
}
struct RadarMapView: UIViewRepresentable {
var mapStyle: MKMapType
var overlay: MKTileOverlay
class Coordinator: NSObject, MKMapViewDelegate {
var parent: RadarMapView
init(_ parent: RadarMapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKTileOverlayRenderer(overlay: overlay)
return renderer
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ mapView: MKMapView, context: Context) {
// var template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
//1600626600
mapView.mapType = self.mapStyle
mapView.delegate = context.coordinator
let overlays = mapView.overlays
mapView.addOverlay(overlay)
let regionRadius: CLLocationDistance = 50000
let location = CLLocationCoordinate2D(latitude: 33.7490, longitude: -84.3880)
let coordinateRegion = MKCoordinateRegion(center: location,
latitudinalMeters: regionRadius * 2.0, longitudinalMeters: regionRadius * 2.0)
mapView.setRegion(coordinateRegion, animated: true)
for overlay in overlays {
// remove all MKPolyline-Overlays
if overlay is MKTileOverlay {
mapView.removeOverlay(overlay)
}
}
}
}
struct RadarView_Previews: PreviewProvider {
static var previews: some View {
RadarView()
}
}
I had a similar problem a few days ago (with a different tile server) and solved it by using 512px tiles.
My theory is, that almost nobody uses 256px tiles, so servers don't cache them.
Do something like
func getOverlays() -> [MKTileOverlay] {
var overlays: [MKTileOverlay] = []
for time in getTimesURL() {
let template = "https://tilecache.rainviewer.com/v2/radar/\(time)/512/{z}/{x}/{y}/7/1_1.png"
let overlay = MKTileOverlay(urlTemplate:template)
overlay.tileSize = CGSize(width: 512, height: 512)
overlays.append(overlay)
}
return overlays
}
Related
I want to show different tracks by pressing different buttons. I started off with trying to show the polyline for the first track by pressing a button.
If I put the "construction" of the polyline in the makeUIView function of my UI Wrapper, I can see it in the map.
However I only want to show it after pressing a button. So I put the polyline-related code in the updateUIView-function and designed the button with the help of an Observable Object class. Unfortunately it doesn't work.
Here's what I got so far (The lat1 and long1 Array are actually much longer):
import SwiftUI
import MapKit
import CoreLocation
// MARK: ObservableObject
class MapViewModel: ObservableObject {
#Published var didPressTrack01 = false
}
struct MapView: UIViewRepresentable {
// MARK: UIViewRepresentable
#ObservedObject var viewModel: MapViewModel
#Binding var region: MKCoordinateRegion
let track01_lat = "49.0228176937071,49.022820134536,49.0228225788423"
let track01_long = "8.43144629937264,8.43144455316597,8.43144281824249"
var lat1 : [Double] {
return track01_lat.components(separatedBy: ",").compactMap(CLLocationDegrees.init)
}
var lon1 : [Double] {
return track01_long.components(separatedBy: ",").compactMap(CLLocationDegrees.init)
}
var trackCoordinates1 : [CLLocationCoordinate2D] {
var LocationsArray = [CLLocationCoordinate2D]()
for i in 0..<lat1.count {
LocationsArray.append(CLLocationCoordinate2D(latitude: lat1[i], longitude: lon1[i]))
}
return LocationsArray
}
let map = MKMapView()
///Creating map view at startup
func makeUIView(context: Context) -> MKMapView {
let map = context.coordinator.mapView
map.setRegion(region, animated: false)
map.showsUserLocation = true
map.delegate = context.coordinator
return map
}
func updateUIView(_ view: MKMapView, context: Context) {
if viewModel.didPressTrack01 == true {
let polyline = MKPolyline(coordinates: trackCoordinates1, count: trackCoordinates1.count)
map.addOverlay(polyline)
}
}
///Use class Coordinator method
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
//MARK: - Core Location manager delegate
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
var mapView = MKMapView()
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let routePolyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: routePolyline)
renderer.strokeColor = UIColor.systemRed
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer()
}
}
// MARK: View that shows map to users
struct Tap_Map_View: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude:20.02281, longitude: 8.43188), latitudinalMeters: 180, longitudinalMeters: 180)
#StateObject var viewModel = MapViewModel()
var body: some View {
ZStack(alignment: .bottom){
MapView(viewModel: viewModel, region: $region)
Button {
viewModel.didPressTrack01.toggle()
} label: {
Label("", systemImage: "point.topleft.down.curvedto.point.bottomright.up")
}
}
}
}
I have a problem with an app.
I wrote it last week and it worked on both the simulator and my iPhone.
Now I wanted to make a few small changes today, but every time I try to simulate them, I get the error:
In the simulator:
"The operation couldn't be completed. (KCLErrorDomain error 1.)"
On my iPhone:
"QUARANTINED DUE TO HIGH LOGGING VOLUME"
although I haven't changed anything in the code and always ask the user for permission whether the app is allowed to use the location.
Whenever I start the app, my location is shown in the window in which I am asked for permission and if I then click on "Only use once", I no longer see my location and the app does not react to gestures etc.
Here is my code, it would be great if someone had an idea what the problem is, since I've googled a lot and tried, but somehow nothing seems to work.
p.s.: sorry for no commentary :X
MapViewModel:
import SwiftUI
import MapKit
import CoreLocation
var region1 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.503117, longitude: 13.348901), radius: 20, identifier: "region1")
var region2 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504829, longitude: 13.336798), radius: 20, identifier: "region2")
var region3 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504733, longitude: 13.341834), radius: 20, identifier: "region3")
var region4 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.503312, longitude: 13.347718), radius: 20, identifier: "region4")
var region5 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505699, longitude: 13.352198), radius: 20, identifier: "region5")
var region6 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.513517, longitude: 13.350253), radius: 20, identifier: "region6")
class MapViewModel : NSObject,ObservableObject, CLLocationManagerDelegate, MKMapViewDelegate{
#Published var mapView = MKMapView()
#Published var region : MKCoordinateRegion!
#Published var permissionDenied = false
#Published var alerts = Array(repeating: false, count: 7)
#Published var mapType : MKMapType = .standard
#Published var searchTxt = ""
#Published var places : [Place] = []
#Published var currentPin: MKPointAnnotation = MKPointAnnotation()
#Published var currentmap = CGPoint()
#Published var UserDidZoom: Bool = false;
private let manager = CLLocationManager()
var Waypoints = [region1, region2, region3, region4, region5, region6];
var Pins: [MKPointAnnotation] = []
#Published var RegionIndex: Int = 6
private var mapChangedFromUserInteraction = false
func mapViewRegionDidChangeFromUserInteraction() -> Bool {
let view = mapView.subviews[0]
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if( recognizer.state == UIGestureRecognizer.State.began || recognizer.state == UIGestureRecognizer.State.ended ) {
return true
}
}
}
return false
}
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if (mapChangedFromUserInteraction) {
UserDidZoom = true
mapView.userTrackingMode = .none
}
}
func updateMapType(){
if mapType == .standard{
mapType = .hybrid
mapView.mapType = mapType
}else{
mapType = .standard
mapView.mapType = mapType
}
}
func focusLocation() {
let rangeInMeters: Double = 500
let coordinateRegion = MKCoordinateRegion.init(center: region.center, latitudinalMeters: rangeInMeters,longitudinalMeters: rangeInMeters)
mapView.setRegion(coordinateRegion, animated: true)
}
func searchQuery(){
places.removeAll()
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = searchTxt
MKLocalSearch(request: request).start { (response, _) in
guard let result = response else{return}
self.places = result.mapItems.compactMap({ (item) -> Place? in
return Place(placemark: item.placemark)
})
}
}
func selectPlace(place: Place){
searchTxt = ""
guard let coordinate = place.placemark.location?.coordinate else{return}
let pointAnnotation = MKPointAnnotation()
pointAnnotation.coordinate = coordinate
pointAnnotation.title = place.placemark.name ?? "No Name"
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotation(pointAnnotation)
let coordinateRegion = MKCoordinateRegion(center: coordinate, latitudinalMeters: 200, longitudinalMeters: 200)
mapView.setRegion(coordinateRegion, animated: true)
mapView.setVisibleMapRect(mapView.visibleMapRect, animated: true)
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus{
case .denied:
permissionDenied.toggle()
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
manager.requestLocation()
default:
()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else{return}
self.region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 200, longitudinalMeters: 200)
if(!UserDidZoom){
mapView.setRegion(region, animated: true)
}
self.isWayPoint()
}
func initPositionService(){
let startPointRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 52.506016, longitude: 13.33206), latitudinalMeters: 200, longitudinalMeters: 200)
mapView.setRegion(startPointRegion, animated: true)
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestAlwaysAuthorization()
manager.requestWhenInUseAuthorization()
manager.delegate = self
manager.startUpdatingLocation()
mapView.delegate = self
}
func placeAnnotations(){
let toDoLocation = ["Friseur: Haare schneiden " , "Dönerbude: zu Mittag essen" , "Bank: Geld abheben" , "Supermarkt: einkaufen" , "Blumeneladen: Blumen kaufen" , "Lisa's Wohnung: Lisa Blumen überreichen"]
for i in 0...5 {
let point = MKPointAnnotation()
point.coordinate = Waypoints[i].center
point.title = toDoLocation[i]
Pins.append(point)
}
mapView.addAnnotations(Pins)
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if(region.identifier == "region1"){
self.RegionIndex = 0
}else if(region.identifier == "region2"){
self.RegionIndex = 1
}else if(region.identifier == "region3"){
self.RegionIndex = 2
}else if(region.identifier == "region4"){
self.RegionIndex = 3
}else if(region.identifier == "region5"){
self.RegionIndex = 4
}else if(region.identifier == "region6"){
self.RegionIndex = 5
}
}
func isWayPoint() {
while(self.Waypoints.count > 0){
for i in self.Waypoints {
self.manager.startMonitoring(for: i)
}
}
}
}
MapView
import SwiftUI
import MapKit
import Combine
struct MapView: UIViewRepresentable {
#EnvironmentObject var mapData: MapViewModel
func makeCoordinator() -> Coordinator {
return MapView.Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
let view = mapData.mapView
view.showsUserLocation = true
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
class Coordinator: NSObject, MKMapViewDelegate{
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKind(of: MKUserLocation.self){return nil}
else{
let pinAnnotation = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "PIN_VIEW")
pinAnnotation.tintColor = .blue
pinAnnotation.animatesDrop = true
pinAnnotation.canShowCallout = true
return pinAnnotation
}
}
}
}
Place
import SwiftUI
import MapKit
struct Place: Identifiable{
var id = UUID().uuidString
var placemark: CLPlacemark
}
ContentView
import MapKit
import SwiftUI
import CoreLocation
import AVFoundation
struct ContentView: View {
#StateObject var mapData = MapViewModel()
#State var locationManager = CLLocationManager()
let systemSoundID: SystemSoundID = 1015
var body: some View {
ZStack{
MapView()
.environmentObject(mapData)
.ignoresSafeArea(.all, edges: .all)
VStack{
VStack(spacing : 0){
HStack{
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
TextField("Search", text: $mapData.searchTxt)
.colorScheme(.light)
}
.padding(.vertical, 10)
.padding(.horizontal)
.background(Color.white)
if !mapData.places.isEmpty && mapData.searchTxt != ""{
ScrollView{
VStack(spacing:10){
ForEach(mapData.places){place in
Text(place.placemark.name ?? "")
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading)
.onTapGesture{
mapData.selectPlace(place: place)
}
Divider()
}
}
.padding(.top)
}
.background(Color.white)
}
}
.padding()
Spacer()
VStack{
Button(action:{ mapData.focusLocation(); mapData.UserDidZoom = false; mapData.mapView.userTrackingMode = .follow },label:{
Image(systemName: "location.fill")
.font(.title2)
.padding(10)
.background(Color.primary)
.clipShape(Circle())
})
.opacity(mapData.UserDidZoom ? 1 : 0)
Button(action: mapData.updateMapType ,label:{
Image(systemName: mapData.mapType == .standard ? "network" : "map")
.font(.title2)
.padding(10)
.background(Color.primary)
.clipShape(Circle())
})
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.horizontal, 10)
}
}
.onAppear(perform: {
mapData.initPositionService()
mapData.placeAnnotations()
})
.onChange(of: mapData.RegionIndex) { regionIndex in
var done = Array(repeating: false, count: 7)
if( regionIndex <= 5 ){
if(done[regionIndex] == false){
mapData.currentPin = mapData.Pins[regionIndex]
mapData.alerts[regionIndex] = true
AudioServicesPlayAlertSound(systemSoundID)
mapData.mapView.removeAnnotation(mapData.currentPin)
done[regionIndex] = true
}
}
}
.alert(isPresented: $mapData.alerts[mapData.RegionIndex], content: {
Alert(title: Text("Ziel erreicht"), message: Text("Sie haben \(mapData.currentPin.title!) erledigt"), dismissButton: .default(Text("Ok"), action: {mapData.alerts[mapData.RegionIndex] = false} ))
})
.onChange(of: mapData.searchTxt, perform: { value in
let delay = 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + delay){
if value == mapData.searchTxt{
self.mapData.searchQuery()
}
}
})
}
}
I am implementing Google Maps SDK in a SwiftUI project, In this project I bind markers according with a list of order's locations that I request from server.
I am already binding the markers on map, but now I want to centralize the maps with the right zoom on markers, I already did this on the android app version using bounds, and now I want to do the same on swift. But neither mapView.moveCamera() and mapView.animate() are working.
I am requesting the camera change in updateUIView(_ mapView: GMSMapView, context: Context) after that I bind the markers.
The code:
import SwiftUI
import GoogleMaps
import shared
struct GoogleMapsView: UIViewRepresentable {
private let MARKER_SIZE = 40
private let zoom: Float = 15.0
private let bounds: GMSCoordinateBounds = GMSCoordinateBounds()
#Binding var mappedOrders: Dictionary<Int32, [OrderDTO]>
#Binding var filteredOrders: [OrderItem]
#Binding var showAlert: Bool
#Binding var wichAlert: Int
func makeUIView(context: Self.Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(withLatitude: 40.4637, longitude: 3.7492, zoom: 6.0)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
mapView.clear()
for (key) in mappedOrders.keys {
let orderList: [OrderDTO] = mappedOrders[key]!
orderList.filter { order in
return order.deliveryStateString == "rider_assigned" ||
order.deliveryStateString == "delivery_in_progress" ||
order.deliveryStateString == "pending_rider"
}.forEach{ order in
switch order.deliveryState {
case .deliveryInProgress:
if (order.deliveryLocation != nil) {
createDeliveryPointMarker(order: order).map = mapView
}
break
case .pendingRider:
if (order.deliveryLocation != nil) {
createDeliveryPointMarker(order: order).map = mapView
}
if(order.restaurantLocation != nil) {
createRestaurantMarker(order: order).map = mapView
}
break
case .riderAssigned:
if (order.deliveryLocation != nil) {
createDeliveryPointMarker(order: order).map = mapView
}
if(order.restaurantLocation != nil) {
createRestaurantMarker(order: order).map = mapView
}
break
default:
break
}
}
}
let update = GMSCameraUpdate.fit(bounds)
mapView.animate(with: update)
}
private func createDeliveryPointMarker(order: OrderDTO) -> GMSMarker {
let location = CLLocationCoordinate2DMake(
order.restaurantLocation!.latitude,
order.restaurantLocation!.longitude
)
let marker = GMSMarker(
position: location
)
bounds.includingCoordinate(location)
marker.title = order.user
marker.userData = MarkerDataStored(
order: order,
isClientMarker: true
)
let iconName:String
switch order.deliveryState {
case .pendingRider:
iconName = "flag_red"
break
case .riderAssigned:
iconName = "flag_blue"
break
default:
iconName = "flag_green"
break
}
marker.isTappable = true
marker.icon = resizeImage(
image: UIImage(named: iconName)!,
scaledToSize: CGSize(width: MARKER_SIZE, height: MARKER_SIZE)
)
return marker
}
private func createRestaurantMarker(order: OrderDTO) -> GMSMarker {
let marker = GMSMarker(
position: CLLocationCoordinate2DMake(
order.deliveryLocation!.latitude,
order.deliveryLocation!.longitude
)
)
marker.title = order.user
marker.userData = MarkerDataStored(
order: order,
isClientMarker: false
)
let iconName:String
switch order.deliveryState {
case .pendingRider:
iconName = "restaurant_red"
break
default:
iconName = "restaurant_blue"
break
}
marker.isTappable = true
marker.icon = resizeImage(
image: UIImage(named: iconName)!,
scaledToSize: CGSize(width: MARKER_SIZE, height: MARKER_SIZE)
)
return marker
}
func makeCoordinator() -> MapsDelegate {
let delegate = MapsDelegate()
delegate.onClickMarker = { marker in
filterByRestaurant(marker: marker)
wichAlert = OPEN_RESTAURANT_ORDERS
showAlert = true
}
return delegate
}
private func filterByRestaurant(marker: GMSMarker) {
let data: MarkerDataStored = marker.userData as! MarkerDataStored
filteredOrders.removeAll()
if (data.isClientMarker) {
filteredOrders.append(OrderItem(order: data.order))
} else {
self.mappedOrders[data.order.commercialPremiseId]?.forEach() { order in
filteredOrders.append(OrderItem(order: order))
}
}
}
}
struct MarkerDataStored {
let order: OrderDTO
let isClientMarker: Bool
}
The correct way is :
Change this line
bounds.includingCoordinate(location)
with this
bounds = bounds.includingCoordinate(location)
I have a SwiftUI project in which I want to forward geocode an address to coordinates and update the Mapbox map view to display a pin annotation at that address's coordinates on a button press.
When pressing the button I created, the action seems to work but the map does not show a new pin anywhere even though my annotation array (which is used for pin placement) is updated with the information for the new address.
My content and map view are as follows:
MapView
import SwiftUI
import Mapbox
extension MGLPointAnnotation {
convenience init(title: String, coordinate: CLLocationCoordinate2D) {
self.init()
self.title = title
self.coordinate = coordinate
}
}
struct MapView: UIViewRepresentable {
var mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
#Binding var annotations: [MGLPointAnnotation]
final class Coordinator: NSObject, MGLMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
return nil
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
}
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
return mapView
}
func styleURL(_ styleURL: URL) -> MapView {
mapView.styleURL = styleURL
return self
}
func centerCoordinate(_ centerCoordinate: CLLocationCoordinate2D) -> MapView {
mapView.centerCoordinate = centerCoordinate
return self
}
func zoomLevel(_ zoomLevel: Double) -> MapView {
mapView.zoomLevel = zoomLevel
return self
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
updateAnnotations()
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
private func updateAnnotations() {
if let currentAnnotations = mapView.annotations {
mapView.removeAnnotations(currentAnnotations)
}
mapView.addAnnotations(annotations)
}
}
ContentView
import SwiftUI
import Mapbox
import CoreLocation
struct ContentView: View {
#ObservedObject var VModel = ViewModel()
#ObservedObject var locationManager = LocationManager()
var userLatitude: CLLocationDegrees {
return (locationManager.lastLocation?.coordinate.latitude ?? 0)
}
var userLongitude: CLLocationDegrees {
return (locationManager.lastLocation?.coordinate.longitude ?? 0)
}
var lat: Double {
return (VModel.lat ?? 0)
}
var long: Double {
return (VModel.lon ?? 0)
}
#State var searchedLocation: String = ""
#State var annotations = [
MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267))
]
var body: some View {
VStack{
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)){
MapView(annotations: $annotations).centerCoordinate(.init(latitude: self.lat, longitude: self.long)).zoomLevel(16).edgesIgnoringSafeArea(.all)
VStack{
Button(action: {
self.VModel.fetchCoords(address: "address")
let delayInSeconds = 3.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {
self.annotations.append(MGLPointAnnotation(title: "Home", coordinate: .init(latitude: self.lat, longitude: self.long)))
}
print("\(self.annotations)")
}, label: {Text("Press to update annotation")})
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any information on updating the map to show new pin annotations on a button press is much appreciated. Please comment if more information is needed
Without knowledge of your VModel to LocationManager I stripped them out and just hard-coded some values for some annotations. So although the code below is a much simplified version of yours, you can see the addition of the third annotation when the button is pressed.
It is a straightforward implementation where the view-model (AnnotationsVM) controlling the annotations is referenced as an #ObservedObject in the ContextView. It publishes the array of annotations. These are passed into the #Binding variable in the MapView and when that array changes (additions, deletions, etc) the updateUIView method updates the map.
import SwiftUI
import Mapbox
class MapViewCoordinator: NSObject, MGLMapViewDelegate {
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
}
struct MapView: UIViewRepresentable {
#Binding var annos: [MGLPointAnnotation]
func makeUIView(context: Context) -> MGLMapView {
let map = MGLMapView()
map.styleURL = MGLStyle.outdoorsStyleURL
map.delegate = context.coordinator
let location = CLLocation(latitude: 40.763783, longitude: -73.973133)
map.setCenter(location.coordinate, zoomLevel: 13, animated: true)
return map
}
func updateUIView(_ uiView: MGLMapView, context: Context) {
if let currentAnnotations = uiView.annotations {
uiView.removeAnnotations(currentAnnotations)
}
uiView.addAnnotations(annos)
}
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator()
}
}
struct ContentView: View {
#ObservedObject var annotationsVM = AnnotationsVM()
var body: some View {
NavigationView {
MapView(annos: $annotationsVM.annos)
.navigationBarTitle(Text("Hello"))
.navigationBarItems(trailing: Button(action: {
self.annotationsVM.addNextAnnotation()
}, label: {
Image(systemName: "plus.circle")
}))
}
}
}
class AnnotationsVM: ObservableObject {
#Published var annos = [MGLPointAnnotation]()
init() {
var annotation = MGLPointAnnotation()
annotation.title = "Apple Store"
annotation.coordinate = CLLocationCoordinate2D(latitude: 40.77, longitude: -73.98)
annotation.subtitle = "Think Different"
annos.append(annotation)
annotation = MGLPointAnnotation()
annotation.title = "Shoe Store"
annotation.coordinate = CLLocationCoordinate2D(latitude: 40.78, longitude: -73.98)
annotation.subtitle = "Shoe Different"
annos.append(annotation)
}
func addNextAnnotation() {
let newAnnotation = MGLPointAnnotation()
newAnnotation.title = "New Annotation"
newAnnotation.coordinate = CLLocationCoordinate2D(latitude: 40.763783, longitude: -73.973133)
newAnnotation.subtitle = "Ben Button"
annos.append(newAnnotation)
}
}
I am trying to my data derived from an API into a line chart but I can't seem to get it to work. I am storing the data in an observable object so it takes a few seconds to get it so it won't show up on my graph but when I hardcode data it works I am certain that I am getting the data but it simply won't show up. thanks
struct HomeView: View {
#State var tabIndex:Int = 0
#ObservedObject var homeViewModel = HomeViewModel()
init() {
homeViewModel.getTimelineBy("US")
}
var body: some View {
VStack(alignment: .center) {
TimelineChartView(timelineDataSet: self.$homeViewModel.countryTimeline)
}.frame(height: 500.0)
}
}
struct TimelineChartView: UIViewRepresentable {
#Binding var timelineDataSet: [ChartDataEntry]
func updateUIView(_ uiView: LineChartView, context: UIViewRepresentableContext<TimelineChartView>) {
}
var lineChart = LineChartView()
func makeUIView(context: UIViewRepresentableContext<TimelineChartView>) -> LineChartView {
setUpChart()
return lineChart
}
func setUpChart() {
lineChart.noDataText = "No Data Available"
lineChart.rightAxis.enabled = false
lineChart.backgroundColor = .white
let dataSets = [getLineChartDataSet()]
let yAxis = lineChart.leftAxis
yAxis.labelFont = .boldSystemFont(ofSize: 13)
yAxis.setLabelCount(5, force: false)
yAxis.labelTextColor = .black
yAxis.axisLineColor = .black
yAxis.labelPosition = .outsideChart
lineChart.xAxis.labelPosition = .bottom
lineChart.xAxis.labelFont = .boldSystemFont(ofSize: 13)
lineChart.xAxis.labelTextColor = .black
lineChart.xAxis.axisLineColor = .systemBlue
lineChart.animate(xAxisDuration: 2.5)
lineChart.notifyDataSetChanged()
let data = LineChartData(dataSets: dataSets)
data.setValueFont(.systemFont(ofSize: 7, weight: .black))
lineChart.data = data
}
func getChartDataPoints(selectedTimelineData: [ChartDataEntry]) -> [ChartDataEntry] {
var dataPoints: [ChartDataEntry] = []
for eachTimeline in selectedTimelineData {
let entry = ChartDataEntry(x: eachTimeline.x, y: eachTimeline.y)
dataPoints.append(entry)
}
return dataPoints
}
func getLineChartDataSet() -> LineChartDataSet {
let test = getChartDataPoints(selectedTimelineData: timelineDataSet)
let set = LineChartDataSet(entries: test, label: "DataSet")
set.lineWidth = 4
set.drawCirclesEnabled = false
set.mode = .cubicBezier
set.fillAlpha = 0.9
set.drawFilledEnabled = true
set.highlightColor = .systemRed
return set
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
Let me share some of my codes working. When the binding object is changed the chart will show the data changed.
struct HomeView: View {
#State var barData: [String: Double] = [String: Double]()
var body: some View {
NavigationView {
List {
CustomeView([BarChartVM(arg: self.$barData, title: "BarChart")])
}
}
}
}
struct CustomeView<Page:View>: View {
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
CustomeViewController(controllers: viewControllers)
}
}
struct CustomeViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true)
}
}
struct BarChartVM: UIViewRepresentable {
#Binding var arg: [String: Double]
var title: String = ""
let chart = BarChartView()
func makeUIView(context: UIViewRepresentableContext<BarChartVM>) -> BarChartView {
setUpChart()
return chart
}
func updateUIView(_ uiView: BarChartView, context: UIViewRepresentableContext<BarChartVM>) {
updateChartData()
}
func setUpChart() {
chart.noDataText = "No data available"
let pointArray = arg.sorted(by: <).map { $0.key }
}
func updateChartData() {
var entries = [BarChartDataEntry]()
let valueArray = arg.sorted(by: <).map { $1 }
for i in 0..<valueArray.count {
let entry = BarChartDataEntry(x: Double(i), yValues: [valueArray[i]])
entries.append( entry)
}
let set = BarChartDataSet(entries: entries, label: title)
set.colors = ChartColorTemplates.material()
let data = BarChartData(dataSet: set)
chart.data = data
}
}