Issue when integrating Mapbox default Search UI into SwiftUI - swift

Recently I tried to integrate Mapbox default Search UI into my project. My project basically is based on SwiftUI, and the usage example on the Mapbox website is using UIKit. I was attempted to wrapping UIKit into the SwiftUI code. Here is what I wrote on the SearchController which conformed to protocol 'UIViewControllerRepresentable':
struct Search: UIViewControllerRepresentable, LocationProvider {
let userCoordinate = CLLocationCoordinate2D(latitude: 37.785834, longitude: -122.406417)
/// `LocationProvider` protocol implementation
func currentLocation() -> CLLocationCoordinate2D? { userCoordinate }
func makeUIViewController(context: UIViewControllerRepresentableContext<Search>) -> MapboxPanelController {
let searchController = MapboxSearchController(locationProvider: self)
// default panel
let panelController = MapboxPanelController(rootViewController: searchController)
searchController.delegate = context.coordinator
return panelController
}
func updateUIViewController(_ uiViewController: MapboxPanelController, context: UIViewControllerRepresentableContext<Search>) {
}
...
}
And the parent content View I wrote:
...
var body: some View {
VStack{
ZStack {
NavigationMap()
Search(annotations: $annotations, position: $position)
}
}
}
...
The NavigationMap() is the MapView I create on another file. Basically what I tried to do here is to add a default panelController by Mapbox as the child view of the NavigationMap. But the result I ran is I am not able to interact with the NavigationMap anymore. It seems like the searchController completely covers the NavigationMap even with the panelController which allows you to drag the panel up and down. I can't figure out what I did wrong. Here is the original document provided by Mapbox:
class SimpleUISearchViewController: UIViewController, LocationProvider {
lazy var searchController = MapboxSearchController(locationProvider: self)
/// `LocationProvider` protocol implementation
func currentLocation() -> CLLocationCoordinate2D? { mapboxSFOfficeCoordinate }
lazy var panelController = MapboxPanelController(rootViewController: searchController)
let mapView = MGLMapView()
let mapboxSFOfficeCoordinate = CLLocationCoordinate2D(latitude: 37.7911551, longitude: -122.3966103)
override func viewDidLoad() {
super.viewDidLoad()
/// Add MGLMapView on the screen
mapView.frame = view.bounds
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.showsUserLocation = true
mapView.setCenter(mapboxSFOfficeCoordinate, zoomLevel: 15, animated: false)
view.addSubview(mapView)
searchController.delegate = self
addChild(panelController)
}
...
}
I am not sure why I implemented the wrong way in SwiftUI or the default document it provided cannot completely support SwiftUI. May anyone help me with that? I appreciate it.

Related

SwiftUI - STPAddCardViewController is not showing billing address

I am trying to implement Add Payment Method with Stripe in SwiftUI. The code works fine but it does not display the field for user Billing address. This is my code
struct AddCardView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
// #ObservedObject var model: PaymentMethodViewModel
func makeUIViewController(context: Context) -> UINavigationController {
let controller = STPAddCardViewController()
controller.delegate = context.coordinator
controller.edgesForExtendedLayout = .all
let navigationController = UINavigationController(rootViewController: controller)
navigationController.navigationBar.prefersLargeTitles = true
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, STPAddCardViewControllerDelegate, UINavigationControllerDelegate {
var parent: AddCardView
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
parent.presentation.wrappedValue.dismiss()
}
func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreatePaymentMethod paymentMethod: STPPaymentMethod, completion: #escaping STPErrorBlock) {
print(paymentMethod)
// parent.model.isCards = true
parent.presentation.wrappedValue.dismiss()
}
init(_ parent: AddCardView) {
self.parent = parent
}
}
}
the code above produces this
I thought the Billing information text field will be included in the STPAddCardViewController method like this image
How can fix the code above to include text field for users to enter the card billing address?
you could try using "STPPaymentConfiguration" parameters, and call
let controller = STPAddCardViewController(configuration: ... , theme: ...)
Provide a configuration and a theme in your initialize method.
Here is sample in Objective-C.
STPPaymentConfiguration *configuration = [[STPPaymentConfiguration alloc] init];
configuration.requiredBillingAddressFields = STPBillingAddressFieldsFull;
STPAddCardViewController *addCardVC = [[STPAddCardViewController alloc] initWithConfiguration:configuration theme:[STPTheme defaultTheme]];
addCardVC.alwaysEnableDoneButton = YES;
addCardVC.delegate = self;
vc = addCardVC;
https://github.com/stripe/stripe-ios/blob/591f54d11f6f60be609e294f7dd7570c0ad32b49/LocalizationTester/ViewController.m

Button in ContentView that zooms to current location on MapBox map?

So....in my coordinator--which conforms to the mapbox delegate protocol, I can just do:
mapView.setCenter(mapView.userLocation!.coordinate, zoomLevel: 13, animated: true)
and this function works fine in the coordinator or when it is called in the mapView class. The only problem is I don't know how to pass this mapView instance around (specifically back into ContentView where I want to have a button that does the same thing). I also have a LocationManager struct but I don't know how much use that would be here. Is the passing of the MapView instance the simplest way to accomplish what I'd like to do?
Thank you in advance!
Here is a demo of solution (based on MapKit, but it not important for idea). Tested with Xcode 12.
struct DemoActionToMapView: View {
#State private var centerToUser: () -> () = {}
var body: some View {
VStack {
Button("Center", action: centerToUser)
MapView { map in
self.centerToUser = {
map.setCenter(map.userLocation.coordinate, animated: true)
}
}
}
}
}
struct MapView: UIViewRepresentable {
var configure: (MKMapView) -> () = { _ in }
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
DispatchQueue.main.async {
configure(map)
}
return map
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}

How to add polylines and annotations from user taps in SwiftUI

I am trying to drop annotations/pins wherever a user touches a certain location on the map. Whenever a user drops more 2 or more pins, it creates a polyline that connects the points. I got it to work in regular Swift 5, but I am trying to make it work in SwiftUI. I am using Mapbox. I am having trouble figuring out how to make it work with the coordinator. Can anyone help me figure this out? Thanks!
import SwiftUI
import Mapbox
// Creates an annotation using title & coordinate
extension MGLPointAnnotation {
convenience init(title: String, coordinate: CLLocationCoordinate2D) {
self.init()
self.title = title
self.coordinate = coordinate
}
}
// Represents & Displays an MGLMapView in SwiftUI
struct MapView: UIViewRepresentable {
// Property binding to add annotations
#Binding var annotations: [MGLPointAnnotation]
//var mapView: MGLMapView!
//var coordinates = [CLLocationCoordinate2D]()
// Creates a mapView with MGLMapView type
private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
// Needed function for UIViewRepresentable
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
// Add a single tap gesture recognizer. This gesture requires the built-in MGLMapView tap gestures (such as those for zoom and annotation selection) to fail.
let singleTap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleMapTap(sender:)))
for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
singleTap.require(toFail: recognizer)
}
mapView.addGestureRecognizer(singleTap)
// Convert `mapView.centerCoordinate` (CLLocationCoordinate2D) to screen location (CGPoint).
let centerScreenPoint: CGPoint = mapView.convert(mapView.centerCoordinate, toPointTo: nil)
print("Screen center: \(centerScreenPoint) = \(mapView.center)")
return mapView
}
// Needed function for UIViewRepresentable
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
updateAnnotations()
uiView.addAnnotations(annotations)
}
// Styles the map with a MapBox Studio URL
func styleURL(_ styleURL: URL) -> MapView {
mapView.styleURL = styleURL
return self
}
// Specifies where the map is centered
func centerCoordinate(_ centerCoordinate: CLLocationCoordinate2D) -> MapView {
mapView.centerCoordinate = centerCoordinate
return self
}
// Specifies the zoom level of the initial view
func zoomLevel(_ zoomLevel: Double) -> MapView {
mapView.zoomLevel = zoomLevel
return self
}
// Updates the annotations in the view
private func updateAnnotations() {
if let currentAnnotations = mapView.annotations {
mapView.removeAnnotations(currentAnnotations)
}
mapView.addAnnotations(annotations)
}
// Makes the coordinater (coordinator class below)
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self, mapView)
}
// A coordinator used with a delegate to add the annotation view to the map
// A coordinator class is declared to implement and view MGLMapViewDelegate in SwiftUI
final class Coordinator: NSObject, MGLMapViewDelegate {
var control: MapView
var mapView: MGLMapView!
var coordinates = [CLLocationCoordinate2D]()
var pointAnnotations = [MGLPointAnnotation]()
init(_ control: MapView, _ mapView: MGLMapView) {
self.control = control
self.mapView = mapView
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
return nil
}
#objc func handleMapTap(sender: UITapGestureRecognizer) {
// Convert tap location (CGPoint) to geographic coordinate (CLLocationCoordinate2D).
let tapPoint: CGPoint = sender.location(in: mapView)
let tapCoordinate: CLLocationCoordinate2D = mapView.convert(tapPoint, toCoordinateFrom: mapView)
print("You tapped at: \(tapCoordinate.latitude), \(tapCoordinate.longitude)")
// Create an array of coordinates for our polyline, starting at the center of the map and ending at the tap coordinate.
//var coordinates: [CLLocationCoordinate2D] = [mapView.centerCoordinate]
coordinates.append(tapCoordinate)
print("Coordinates list: \(coordinates)")
//var pointAnnotations = [MGLPointAnnotation]()
for coordinate in coordinates {
let point = MGLPointAnnotation()
point.coordinate = coordinate
point.title = "\(coordinate.latitude), \(coordinate.longitude)"
pointAnnotations.append(point)
print("Annotations: \(pointAnnotations)")
let polyline = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
mapView.addAnnotation(polyline)
}
mapView.addAnnotations(pointAnnotations)
// Remove any existing polyline(s) from the map.
//if mapView.annotations?.count != nil, let existingAnnotations = mapView.annotations {
//mapView.removeAnnotations(existingAnnotations)
//}
// Add a polyline with the new coordinates.
//let polyline = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
//mapView.addAnnotation(polyline)
}
}
}
First of all, the single tap cannot work if making it 'to fail' for other recognisers, (I don't know why that happened in SwiftUI). Removing the for loop makes it work, but also disables other single tap features on the map, for example, annotations will not be clickable.
Therefore, I suggest using some other gesture instead, the following example is based on long press.
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
let longTap = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleMapTap(sender:)))
mapView.addGestureRecognizer(longTap)
return mapView
}
Second, the 'mapView' in the handler function should be 'control.mapView' according to the first line of your 'Coordinator' class.
#objc func handleMapTap(sender: UILongPressGestureRecognizer) {
// Run the code unless user end the long press (Long press handler keep calling during press)
//guard sender.state == .ended else { return }
// Convert tap location (CGPoint) to geographic coordinate (CLLocationCoordinate2D).
let locationInMap = sender.location(in: control.mapView)
let coordinateSet = control.mapView.convert(locationInMap, toCoordinateFrom: nil)
print("You tapped at: \(coordinateSet.latitude)")
if sender.state == UIGestureRecognizer.State.ended {
print("You final tapped at: \(coordinateSet.latitude)")
}
}
I faced with the same issue with you.
I updated my library to the newest version and every thing worked!

class extension not being called - unable to configure annotations on map

I'm on Swift 5 and Xcode 11, currently trying to create a map with predefined annotations using custom images instead of the default markers. I've been playing around with Apple's sample code for AnnotatingMapWithCustomData and could tailor it to what I needed. But once I tried to copy the code into my own project, the annotations showed up as default markers instead of the custom annotationViews I configured.
import UIKit
import MapKit
class MapViewController: UIViewController {
#IBOutlet private weak var mapView: MKMapView!
private var allAnnotations: [MKAnnotation]?
private var displayedAnnotations: [MKAnnotation]? {
willSet {
if let currentAnnotations = displayedAnnotations {
mapView.removeAnnotations(currentAnnotations)
}
}
didSet {
if let newAnnotations = displayedAnnotations {
mapView.addAnnotations(newAnnotations)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
registerMapAnnotationViews()
// Create the array of annotations and the specific annotations for the points of interest.
allAnnotations = [ShrekAnnotation(), CoffeeAnnotation()]
// Dispaly all annotations on the map.
displayedAnnotations = allAnnotations
centerMapOnLondon()
}
/// - Tag: RegisterAnnotationViews
private func registerMapAnnotationViews() {
mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(ShrekAnnotation.self))
mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(CoffeeAnnotation.self))
}
private func centerMapOnLondon() {
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let center = CLLocationCoordinate2D(latitude: 51.507911, longitude: -0.132222)
mapView.setRegion(MKCoordinateRegion(center: center, span: span), animated: true)
}
}
extension MapViewController: MKMapViewDelegate {
/// - Tag: CreateAnnotationViews
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !annotation.isKind(of: MKUserLocation.self) else {
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
return nil
}
var annotationView: MKAnnotationView?
if let annotation = annotation as? ShrekAnnotation {
annotationView = setupShrekAnnotationView(for: annotation, on: mapView)
} else if let annotation = annotation as? CoffeeAnnotation {
annotationView = setupCoffeeAnnotationView(for: annotation, on: mapView)
}
return annotationView
}
/// - Tag: ConfigureAnnotationViews
private func setupShrekAnnotationView(for annotation: ShrekAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(ShrekAnnotation.self)
let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
view.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "map-shrek1")
view.image = image
return view
}
/// - Tag: ConfigureAnnotationViews
private func setupCoffeeAnnotationView(for annotation: CoffeeAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(CoffeeAnnotation.self)
let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
view.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "map-cof1")
view.image = image
return view
}
}
Apparently the class extension is not being called but I don't know how to fix this. The code that worked before I tried implementing it in my own project is identical. Could I have missed something I should've copied too or can you find the reason for the bug in the code?
Thanks for any help.
I think you are not setting the mapView delegate hence viewFor annotation is not being triggered and your custom annotation view is not appearing on map. Set
mapView.delegate = self
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
registerMapAnnotationViews()
// Create the array of annotations and the specific annotations for the points of interest.
allAnnotations = [ShrekAnnotation(), CoffeeAnnotation()]
// Dispaly all annotations on the map.
displayedAnnotations = allAnnotations
centerMapOnLondon()
}
Hope this helps

How to integrate Mapbox SDK with SwiftUI

I installed the Mapbox SDK into my project, but I don't understand how to integrate this code snippet with SwiftUI.
I created a SwiftUI View named MapView, where I import the Mapbox Framework.
I try to use the UIViewRepresentable protocol, as in Apple's tutorial, but without success.
import Mapbox
class MapView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let styleURL = URL(string: "mapbox://styles/mapbox/outdoors-v9")
let mapView = MGLMapView(frame: view.bounds,
styleURL: styleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(CLLocationCoordinate2D(latitude: 45.52954,
longitude: -122.72317),
zoomLevel: 14,
animated: false)
view.addSubview(mapView)
}
}
I am new to iOS development, so any help would be much appreciated.
This is a working sample on how you can integrate the MGLMapView with SwiftUI.
When using UIViewRepresentable you have to implement two delegates: makeUIView that will return your view (in this case MGLMapView) and updateUIView which will receive the same view type as the one returned in makeUIView (again MGLMapView).
You can use this to experiment.
Also I recommend you get familiar with the React architecture to understand better the SwiftUI approach.
You can improve this sample by making ContentView receive a styleURL, and trying different styles in the preview.
import Mapbox
import SwiftUI
struct ContentView : View {
var body: some View {
MapboxViewRepresentation(MGLStyle.streetsStyleURL)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
struct MapboxViewRepresentation : UIViewRepresentable {
let styleURL: URL
init(_ styleURL: URL) {
self.styleURL = styleURL
}
func makeUIView(context: UIViewRepresentableContext<MapboxViewRepresentation>) -> MGLMapView {
let mapView = MGLMapView(frame: .zero, styleURL: styleURL)
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapboxViewRepresentation>) {
}
}
UPDATE: Here is the official guide on how to integrate Mapbox with SwiftUI https://docs.mapbox.com/help/tutorials/ios-swiftui/