Passing data from UIViewRepresentable function to SwiftUI View - swift

The user looks for the delivery address on the map, then the address is identified by the marker located in the middle of the screen. And then the address is obtained through this marker. How to display an address in the user interface ?
struct MapView: UIViewRepresentable {
#Binding var centerCoordinate: CLLocationCoordinate2D
var currentLocation: CLLocationCoordinate2D?
var withAnnotation: MKPointAnnotation?
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
var addressLabel: String = "222"
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.centerCoordinate = mapView.centerCoordinate
}
}
...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
let center = getCenterLocation(for: mapView)
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(center) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
print("error")
return
}
guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
DispatchQueue.main.async {
self.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.addressLabel)
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = self.currentLocation {
if let annotation = self.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = self.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}
I am trying to pass the address to the UI.
What's the most correct way to do this?
In the interface, I want to get the address from an ever-changing variable addressLabel
import SwiftUI
import MapKit
fileprivate let locationFetcher = LocationFetcher()
struct LocationView: View {
#State var centerCoordinate = CLLocationCoordinate2D()
#State var currentLocation: CLLocationCoordinate2D?
#State var annotation: MKPointAnnotation?
var body: some View {
ZStack {
MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
locationFetcher.start()
})
}
.overlay(
ZStack {
Text("\(*MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation).makeCoordinator().addressLabel OMG????*)")
.offset(y: 44)
}
)
}
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
LocationView()
}
}
How can i do this ?
Thanks in advance

Here is one approach. Have a single source of truth that both UIKit and SwiftUI can access.
#available(iOS 15.0, *)
struct LocationView: View {
//It is better to have one source of truth
#StateObject var vm: MapViewModel = MapViewModel()
var body: some View {
ZStack {
MapView(vm: vm)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
//locationFetcher.start() //No Code provided
})
}
.overlay(
HStack{
Spacer()
Text(vm.addressLabel)
Spacer()
//Using offset is subjective since screen sizes change just center it
}
)
//Sample alert that adapts to what is
.alert(isPresented: $vm.errorAlert.isPresented, error: vm.errorAlert.error, actions: {
if vm.errorAlert.defaultAction != nil{
Button("ok", role: .none, action: vm.errorAlert.defaultAction!)
}
if vm.errorAlert.cancelAction != nil{
Button("cancel", role: .cancel, action: vm.errorAlert.cancelAction!)
}
if vm.errorAlert.defaultAction == nil && vm.errorAlert.cancelAction == nil {
Button("ok", role: .none, action: {})
}
})
}
}
//UIKit and SwiftUI will have access to this ViewModel so all the data can have one souce of truth
class MapViewModel: ObservableObject{
//All the variables live here
#Published var addressLabel: String = "222"
#Published var centerCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D()
#Published var currentLocation: CLLocationCoordinate2D? = nil
#Published var withAnnotation: MKPointAnnotation? = nil
#Published var annotation: MKPointAnnotation?
//This tuple variable allows you to have a dynamic alert in the view
#Published var errorAlert: (isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?) = (false, MapErrors.unknown, nil, nil)
//The new alert requires a LocalizedError
enum MapErrors: LocalizedStringKey, LocalizedError{
case unknown
case failedToRetrievePlacemark
case failedToReverseGeocode
case randomForTestPurposes
//Add localizable.strings to you project and add these keys so you get localized messages
var errorDescription: String?{
switch self{
case .unknown:
return "unknown".localizedCapitalized
case .failedToRetrievePlacemark:
return "failedToRetrievePlacemark".localizedCapitalized
case .failedToReverseGeocode:
return "failedToReverseGeocode".localizedCapitalized
case .randomForTestPurposes:
return "randomForTestPurposes".localizedCapitalized
}
}
}
//Presenting with this will ensure that errors keep from gettting lost by creating a loop until they can be presented
func presentError(isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?, count: Int = 1){
//If there is an alert already showing
if errorAlert.isPresented{
//See if the current error has been on screen for 10 seconds
if count >= 10{
//If it has dismiss it so the new error can be posted
errorAlert.isPresented = false
}
//Call the method again in 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let newCount = count + 1
self.presentError(isPresented: isPresented, error: error, defaultAction: defaultAction, cancelAction: cancelAction, count: newCount)
}
}else{
errorAlert = (isPresented, error, defaultAction, cancelAction)
}
}
}
struct MapView: UIViewRepresentable {
#ObservedObject var vm: MapViewModel
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.vm.centerCoordinate = mapView.centerCoordinate
}
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
getAddress(center: mapView.centerCoordinate)
//Just to demostrate the error
//You can remove this whenever
#if DEBUG
if Bool.random(){
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.randomForTestPurposes, defaultAction: nil, cancelAction: nil)
}
#endif
}
//Gets the addess from CLGeocoder if available
func getAddress(center: CLLocationCoordinate2D){
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(CLLocation(latitude: center.latitude, longitude: center.longitude)) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
print("error")
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToReverseGeocode, defaultAction: nil, cancelAction: nil)
return
}
guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToRetrievePlacemark, defaultAction: nil, cancelAction: nil)
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
DispatchQueue.main.async {
self.parent.vm.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.parent.vm.addressLabel)
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = vm.currentLocation {
if let annotation = vm.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = vm.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}

Related

Mapbox Maps SDK for iOS - Maps won't display anymore (or gets cut off)

Maps (from MapBox API) were working just fine in our app, but about a month they stopped displaying on a swiftui view. BUT the tricky thing is that the maps in some phones still work (or works partially but being cut off), and they still show the annotations/pins (see screenshot).
We have the MapBox Maps SDK for iOS installed as cocoapods into Xcode.
It's a simple app, it just detects a vehicle trip and add event points to the map (eg. point A as trip start, and point B as trip end).
Versions:
Xcode: 13.2.1
pod 'Mapbox-iOS-SDK', '~> 6.4.1'
pod 'MapboxNavigation', '~> 1.4.2'
Basically, the following is our main code to show the map.
Note: It also adds speeding, hard brake and rapid accelaration annotations to the map.
Does someone have any idea why the map is not showing properly?
Thanks much in advance.
import SwiftUI
import Mapbox
import MapboxDirections
import Polyline
struct TestMapDriveView: View {
var selectedTrip: VehicleTripData // The selected trip that needs to be shown
#Binding var speedingCount: Int? // the number of speeding events in the selected trip
#Binding var showCallouts: Bool // Whether to show callout when a point annotation is selected
#Binding var showInfoSheet: Bool // whether show the event details view or not (show the details when the user tap on a specific marker on the map)
#Binding var isFullScreen: Bool // whether show full screen map or not
#Binding var selectedEncodedPolyline: String // The encoded polyline for the trip
#State var alreadyClean: Bool = false
#State var changed: Bool = false
#State var showSpecificPoint = false // Whether focues on a specific trip point or not
let action: () -> () // Used to toggle the full screen state variable
var body: some View {
VStack {
NavigationLink(destination: EmptyView(), label: {EmptyView()})
NavigationLink(destination: EventDetails(tripPoint: selectedTripPoint, selectedTrip: selectedTrip, typeInfo: .constant(selectedTypeInfo))
, isActive: $showInfoSheet, label: {EmptyView()}).navigationTitle("").navigationBarTitle("").navigationBarHidden(true)
MapboxMap(selectedTrip: selectedTrip, speedingCount: $speedingCount, showCallouts: $showCallouts, showInfoSheet: $showInfoSheet, isFullScreen: $isFullScreen, selectedEncodedPolyline: $selectedEncodedPolyline, alreadyClean: $alreadyClean, changed: $changed, showSpecificPoint: $showSpecificPoint)
.onAppear {
showDetails = false
// The reason for adding a timer here is that since showDetails is a global variable,
// and it is not a state variable, we canot trigger its value
// We need a timer so that we can check the value of the showDetails every 0.1 second
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
showInfoSheet = showDetails
if showInfoSheet {
isFullScreen = false
findTripPoint()
timer.invalidate()
}
}
}
}
}
}
// MARK: - New Map Provided by MapBox
// MARK: - Since MapBox is designed to be implemented with the UIView, for SwiftUI, we need to transfer the UIView to View
struct MapboxMap: UIViewRepresentable {
var selectedTrip: VehicleTripData
#Binding var speedingCount: Int?
#Binding var showCallouts: Bool
#Binding var showInfoSheet: Bool
#Binding var isFullScreen: Bool
#Binding var selectedEncodedPolyline: String
#Binding var alreadyClean: Bool
#Binding var changed: Bool
#Binding var showSpecificPoint: Bool
private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
class Coordinator: NSObject, MGLMapViewDelegate {
var showCallouts: Bool
init(showcallouts: Bool) {
self.showCallouts = showcallouts
}
func mapView(_ mapView: MGLMapView, shapeAnnotationIsEnabled annotation: MGLShape) -> Bool {
return annotation.title != nil && annotation.title != "Speeding"
}
// When you tap on a point on the map, whether show the callout or not
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
if let title = annotation.title {
return (!(title == "Speeding") && showCallouts)
}
else {
return false
}
}
// MARK: - This function is used to replace the default marker icon with our icons for specific events
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
guard annotation is MGLPointAnnotation else {
return nil
}
guard annotation.title != nil else {return nil}
guard annotation.title! != nil else {return nil}
if annotation.title != ""{
let identifier = annotation.title!!
print("error \(annotation.title)")
var image = UIImage()
if identifier.hasPrefix("Speeding") {
image = UIImage(named: "Speeding Marker")!
} else if identifier.hasPrefix("Hard"){
image = UIImage(named: "Hard Brake Pin")!
} else if identifier.hasPrefix("Rapid") {
image = UIImage(named: "Rapid Accel Pin")!
} else {
image = UIImage(named: identifier)!
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MGLAnnotationView(annotation: annotation, reuseIdentifier: identifier as! String)
let imageView = UIImageView(image: image)
annotationView!.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
annotationView?.addSubview(imageView)
annotationView?.centerOffset = CGVector(dx: 0, dy: -image.size.height / 2.0)
}
return annotationView
}
return nil
}
// MARK: - This function is executed when the user taps on "View Details"
// In fact, we should set showInfoSheet to be true in order to show the EventDetailView
// However, since we are using a coordinator here, we cannnot change showInfoSheet directly
// Therefore, we change the global variable "showDetails" to be true
#objc func labelAction(_ sender: UITapGestureRecognizer) {
showDetails = true
}
// MARK: - Add the "View Details" Label in the Callout View
// MARK: - We should use UIButtion, however, if we use UIButtion, it will never be shown. Need further tests
func mapView(_ mapView: MGLMapView, leftCalloutAccessoryViewFor annotation: MGLAnnotation) -> UIView? {
guard let title = annotation.title else {return nil}
if title != nil {
if title!.hasPrefix("Speeding:") {
selectedTypeInfo = "speeding"
} else if title!.hasPrefix("Hard") {
selectedTypeInfo = "hardBrake"
} else if title!.hasPrefix("Rapid") {
selectedTypeInfo = "rapidAccel"
} else {
return nil
}
let labelTap = UITapGestureRecognizer(target: self, action: #selector(labelAction(_:)))
labelTap.numberOfTapsRequired = 1
// Callout height is fixed; width expands to fit its content.
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 60, height: 50))
label.textAlignment = .center
label.textColor = UIColor(named: "Blue")
label.text = "View Details"
label.numberOfLines = 2
label.font = UIFont(name: "NotoSans-Regular", size: 12)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(labelTap)
selectedCoor = annotation.coordinate
return label
}
return nil
}
// MARK: - This function is used to change the color of the polyline based on the event name
func mapView(_ mapView: MGLMapView, strokeColorForShapeAnnotation annotation: MGLShape) -> UIColor {
guard let title = annotation.title else {return UIColor(named: "Blue")!}
if title.hasPrefix("Speeding") {
return UIColor(named: "MapRed")!
} else {
return UIColor(named: "Blue")!
}
}
}
func makeCoordinator() -> MapboxMap.Coordinator {
return Coordinator(showcallouts: showCallouts)
}
// MARK: - Initialize the MapView
func makeUIView(context: Context) -> MGLMapView {
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(CLLocationCoordinate2D(latitude: selectedTrip.startedLatitude!, longitude: selectedTrip.startedLongitude!), animated: true)
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: Context) {
if (uiView.annotations ?? []).count == 0 {
mapViewDidFinishLoadingMap(uiView, didFinishLoading: MGLStyle())
}
}
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
mapView.isRotateEnabled = false
// MARK: - Get the result from the encoded polyline string
let result = Polyline.init(encodedPolyline: selectedEncodedPolyline, encodedLevels: .none)
// MARK: - Get the coordinates in the polyline for the trip
let coordinates = result.coordinates
guard let tripPoints = coordinates else {return}
// MARK: - Get the start and end point
let startPoint: CLLocationCoordinate2D = coordinates?.first ?? CLLocationCoordinate2D(latitude: 0, longitude: 0)
let endPoint: CLLocationCoordinate2D = coordinates?.last ?? CLLocationCoordinate2D(latitude: 0, longitude: 0)
var startTripPoint = MGLPointAnnotation()
var endTripPoint = MGLPointAnnotation()
startTripPoint.title = "Starting Point"
startTripPoint.coordinate = startPoint
endTripPoint.title = "End Point"
endTripPoint.coordinate = endPoint
mapView.addAnnotation(startTripPoint)
mapView.addAnnotation(endTripPoint)
let tryLine = MGLPolyline(coordinates: coordinates!, count: UInt(coordinates!.count))
mapView.addAnnotation(tryLine)
var speedingArray = [[TripPoint]]()
DispatchQueue.global(qos: .background).async {
// MARK: - Deal with the speeding events in the trip
// MARK: - The speeding icon will be placed at the point which has the maximum speed for each speeding polyline
var ctr = 0
while ctr < selectedTrip.speedingPoints!.count{
var speedPath = [TripPoint]()
speedPath.append(selectedTrip.speedingPoints![ctr])
while ctr + 1 < selectedTrip.speedingPoints!.count && selectedTrip.speedingPoints![ctr + 1].timeStamp!.timeIntervalSince1970 - selectedTrip.speedingPoints![ctr].timeStamp!.timeIntervalSince1970 < 10{
speedPath.append(selectedTrip.speedingPoints![ctr+1])
ctr += 1
}
speedingArray.append(speedPath)
ctr += 1
}
for speedingLine in speedingArray {
var maxDelta = 0.0
var maxDeltaPoint:TripPoint? = nil
var path: [CLLocationCoordinate2D] = []
for speedingPoint in speedingLine{
path.append(CLLocationCoordinate2D(latitude: speedingPoint.latitude!, longitude: speedingPoint.longitude!))
if speedingPoint.speed_limit_delta! > maxDelta{
maxDelta = speedingPoint.speed_limit_delta!
maxDeltaPoint = speedingPoint
}
}
if let markerPoint = maxDeltaPoint{
tripPointsArray.append(markerPoint)
var speedMarker = MGLPointAnnotation()
speedMarker.coordinate = CLLocationCoordinate2D(latitude: markerPoint.latitude!, longitude: markerPoint.longitude!)
speedMarker.title = "Speeding: \(Int(markerPoint.speed!)) " + markerPoint.speedUnit!
let speedLimitBR = Double(markerPoint.speed!) - markerPoint.speed_limit_delta! //before rounding
var speedLimit = 10 * Int((speedLimitBR / 10.0).rounded())//round to nearest 10
speedMarker.subtitle = "Speed Limit: ~ \(speedLimit) " + markerPoint.speedUnit!
DispatchQueue.main.async {
mapView.addAnnotation(speedMarker)
}
}
let speedingPolyline = MGLPolyline(coordinates: path, count: UInt(path.count))
speedingPolyline.title = "Speeding"
DispatchQueue.main.async {
mapView.addAnnotation(speedingPolyline)
}
}
speedingCount = speedingArray.count
// MARK: - Deal with hard brakes in the trip
for hardBrakePoint in selectedTrip.hardBrakePoints!{
tripPointsArray.append(hardBrakePoint)
let hardBrakeMarker = MGLPointAnnotation()
hardBrakeMarker.coordinate = CLLocationCoordinate2D(latitude: hardBrakePoint.latitude!, longitude: hardBrakePoint.longitude!)
hardBrakeMarker.title = "Hard Brake"
hardBrakeMarker.subtitle = "Acceleration: \(String(format: "%.1f", hardBrakePoint.acceleration! * 3.6)) " + "km/h/s"
DispatchQueue.main.async {
mapView.addAnnotation(hardBrakeMarker)
}
}
// MARK: - Deal with rapid accel in the trip
for rapidAccelPoint in selectedTrip.rapidAccelPoints!{
tripPointsArray.append(rapidAccelPoint)
let rapidAccelMarker = MGLPointAnnotation()
rapidAccelMarker.coordinate = CLLocationCoordinate2D(latitude: rapidAccelPoint.latitude!, longitude: rapidAccelPoint.longitude!)
rapidAccelMarker.title = "Rapid Accel"
rapidAccelMarker.subtitle = "Acceleration: \(String(format: "%.1f", rapidAccelPoint.acceleration! * 3.6)) " + "km/h/s"
DispatchQueue.main.async {
mapView.addAnnotation(rapidAccelMarker)
}
}
}
// MARK: - If we are not in EventDetailView, then the mapView.showAnnotations will help us to zoom the map to the proper level so that all the markers and annotations in the map will be shown
if !showSpecificPoint {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { timer in
guard let annotations = mapView.annotations else {return}
mapView.showAnnotations(annotations, edgePadding: .init(top: 60, left: 40, bottom: 10, right: 40), animated: true, completionHandler: nil)
}
} else {
// MARK: - If we need to zoom into a specific point, we need to find which point it is among all the trip points and zoom into that specific point
var alreadyAdded = false
var point = MGLPointAnnotation()
point.coordinate = CLLocationCoordinate2D(latitude: selectedTripPoint.latitude ?? 0, longitude: selectedTripPoint.longitude ?? 0)
for hardBrakePoint in selectedTrip.hardBrakePoints! {
if hardBrakePoint.latitude == selectedTripPoint.latitude && hardBrakePoint.longitude == selectedTripPoint.longitude {
point.title = "Hard Brake"
point.subtitle = "Acceleration: \(String(format: "%.1f", hardBrakePoint.acceleration! * 3.6)) " + "km/h/s"
mapView.addAnnotation(point)
alreadyAdded = true
break
}
}
if !alreadyAdded {
for rapidAccelPoint in selectedTrip.rapidAccelPoints!{
if rapidAccelPoint.latitude == selectedTripPoint.latitude && rapidAccelPoint.longitude == selectedTripPoint.longitude {
point.title = "Rapid Accel"
point.title! += "Acceleration: \(String(format: "%.1f", rapidAccelPoint.acceleration! * 3.6)) " + "km/h/s"
mapView.addAnnotation(point)
alreadyAdded = true
break
}
}
}
if !alreadyAdded {
var presentPolyLine: MGLPolyline = MGLPolyline()
for speedingLine in speedingArray{
var maxDelta = 0.0
var maxDeltaPoint:TripPoint? = nil
var path: [CLLocationCoordinate2D] = []
for speedingPoint in speedingLine{
path.append(CLLocationCoordinate2D(latitude: speedingPoint.latitude!, longitude: speedingPoint.longitude!))
if speedingPoint.speed_limit_delta! > maxDelta{
maxDelta = speedingPoint.speed_limit_delta!
maxDeltaPoint = speedingPoint
}
}
if let markerPoint = maxDeltaPoint{
if selectedTripPoint.latitude == markerPoint.latitude && selectedTripPoint.longitude == markerPoint.longitude {
let specificMarker = MGLPointAnnotation()
specificMarker.coordinate = CLLocationCoordinate2D(latitude: markerPoint.latitude ?? 0, longitude: markerPoint.longitude ?? 0)
var speedMarker = MGLPointAnnotation()
speedMarker.coordinate = CLLocationCoordinate2D(latitude: markerPoint.latitude!, longitude: markerPoint.longitude!)
speedMarker.title = "Speeding: \(Int(markerPoint.speed!)) " + markerPoint.speedUnit!
let speedLimitBR = Double(markerPoint.speed!) - markerPoint.speed_limit_delta! //before rounding
var speedLimit = 10 * Int((speedLimitBR / 10.0).rounded())//round to nearest 10
speedMarker.subtitle = "Speed Limit: ~ \(speedLimit) " + markerPoint.speedUnit!
point = speedMarker
DispatchQueue.main.async {
let speedingPolyline = MGLPolyline(coordinates: path, count: UInt(path.count))
speedingPolyline.title = "Speeding"
presentPolyLine = speedingPolyline
}
break
} else {
path.removeAll()
}
}
}
mapView.showAnnotations([point], edgePadding: .init(top: 60, left: 40, bottom: 10, right: 40), animated: true, completionHandler: nil)
} else {
mapView.showAnnotations([point], edgePadding: .init(top: -10, left: -10, bottom: -10, right: -10), animated: true, completionHandler: nil)
}
}
}
}
I tried updating the pods from 6.3 to 6.4.1 but it didn't work.
I know MapBox has a new framework as of v6.4.1 called v10, but I haven't installed yet cause that would change the whole code.
I am expecting to see the map as it was displaying before this new bug. eg. see screenshot

MapAnnotations Not Showing Up

I have a function that takes my coordnaties that are stored inside of my Firebase Storage, and turns them into MKPointAnnotations For Some Reason I keep on getting the error Type '()' cannot conform to 'View'
Here is my code for the function:
import SwiftUI
import MapKit
import CoreLocationUI
import Firebase
import FirebaseFirestore
struct Marker: Identifiable {
let id = UUID()
var coordinate : CLLocationCoordinate2D
}
struct MapView: View {
#StateObject private var viewModel = ContentViewModel()
//For GeoCoder
let geocoder = CLGeocoder()
#State private var result = "result of lat & long"
#State private var lat = 0.0
#State private var long = 0.0
#State private var country = "country name"
#State private var state = "state name"
#State private var zip = "zip code"
//For Map Annotations
#State var address = ""
#State var realLat = 0.00
#State var realLong = 0.00
#State var email = ""
//For TopBar
#State var goToAddress = ""
#State var filters = false
var body: some View {
let markers = [
Marker(coordinate: CLLocationCoordinate2D(latitude: realLat, longitude: realLong))
]
NavigationView {
VStack {
ZStack (alignment: .bottom) {
LocationButton(.currentLocation) {
viewModel.requestAllowOnceLocationPermission()
}
.foregroundColor(.white)
.cornerRadius(8)
.labelStyle(.iconOnly)
.symbolVariant(.fill)
.tint(.pink)
.padding(.bottom)
.padding(.trailing, 300)
getAnnotations { (annotations) in
if let annotations = annotations {
Map(coordinateRegion: $viewModel.region, showsUserLocation: true, annotationItems: MKPointAnnotation) { annotations in
MapAnnotation(coordinate: annotations.coordinate) {
Circle()
}
}
.ignoresSafeArea()
.tint(.pink)
} else {
print("There has been an error with the annotations")
}
}
}
}
}
}
func getAnnotations(completion: #escaping (_ annotations: [MKPointAnnotation]?) -> Void) {
let db = Firestore.firestore()
db.collection("annotations").addSnapshotListener { (querySnapshot, err) in
guard let snapshot = querySnapshot else {
if let err = err {
print(err)
}
completion(nil) // return nil if error
return
}
guard !snapshot.isEmpty else {
completion([]) // return empty if no documents
return
}
var annotations = [MKPointAnnotation]()
for doc in snapshot.documents {
if let lat = doc.get("lat") as? String,
let lon = doc.get("long") as? String,
let latitude = Double(lat),
let longitude = Double(lon) {
let coord = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let annotation = MKPointAnnotation()
annotation.coordinate = coord
annotations.append(annotation)
}
}
completion(annotations) // return array
}
}
func goToTypedAddress() {
geocoder.geocodeAddressString(goToAddress, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
let coordinates:CLLocationCoordinate2D = placemark.location!.coordinate
print("Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)")
//added code
result = "Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)"
lat = coordinates.latitude
long = coordinates.longitude
}
})
print("\(lat)")
print("\(long)")
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
struct Item: Identifiable {
let id = UUID()
let text: String
}
//LocationButton
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: 120), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowOnceLocationPermission() {
locationManager.requestLocation()
}
func locationManager( _ _manager:CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let latestLocation = locations.first else {
// show an error
return
}
DispatchQueue.main.async{
self.region = MKCoordinateRegion(
center: latestLocation.coordinate,
span:MKCoordinateSpan(latitudeDelta:0.05, longitudeDelta:0.05))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
The updated error is on line 50 now. Might be because of all of the new functions that I have. Also is the firebase function correct? I would like to make sure that it correct too.

Error with Geocoding - Error 1 and Error 8

I am pulling all of the users locations inside of Firebase storage, and when I try to geocode them into lat and long coordinates, I am getting the error 2022-07-26 15:40:39.676845-0400 Polygon[96779:3864577] [MKCoreLocationProvider] CLLocationManager(<CLLocationManager: 0x6000004c2c90>) for <MKCoreLocationProvider: 0x6000034f0870> did fail with error: Error Domain=kCLErrorDomain Code=1 "(null) and Error Error Domain=kCLErrorDomain Code=8 "(null)"
My code seems to be working fine, and when I take one of the addresses and geocode them in a different project, they work well. Please take a look at the code below and tell me of any errors or if I missed something?
import SwiftUI
import MapKit
import CoreLocationUI
import Firebase
struct Place: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}
struct MapView: View {
var empireStateBuilding =
Place(name: "Empire State Building", coordinate: CLLocationCoordinate2D(latitude: 40.748433, longitude: -73.985656))
#StateObject private var viewModel = ContentViewModel()
#State var address = ""
#State var addresses:[String] = []
let geocoder = CLGeocoder()
#State private var result = "result of lat & long"
#State private var lat = 0.0
#State private var long = 0.0
#State private var country = "country name"
#State private var state = "state name"
#State private var zip = "zip code"
var body: some View {
NavigationView {
ZStack (alignment: .bottom) {
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.ignoresSafeArea()
.tint(.pink)
LocationButton(.currentLocation) {
viewModel.requestAllowOnceLocationPermission()
}
.foregroundColor(.white)
.cornerRadius(8)
.labelStyle(.iconOnly)
.symbolVariant(.fill)
.tint(.pink)
.padding(.bottom)
.padding(.trailing, 300)
Button {
print(address)
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
if let coordinates:CLLocationCoordinate2D = placemark.location.coordinate {
print("Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)")
//added code
result = "Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)"
lat = coordinates.latitude
long = coordinates.longitude
}
}
})
} label: {
Image(systemName: "arrow.clockwise")
}
.padding(.leading, 300)
.padding(.bottom)
.cornerRadius(8)
Text("\(result)")
}
List(0..<addresses.count, id: \.self) {i in Text(addresses[i]) }
}
.onAppear(perform: {
downloadServerData()
})
}
func downloadServerData() {
let db = Firestore.firestore()
db.collection("UserInfo").addSnapshotListener {(snap, err) in
if err != nil{
print("There is an error")
return
}
for i in snap.documentChanges {
let documentId = i.document.documentID
if let address = i.document.get("address") as? String {
DispatchQueue.main.async {
addresses.append("\(address)")
print(address)
}
}
}
}
}
}
struct AnnotatedItem: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
//LocationButton
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: 120), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowOnceLocationPermission() {
locationManager.requestLocation()
}
func locationManager( _ _manager:CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let latestLocation = locations.first else {
// show an error
return
}
DispatchQueue.main.async{
self.region = MKCoordinateRegion(
center: latestLocation.coordinate,
span:MKCoordinateSpan(latitudeDelta:0.05, longitudeDelta:0.05))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
struct MapView1_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}

SwiftUI: Send email

In a normal UIViewController in Swift, I use this code to send a mail.
let mailComposeViewController = configuredMailComposeViewController()
mailComposeViewController.navigationItem.leftBarButtonItem?.style = .plain
mailComposeViewController.navigationItem.rightBarButtonItem?.style = .plain
mailComposeViewController.navigationBar.tintColor = UIColor.white
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: true, completion: nil)
} else {
self.showSendMailErrorAlert()
}
How can I achieve the same in SwiftUI?
Do I need to use UIViewControllerRepresentable?
#Matteo's answer is good but it needs to use the presentation environment variable. I have updated it here and it addresses all of the concerns in the comments.
import SwiftUI
import UIKit
import MessageUI
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Usage:
import SwiftUI
import MessageUI
struct ContentView: View {
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
var body: some View {
Button(action: {
self.isShowingMailView.toggle()
}) {
Text("Tap Me")
}
.disabled(!MFMailComposeViewController.canSendMail())
.sheet(isPresented: $isShowingMailView) {
MailView(result: self.$result)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you mentioned, you need to port the component to SwiftUI via UIViewControllerRepresentable.
Here's a simple implementation:
struct MailView: UIViewControllerRepresentable {
#Binding var isShowing: Bool
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var isShowing: Bool
#Binding var result: Result<MFMailComposeResult, Error>?
init(isShowing: Binding<Bool>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_isShowing = isShowing
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
isShowing = false
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShowing: $isShowing,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Usage:
struct ContentView: View {
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
var body: some View {
VStack {
if MFMailComposeViewController.canSendMail() {
Button("Show mail view") {
self.isShowingMailView.toggle()
}
} else {
Text("Can't send emails from this device")
}
if result != nil {
Text("Result: \(String(describing: result))")
.lineLimit(nil)
}
}
.sheet(isPresented: $isShowingMailView) {
MailView(isShowing: self.$isShowingMailView, result: self.$result)
}
}
}
(Tested on iPhone 7 Plus running iOS 13 - works like a charm)
Updated for Xcode 11.4
Answers are correct Hobbes the Tige & Matteo
From the comments, if you need to show an alert if no email is set up on the button or tap gesture
#State var isShowingMailView = false
#State var alertNoMail = false
#State var result: Result<MFMailComposeResult, Error>? = nil
HStack {
Image(systemName: "envelope.circle").imageScale(.large)
Text("Contact")
}.onTapGesture {
MFMailComposeViewController.canSendMail() ? self.isShowingMailView.toggle() : self.alertNoMail.toggle()
}
// .disabled(!MFMailComposeViewController.canSendMail())
.sheet(isPresented: $isShowingMailView) {
MailView(result: self.$result)
}
.alert(isPresented: self.$alertNoMail) {
Alert(title: Text("NO MAIL SETUP"))
}
To pre-populate To, Body ... also I add system sound same as Apple email sending sound
Parameters: recipients & messageBody can be injected when you init. MailView
import AVFoundation
import Foundation
import MessageUI
import SwiftUI
import UIKit
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
var recipients = [String]()
var messageBody = ""
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>)
{
_presentation = presentation
_result = result
}
func mailComposeController(_: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?)
{
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
if result == .sent {
AudioServicesPlayAlertSound(SystemSoundID(1001))
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.setToRecipients(recipients)
vc.setMessageBody(messageBody, isHTML: true)
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_: MFMailComposeViewController,
context _: UIViewControllerRepresentableContext<MailView>) {}
}
I also improved #Hobbes answer to easily configure parameters like, subject, recipients.
Checkout this gist
Even too lazy to checkout gist, then what about a SPM?
You can now easily copy paste this gift across different projects.
Usage;
import SwiftUI
import MessagesUI
// import SwiftUIEKtensions // via SPM
#State private var result: Result<MFMailComposeResult, Error>? = nil
#State private var isShowingMailView = false
var body: some View {
Form {
Button(action: {
if MFMailComposeViewController.canSendMail() {
self.isShowingMailView.toggle()
} else {
print("Can't send emails from this device")
}
if result != nil {
print("Result: \(String(describing: result))")
}
}) {
HStack {
Image(systemName: "envelope")
Text("Contact Us")
}
}
// .disabled(!MFMailComposeViewController.canSendMail())
}
.sheet(isPresented: $isShowingMailView) {
MailView(result: $result) { composer in
composer.setSubject("Secret")
composer.setToRecipients(["fancy#mail.com"])
}
}
}
Well, I have an old code that I used in SwiftUI in this way. The static function belongs to this class basically stays in my Utilities.swift file. But for demonstration purposes, I moved that in here.
Also to retain the delegate and works correctly, I have used this one as a singleton pattern.
Step 1: Create an Email Helper class
import Foundation
import MessageUI
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
public static let shared = EmailHelper()
private override init() {
//
}
func sendEmail(subject:String, body:String, to:String){
if !MFMailComposeViewController.canSendMail() {
// Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account")
return //EXIT
}
let picker = MFMailComposeViewController()
picker.setSubject(subject)
picker.setMessageBody(body, isHTML: true)
picker.setToRecipients([to])
picker.mailComposeDelegate = self
EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil)
}
static func getRootViewController() -> UIViewController? {
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController
// OR If you use SwiftUI 2.0 based WindowGroup try this one
// UIApplication.shared.windows.first?.rootViewController
}
}
Step 2: Just call this way in SwiftUI class
Button(action: {
EmailHelper.shared.sendEmail(subject: "Anything...", body: "", to: "")
}) {
Text("Send Email")
}
I am using this is in my SwiftUI based project.
Yeeee #Hobbes the Tige answer is good but...
Let's make it even better! What if user doesn't have Mail app (like I don't). You can handle it by trying out other mail apps.
if MFMailComposeViewController.canSendMail() {
self.showMailView.toggle()
} else if let emailUrl = Utils.createEmailUrl(subject: "Yo, sup?", body: "hot dog") {
UIApplication.shared.open(emailUrl)
} else {
self.alertNoMail.toggle()
}
createEmailUrl
static func createEmailUrl(subject: String, body: String) -> URL? {
let to = YOUR_EMAIL
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
let gmailUrl = URL(string: "googlegmail://co?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let outlookUrl = URL(string: "ms-outlook://compose?to=\(to)&subject=\(subjectEncoded)")
let yahooMail = URL(string: "ymail://mail/compose?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let sparkUrl = URL(string: "readdle-spark://compose?recipient=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let defaultUrl = URL(string: "mailto:\(to)?subject=\(subjectEncoded)&body=\(bodyEncoded)")
if let gmailUrl = gmailUrl, UIApplication.shared.canOpenURL(gmailUrl) {
return gmailUrl
} else if let outlookUrl = outlookUrl, UIApplication.shared.canOpenURL(outlookUrl) {
return outlookUrl
} else if let yahooMail = yahooMail, UIApplication.shared.canOpenURL(yahooMail) {
return yahooMail
} else if let sparkUrl = sparkUrl, UIApplication.shared.canOpenURL(sparkUrl) {
return sparkUrl
}
return defaultUrl
}
Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>ms-outlook</string>
<string>readdle-spark</string>
<string>ymail</string>
</array>
I upgraded and simplified #Mahmud Assan's answer for the new SwiftUI Lifecycle.
import Foundation
import MessageUI
class EmailService: NSObject, MFMailComposeViewControllerDelegate {
public static let shared = EmailService()
func sendEmail(subject:String, body:String, to:String, completion: #escaping (Bool) -> Void){
if MFMailComposeViewController.canSendMail(){
let picker = MFMailComposeViewController()
picker.setSubject(subject)
picker.setMessageBody(body, isHTML: true)
picker.setToRecipients([to])
picker.mailComposeDelegate = self
UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true, completion: nil)
}
completion(MFMailComposeViewController.canSendMail())
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
Usage:
Button(action: {
EmailService.shared.sendEmail(subject: "hello", body: "this is body", to: "asd#gmail.com") { (isWorked) in
if !isWorked{ //if mail couldn't be presented
// do action
}
}
}, label: {
Text("Send Email")
})
For anyone like me, wanting a better solution without glitching the screen of the user, i founded a very nice solution in this post from Medium.
The solution is similar to #Mahmud Assan's answer, but with more email app options and app alert with error.
I replaced some code for a method to allow the opening of more email apps, not only Mail or gmail.
First, remember to add the respective info in Info.plist, in my case:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>ms-outlook</string>
<string>readdle-spark</string>
<string>ymail</string>
</array>
After that you need to create a new swift file with the following code:
import SwiftUI
import MessageUI
class EmailHelper: NSObject {
/// singleton
static let shared = EmailHelper()
private override init() {}
}
extension EmailHelper {
func send(subject: String, body: String, to: [String]) {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
guard let viewController = windowScene?.windows.first?.rootViewController else {
return
}
if !MFMailComposeViewController.canSendMail() {
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let mails = to.joined(separator: ",")
let alert = UIAlertController(title: "Cannot open Mail!", message: "", preferredStyle: .actionSheet)
var haveExternalMailbox = false
if let url = createEmailUrl(to: mails, subject: subjectEncoded, body: bodyEncoded), UIApplication.shared.canOpenURL(url) {
haveExternalMailbox = true
alert.addAction(UIAlertAction(title: "Gmail", style: .default, handler: { (action) in
UIApplication.shared.open(url)
}))
}
if haveExternalMailbox {
alert.message = "Would you like to open an external mailbox?"
} else {
alert.message = "Please add your mail to Settings before using the mail service."
if let settingsUrl = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(settingsUrl) {
alert.addAction(UIAlertAction(title: "Open Settings App", style: .default, handler: { (action) in
UIApplication.shared.open(settingsUrl)
}))
}
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
viewController.present(alert, animated: true, completion: nil)
return
}
let mailCompose = MFMailComposeViewController()
mailCompose.setSubject(subject)
mailCompose.setMessageBody(body, isHTML: false)
mailCompose.setToRecipients(to)
mailCompose.mailComposeDelegate = self
viewController.present(mailCompose, animated: true, completion: nil)
}
private func createEmailUrl(to: String, subject: String, body: String) -> URL? {
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let gmailUrl = URL(string: "googlegmail://co?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let outlookUrl = URL(string: "ms-outlook://compose?to=\(to)&subject=\(subjectEncoded)")
let yahooMail = URL(string: "ymail://mail/compose?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let sparkUrl = URL(string: "readdle-spark://compose?recipient=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let defaultUrl = URL(string: "mailto:\(to)?subject=\(subjectEncoded)&body=\(bodyEncoded)")
if let gmailUrl = gmailUrl, UIApplication.shared.canOpenURL(gmailUrl) {
return gmailUrl
} else if let outlookUrl = outlookUrl, UIApplication.shared.canOpenURL(outlookUrl) {
return outlookUrl
} else if let yahooMail = yahooMail, UIApplication.shared.canOpenURL(yahooMail) {
return yahooMail
} else if let sparkUrl = sparkUrl, UIApplication.shared.canOpenURL(sparkUrl) {
return sparkUrl
}
return defaultUrl
}
}
// MARK: - MFMailComposeViewControllerDelegate
extension EmailHelper: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
Now, go to the view where you want to implement this:
struct OpenMailView: View {
var body: some View {
Button("Send email") {
EmailHelper.shared.send(subject: "Help", body: "", to: ["email#gmail.com"])
}
}
}
I've created a github repository for it. just add it to your project and use it like this:
struct ContentView: View {
#State var showMailSheet = false
var body: some View {
NavigationView {
Button(action: {
self.showMailSheet.toggle()
}) {
Text("compose")
}
}
.sheet(isPresented: self.$showMailSheet) {
MailView(isShowing: self.$showMailSheet,
resultHandler: {
value in
switch value {
case .success(let result):
switch result {
case .cancelled:
print("cancelled")
case .failed:
print("failed")
case .saved:
print("saved")
default:
print("sent")
}
case .failure(let error):
print("error: \(error.localizedDescription)")
}
},
subject: "test Subjet",
toRecipients: ["recipient#test.com"],
ccRecipients: ["cc#test.com"],
bccRecipients: ["bcc#test.com"],
messageBody: "works like a charm!",
isHtml: false)
.safe()
}
}
}
safe() modifier checks if MFMailComposeViewController.canSendMail() is false, it automatically dismesses the modal and tries to open a mailto link.
Before iOS 14, the default email app on iOS was Mail. Of course, you could have had other email apps installed
if MFMailComposeViewController.canSendMail() {
let mailController = MFMailComposeViewController(rootViewController: self)
mailController.setSubject("Test")
mailController.setToRecipients(["mail#test.com"])
mailController.mailComposeDelegate = self
present(mailController, animated: true, completion: nil)
}
Today
As a developer, I want to respect the user’s choice of email app, whether it’s Mail, Edison, Gmail, Outlook, or Hey. To do that, I can’t use MFMailComposeViewController. Instead, I have to add mailto to the LSApplicationQueriesSchemes key in Info.plist and then, when the user wants to send an email, use this code:
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false]) { (success) in
// Handle success/failure
}
}
Unlike MFMailComposeViewController, this approach sends the user to their choice of email app and, at the same time, closes the source app. It’s not ideal.
I don't see the need of binding the isPresented or the result so my proposed solution is to use a callback when the MFMailComposeViewControllerDelegate is called. This also makes the result not nullable.
import Foundation
import MessageUI
import SwiftUI
import UIKit
public struct MailView: UIViewControllerRepresentable {
public struct Attachment {
public let data: Data
public let mimeType: String
public let filename: String
public init(data: Data, mimeType: String, filename: String) {
self.data = data
self.mimeType = mimeType
self.filename = filename
}
}
public let onResult: ((Result<MFMailComposeResult, Error>) -> Void)
public let subject: String?
public let message: String?
public let attachment: Attachment?
public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
public var onResult: ((Result<MFMailComposeResult, Error>) -> Void)
init(onResult: #escaping ((Result<MFMailComposeResult, Error>) -> Void)) {
self.onResult = onResult
}
public func mailComposeController(
_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?
) {
if let error = error {
self.onResult(.failure(error))
} else {
self.onResult(.success(result))
}
}
}
public init(
subject: String? = nil,
message: String? = nil,
attachment: MailView.Attachment? = nil,
onResult: #escaping ((Result<MFMailComposeResult, Error>) -> Void)
) {
self.subject = subject
self.message = message
self.attachment = attachment
self.onResult = onResult
}
public func makeCoordinator() -> Coordinator {
Coordinator(onResult: onResult)
}
public func makeUIViewController(
context: UIViewControllerRepresentableContext<MailView>
) -> MFMailComposeViewController {
let controller = MFMailComposeViewController()
controller.mailComposeDelegate = context.coordinator
if let subject = subject {
controller.setSubject(subject)
}
if let message = message {
controller.setMessageBody(message, isHTML: false)
}
if let attachment = attachment {
controller.addAttachmentData(
attachment.data,
mimeType: attachment.mimeType,
fileName: attachment.filename
)
}
return controller
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>
) {
// nothing to do here
}
}
Usage
struct ContentView: View {
#State var showEmailComposer = false
var body: some View {
Button("Tap me") {
showEmailComposer = true
}
.sheet(isPresented: $showEmailComposer) {
MailView(
subject: "Email subject",
message: "Message",
attachment: nil,
onResult: { _ in
// Handle the result if needed.
self.showEmailComposer = false
}
)
}
}
}
Unfortunately, #Matteo's solution doesn't work perfectly for me. It looks buggy :(
Alternative solution
struct MailComposeSheet<T: View>: UIViewControllerRepresentable {
let view: T
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
uiViewController.rootView = view
if isPresented, uiViewController.presentedViewController == nil {
let picker = MFMailComposeViewController()
picker.mailComposeDelegate = context.coordinator
picker.presentationController?.delegate = context.coordinator
uiViewController.present(picker, animated: true)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MFMailComposeViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
var parent: MailComposeSheet
init(_ mailComposeSheet: MailComposeSheet) {
self.parent = mailComposeSheet
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) { [weak self] in
self?.parent.isPresented = false
}
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
parent.isPresented = false
}
}
}
extension View {
func mailComposeSheet(isPresented: Binding<Bool>) -> some View {
MailComposeSheet(
view: self,
isPresented: isPresented
)
}
}
Usage:
struct ContentView: View {
#State var showEmailComposer = false
var body: some View {
Button("Tap me") {
showEmailComposer = true
}
.mailComposeSheet(isPresented: $showEmailComposer)
}
}
I'm new to Swift, please tell me if I'm doing something wrong.
I went through all the answers above - continued to get AXSERVER / CPT port errors.
What worked for me
Button(action: {
let email = "mailto://"
let emailformatted = email + centreStaff.userName // from MongoDB Atlas
guard let url = URL(string: emailformatted) else { return }
UIApplication.shared.open(url)
}) {
Image (systemName: "envelope.circle.fill")
.symbolRenderingMode(.multicolor)
}
opens Outlook with name of the staff filled in...and Boom! email sent.

How to update annotations when in mapview on zoom?

I'm trying to make an app, where you get buses positions from web API. When I got the data, I put annotations on mapview for all of the coordinates i got from API. Then, when annotation is tapped on, I try to hide all the annotations which aren't on the same route as the one clicked. It works fine, but when I zoom out the annotations that weren't in the original region aren't hidden. Is there a way to refresh what is displayed when user zooms on map view?
import UIKit
import MapKit
import MBProgressHUD
import Alamofire
class MapViewController: UIViewController {
//MARK: Properties and Outlets
private var timer: NSTimer?
private lazy var dateFormatter = NSDateFormatter()
private var vehicles = [String: Vehicle]()
private var points = [CLLocationCoordinate2D]()
private let locationManager = CLLocationManager()
private var currentLocation: CLLocation?
#IBOutlet weak var mapView: MKMapView!
//MARK: Actions
#IBAction func getMyLocation(sender: UIBarButtonItem) {
getLocation()
}
//MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
dateFormatter.locale = NSLocale.currentLocale()
dateFormatter.dateFormat = "HH:mm:ss"
timer = NSTimer.scheduledTimerWithTimeInterval(30, target: self, selector: "fetchVehiclesLocations", userInfo: nil, repeats: true)
fetchVehiclesLocations()
getLocation()
}
//MARK: Get Location
private func getLocation() {
let authStatus = CLLocationManager.authorizationStatus()
if authStatus == .NotDetermined {
locationManager.requestWhenInUseAuthorization()
return
}
if authStatus == .Denied || authStatus == .Restricted {
showLocationServicesDeniedAlert()
return
}
startLocationManager()
}
//MARK: Helper methods
private func showLocationServicesDeniedAlert() {
let alert = UIAlertController(title: "Location Services Disabled", message: "Please enable location services for this app in Settings.", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alert.addAction(okAction)
presentViewController(alert, animated: true, completion: nil)
}
private func startLocationManager () {
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
showLoadingHUD()
}
}
private func stopLocationManager() {
locationManager.delegate = nil
locationManager.stopUpdatingLocation()
hideLoadingHUD()
}
private func rotateBus() {
UIView.animateWithDuration(1.0, delay: 0, options: .CurveLinear, animations: {
for (id, _) in self.vehicles {
if self.vehicles[id]!.lastPosition != nil {
let annotationView = self.mapView.viewForAnnotation(self.vehicles[id]!.annotation!)
let currentLocationPoint = self.mapView.convertCoordinate(self.vehicles[id]!.lastPosition!, toPointToView: self.mapView)
let destinationPoint = self.mapView.convertCoordinate(self.vehicles[id]!.coordinates, toPointToView: self.mapView)
let yDiff = currentLocationPoint.y - destinationPoint.y
let xDiff = destinationPoint.x - currentLocationPoint.x
let arcTan = atan(yDiff / xDiff)
var angle = CGFloat(M_PI / 2) - arcTan
if xDiff < 0 {
angle = angle + CGFloat(M_PI)
}
if angle.isNaN || angle == 0.0 {
continue
}
else {
annotationView?.transform = CGAffineTransformMakeRotation(CGFloat(angle))
}
}
}
}, completion: nil)
moveBus()
}
private func moveBus() {
UIView.animateWithDuration(28.0, delay: 0, options: .CurveLinear, animations: {
for(id, _) in self.vehicles {
if self.vehicles[id]!.lastPosition != nil {
self.vehicles[id]!.annotation?.coordinate = self.vehicles[id]!.coordinates
}
}
}, completion: nil)
}
private func createVehicles() {
print(vehicles.count)
for (id, _) in vehicles {
if vehicles[id]?.annotation == nil {
let annotation = BusAnnotation()
annotation.imageName = "bus"
annotation.coordinate = vehicles[id]!.coordinates
annotation.title = id
vehicles[id]?.annotation = annotation
mapView.addAnnotation(annotation)
}
}
print(mapView.annotations.count)
rotateBus()
}
private func drawPolyline() {
let myPolyline = MKPolyline(coordinates: &points, count: points.count)
mapView.addOverlay(myPolyline)
}
//MARK: HUD display methods
private func showLoadingHUD() {
let hud = MBProgressHUD.showHUDAddedTo(mapView, animated: true)
hud.labelText = "Getting Your Location..."
}
private func hideLoadingHUD() {
MBProgressHUD.hideAllHUDsForView(mapView, animated: true)
}
//MARK: Networking
func fetchVehiclesLocations() {
let URL = urlVehiclesLocations + apiKey
if !vehicles.isEmpty {
for (id , _) in vehicles {
vehicles[id]?.lastPosition = vehicles[id]?.coordinates
}
}
Alamofire.request(.GET, URL)
.validate()
.responseJSON {
(request, response, result) in
guard result.isSuccess else {
print("Error while fetching \(result.error!)")
return
}
let value = result.value as? [String: AnyObject]
let responseDict = value?["response"] as? [String: AnyObject]
let array = responseDict?["entity"] as? [[String: AnyObject]]
for var i = 0; i < array?.count; i++ {
let item = array?[i]["vehicle"] as? [String: AnyObject]
let position = item?["position"] as? [String: AnyObject]
let trip = item?["trip"] as? [String: AnyObject]
let vehicle = Vehicle()
vehicle.latitude = position?["latitude"] as! Double
vehicle.longitude = position?["longitude"] as! Double
vehicle.tripId = trip?["trip_id"] as! String
let startTime = trip?["start_time"] as! String
vehicle.startTime = self.dateFormatter.dateFromString(startTime)
let vehicleId = item?["vehicle"] as? [String: AnyObject]
let id = vehicleId?["id"] as! String
if self.vehicles[id] == nil {
self.vehicles[id] = vehicle
}
else {
self.vehicles[id]?.latitude = vehicle.latitude
self.vehicles[id]?.longitude = vehicle.longitude
}
}
self.createVehicles()
}
}
func fetchPointsForTripPolyline() {
let URL = urlTripPolyline + apiKey
Alamofire.request(.GET, URL)
.validate()
.responseJSON {
(request, response, result) in
guard result.isSuccess else {
print("Error while fetching \(result.error!)")
return
}
let value = result.value as! [String: AnyObject]
let responseArray = value["response"] as! [[String: AnyObject]]
for var i = 0; i < responseArray.count; i++ {
let longitude = responseArray[i]["shape_pt_lon"] as! CLLocationDegrees
let latitude = responseArray[i]["shape_pt_lat"] as! CLLocationDegrees
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
self.points.append(coordinate)
}
self.drawPolyline()
}
}
}
extension MapViewController: CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let newLocation = locations.last!
if newLocation.timestamp.timeIntervalSinceNow < -5 {
return
}
if newLocation.horizontalAccuracy < 0 {
return
}
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
stopLocationManager()
currentLocation = newLocation
let annotation = MKPointAnnotation()
annotation.coordinate = newLocation.coordinate
mapView.addAnnotation(annotation)
}
let center = CLLocationCoordinate2D(latitude: newLocation.coordinate.latitude, longitude: newLocation.coordinate.longitude)
let region = MKCoordinateRegionMakeWithDistance(center, 1000, 1000)
self.mapView.setRegion(region, animated: true)
}
}
extension MapViewController: MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if !(annotation is BusAnnotation) {
return nil
}
let reuseId = annotation.title!
var busAnnotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId!)
if busAnnotationView == nil {
busAnnotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
busAnnotationView?.canShowCallout = true
}
else {
busAnnotationView?.annotation = annotation
}
let busAnnotation = annotation as! BusAnnotation
busAnnotationView?.image = UIImage(named: busAnnotation.imageName)
return busAnnotationView
}
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolyline {
let lineView = MKPolylineRenderer(overlay: overlay)
lineView.lineWidth = 3.0
lineView.strokeColor = UIColor.greenColor()
return lineView
}
return MKOverlayRenderer()
}
func hideBuses(tripId: String) {
for (_, vehicle) in vehicles {
if vehicle.tripId != tripId {
let annotationView = mapView.viewForAnnotation(vehicle.annotation!)
annotationView?.hidden = true
}
}
}
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
let vehicleId = view.annotation?.title!
let tripId = vehicles[vehicleId!]?.tripId
hideBuses(tripId!)
}
}