MKMapView wrapper for SwiftUI as drop-in to MapKit's SwiftUI view

Overview

Map

MapKit's SwiftUI implementation of Map (UIKit: MKMapView) is very limited. This library can be used as a drop-in solution (i.e. it features a very similar, but more powerful and customizable interface) to the existing Map and gives you so much more features and control:

πŸš€ Features

πŸ“ Annotations

  • Create annotations from annotationItems as in the default MapKit SwiftUI implementation.
  • Or: Create annotations from a list of MKAnnotation objects - you can even use your existing MKAnnotationView implementations!

πŸ–Ό Overlays

  • Use a SwiftUI-style API based on Identifiable with overlay items and a closure to create overlays from these items
  • Or: Use existing MKOverlay / MKOverlayRenderer objects

πŸ›  Appearance / Behavior Customization

πŸ‘€ Adapt visibility of:

  • Buildings
  • Compass
  • Pitch control
  • Scale
  • Traffic
  • User heading
  • User location
  • Zoom controls

πŸͺ„ Custom controls

πŸ’» Supported Platforms

πŸ“± iOS 13+
πŸ–₯ macOS 10.15+
πŸ“Ί tvOS 13+
⌚️ watchOS 6+

Keep in mind that not all features are equally available on all platforms (based on what MapKit provides) and therefore might not be available here either. However, if you can use them using UIKit, there is a very high change that it is available here as well - if not: Let me/us know by creating an issue!

πŸ§‘πŸ½β€πŸ’» Usage on iOS, macOS and tvOS

Very similar to MapKit's SwiftUI wrapper, you simply create a Map view inside the body of your view. You can define a region or mapRect, the map type (MKMapType), a pointOfInterestFilter (MKPointOfInterestFilter), interactions Modes (with values: .none, .pitch, .pan, .zoon, .rotate and .all - which can be combined as you wish) and showsUserLocation.

import Map
import SwiftUI

struct MyMapView: View {

    let locations: [MyLocation]
    let directions: MKDirections.Response
    
    @State private var region = MKCoordinateRegion()
    @State private var userTrackingMode = UserTrackingMode.follow

    var body: some View {
        Map(
          coordinateRegion: $region,
          type: .satelliteFlyover,
          pointOfInterestFilter: .excludingAll,
          informationVisibility: .default.union(.userLocation),
          interactionModes: [.pan, .rotate],
          userTrackingMode: $userTrackingMode,
          annotationItems: locations,
          annotationContent: { location in
              ViewMapAnnotation(coordinate: location.coordinate) {
                  Color.red
                    .frame(width: 24, height: 24)
                    .clipShape(Circle())
              }
          },
          overlays: directions.routes.map { $0.polyline },
          overlayContent: { overlay in
              RendererMapOverlay(overlay: overlay) { _, overlay in
                  if let polyline = overlay as? MKPolyline else {
                      let isFirstRoute = overlay === directions.routes.first?.overlay
                      let renderer = MKPolylineRenderer(polyline: polyline)
                      renderer.lineWidth = 6
                      renderer.strokeColor = isFirstRoute ? .systemBlue : .systemGray
                      return renderer
                  } else {
                      assertionFailure("Unknown overlay type found.")
                      return MKOverlayRenderer(overlay: overlay)
                  }
              }
          }
        )
        .onAppear {
            region = // ...
        }
    }

}

πŸ“ Annotations: The modern approach

You can use a collection of items conforming to Identifiable and a closure that maps an item to its visual representation (available types: MapPin, MapMarker and ViewMapAnnotation for custom annotations from any SwiftUI View).

Map(
    coordinateRegion: $region,
    annotationItems: items,
    annotationContent: { item in
        if <first condition> {
            ViewMapAnnotation(coordinate: location.coordinate) {
                Color.red
                    .frame(width: 24, height: 24)
                    .clipShape(Circle())
             }
         else if <second condition> {
             MapMarker(coordinate: item.coordinate, tint: .red) // tint is `UIColor`, `NSColor` or `Color`
         } else {
             MapPin(coordinate: item.coordinate, tint: .blue) // tint is `UIColor`, `NSColor` or `Color`
         }
     }
)

πŸ“Œ Annotations: The old-fashioned approach

Moving an existing code base over to SwiftUI is hard, especially when you want to keep methods, types and properties that you have previously built. This library, therefore, allows the use of MKAnnotation instead of being forced to the new Identifiable style. In the additional closure, you can use one of the options mentioned in the modern-approach. Alternatively, we also have an option to use your own MKAnnotationView implementations. Simply create a struct conforming to the following protocol and you are good to go.

public protocol MapAnnotation {

    static func registerView(on mapView: MKMapView)
    
    var annotation: MKAnnotation { get }

    func view(for mapView: MKMapView) -> MKAnnotationView?
    
}

In registerView(on:), your custom annotation implementation can register a cell type for dequeuing using MKMapView.register(_:forAnnotationViewWithReuseIdentifier:). To dequeue the registered cell, implement the view(for:) method, similar to MKMapViewDelegate.mapView(_:viewFor:).

Note: Please make sure not to create the value of the property annotation dynamically. You can either use an existing object or create the object in your type's initializer. Simply put: Do not make annotation a computed property!

πŸŒƒ Overlays: The modern approach

Similarly to how annotations are handled, you can also use a collection of Identifiable and a closure mapping it to specific overlay types. These overlay types currently contain MapCircle, MapMultiPolygon, MapMultiPolyline, MapPolygon and MapPolyline and this list can easily be extended by creating a type conforming to the following protocol:

public protocol MapOverlay {

    var overlay: MKOverlay { get }
    
    func renderer(for mapView: MKMapView) -> MKOverlayRenderer
    
}

In your implementation, the renderer(for:) method creates a renderer for the overlay, similar to MKMapViewDelegate.mapView(_:rendererFor:).

Note: Please make sure not to create the value of the property overlay dynamically. You can either use an existing object or create the object in your type's initializer. Simply put: Do not make overlay a computed property!

πŸ–Ό Overlays: The old-fashioned approach

Especially when working with MKDirections or when more customization to the MKOverlayRenderer is necessary, you can also provide an array of MKOverlay objects and use your own MKOverlayRenderer.

For this, we provide RendererMapOverlay:

Map(
    coordinateRegion: $region,
    overlays: directions.routes.map { $0.polyline },
    overlayContent: { overlay in
        RendererMapOverlay(overlay: overlay) { mapView, overlay in
            guard let polyline = overlay as? MKPolyline else {
                assertionFailure("Unknown overlay type encountered.")
                return MKMapOverlayRenderer(overlay: overlay)
            }
            let renderer = MKPolylineRenderer(polyline: polyline)
            renderer.lineWidth = 4
            renderer.strokeColor = .red
            return renderer
        }
    }
)

πŸͺ„ Custom Map Controls

For the use of MapCompass, MapPitchControl, MapScale and MapZoomControl you will need to associate both the Map and the control with some form of a shared key. This key needs to conform to the Hashable protocol. For each key, there must only be one Map (or MKMapView respectively) in the view hierarchy at once.

Example: We want to display a scale overlay at the topLeading edge of a Map. To accomplish this, let's take a look at the following code snippet.

struct MyMapView: View {

    @Binding var region: MKCoordinateRegion
    
    var body: some View {
        Map(coordinateRegion: $region)
            .mapKey(1)
            .overlay(alignment: .topLeading) {
                MapScale(key: 1, alignment: .leading, visibility: .visible)
                    .fixedSize()
                    .padding(12)
            }
    }
}

⌚️ Usage on watchOS

Since MapKit is very limited on watchOS, there is a separate (also similary limited) wrapper in this library. If you are only targeting watchOS, it might not make sense to use this library as the underlying feature set is already very limited (e.g. no overlay support, only a few kinds of possible annotations, etc).

We do include a drop-in interface though for projects that target multiple platforms and share code extensively across these platforms.

Map(
    coordinateRegion: $region,
    informationVisibility: [.userHeading, .userLocation],
    userTrackingMode: $userTrackingMode,
    annotationItems: annotationItems,
    annotationContent: { item in
        if <first condition> {
            ImageAnnotation(coordinate: item.coordinate, image: UIImage(...), centerOffset: CGPoint(x: 0, y: -2) 
        } else {
            MapPin(coordinate: item.coordinate, color: .red) // color can only be red, green or purple
        }
    }
)

πŸ”© Installation

Map is currently only available via Swift Package Manager. See this tutorial by Apple on how to add a package dependency to your Xcode project.

✍️ Author

Paul Kraft

πŸ“„ License

Map is available under the MIT license. See the LICENSE file for more info.

Comments
  • MapUserTrackingMode.follow is Ambiguous

    MapUserTrackingMode.follow is Ambiguous

    Hi there and thanks for the good work!

    when writing the following: @State private var userTrackingMode = MapUserTrackingMode.follow

    I get the following error: Ambiguous use of 'follow'

    Hope not to bother you with typical newbie stupid stuff, but cannot find a way to go around this...

    opened by rderimay 8
  • How can we update the position of Annotations?

    How can we update the position of Annotations?

    In my app a timer updates the positions every 15 seconds, but I don't want the old Annotation to be deleted and a new one created. The goal would be to update the annotation position with animation. Is this possible?

    opened by sandorbogyo 4
  • I'm very interested in this project!

    I'm very interested in this project!

    I'm having trouble getting a demo app to compile and run. Would you be willing to create a very simple example app to get a new users for your package up and running?

    opened by MatthewMerritt 4
  • [WIP] Expose map camera and allow direct transformations of modified annotations and annotation views

    [WIP] Expose map camera and allow direct transformations of modified annotations and annotation views

    Hi! Thanks for maintaining this library. It's been a pleasure to use. I've got some annotations where I want to maintain their orientation relative to the map and move them over time, so I'm exposing the camera along with a closure that gives the user direct access to modified annotations and annotation views.

    The suggested method online for changing annotation location is to use a MKPointAnnotation rather than an MKAnnotation, and to alter the coordinate field and for changing annotation orientation is to do a CGAffineTransform on the view itself. I'm doing both, but I'm open to something else if you've got a suggestion. It does feel like this requires developers to touch UIKit more than you'd probably like.

    Here's a snippet of it in action.

    // ...
    .modifiedAnnotations { modifiedAnnotation, mkannotation, view in
      mkannotation.coordinate = modification.coordinate
      view.transform = CGAffineTransform(
        rotationAngle: modifiedAnnotation.annotationHeading.degreesToRadians
      )
    }
    .camera(viewStore.binding(get: \.camera, send: MapAction.moveCamera))
    

    Here we've got the camera bound to a field in my view's state, which is being used to calculate annotationHeading behind the scenes.

    Prior to this, if an annotation was modified it wouldn't be updated because Map.Coordinator was only checking for inserted or removed ids. Because of that, annotations are now required to be Equatable so that the coordinator can check for modified annotations in addition to inserted and removed annotations.

    If we naively remove and then insert modified annotations, then that causes the annotation to flicker.

    Breaking Changes

    • annotation items are now required to be Equatable
    opened by alex-reilly-pronto 3
  • ViewMapAnnotation does not allow text

    ViewMapAnnotation does not allow text

    I'm trying to make an annotation with text and icon (like the official POI's in apple maps) but the text is not displayed.

    This is the code I'm using, with SwiftUI maps it works but with this library it doesn't.

    Map(
      coordinateRegion: $region,
      pointOfInterestFilter: .excludingAll,
      annotationItems: POIList){ item in
        ViewMapAnnotation(coordinate: item.coordinate) {
            VStack{
                Text("Text Here")
                Image(systemName: "leaf.circle.fill")
                
            }
            //MapPOIElementView(POI: item)
        }
      }
    

    Expected result (MapKit): Screenshot 2022-07-30 at 16 47 41

    Actual result (Map): Screenshot 2022-07-30 at 16 44 05

    opened by ale-ben 2
  • Getting

    Getting "Somehow an unknown annotation appeared" when trying to ad on MKPlacemark

    I am not sure if this is related to #12 or not to be honest. It could just be me being pretty new to Map.

    When I add this: Screen 2022-06-28 aΜ€ 12 01 48

    I get the following debug assertion: Screen 2022-06-28 aΜ€ 12 01 27

    opened by rderimay 2
  • Map Annotation selection

    Map Annotation selection

    Hello @pauljohanneskraft , loving your library!

    Is there a way to support annotation selection? I already tried with navigationLinks and tapGestures with no luck

    Thanks

    opened by vx8 1
  • impossible to have my liste of annotations

    impossible to have my liste of annotations

    Hello !

    Thanks for the great work in this project ! I just Have a small issues when using map annotations.

    I've got a structured array :

    var places: [PointOfInterest] = []
    
    struct PointOfInterest: Identifiable {
    
        let id = UUID()
        let name: String
        let latitude: Double
        let longitude: Double
        
        var coordinate: CLLocationCoordinate2D {
            CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        }
    }
    

    And I use it like this in my Map :

    Map(coordinateRegion: $region, type: getTypeFromUD(), annotationItems: places, annotationContent: { item in
    
                    MapMarker(coordinate: item.coordinate, tint: UIColor(.red))   
      
    })
    

    My array receive data during the user use the app. When I go to my view where is my map I see only 1 pin which isn't at a corresponding place in my array. But can't see the other ?

    I don't know how to deal with that ?

    opened by Flyer74 0
  • Package not recognised

    Package not recognised

    Dear Paul,

    I added your package 'Map' to my project as explained by Apple. My project has a working map based on MapKit but I want to use extended facilities based on your package. It is visible as a Package dependency as well as listed under'Frameworks, Libraries and embedded Content'.

    However it seems not to be recognised by Xcode as I get 'Cannot find type 'MKCoordinateRegion' in scope and similar errors for MyLocation, MKDirections and UserTrackingMode.

    Any ideas how to resolve this. Kind regards, ahartman, belgium

    opened by ahartman 3
  • Support with documentation

    Support with documentation

    This is not an issue perse but more like a feature request, the README.md has a few lines on how to draw polylines on map elements but doesn't seem to work or is not clear, is possible to add a basic example (even here, if it works ill create a pull request) of a basic implementation.

    Thanks

    opened by ElFredo311 0
  • ViewMapAnnotation doesn't support clustering

    ViewMapAnnotation doesn't support clustering

    Screenshot 2022-11-27 at 1 26 15 PM Unfortunately custom annotation views are required for my project. However, I've noticed that clustering gets turned off if I use ViewMapAnnotation

    If I use MapMarker instead for example, it is clustered as anticipated Screenshot 2022-11-27 at 1 27 30 PM

    opened by bryan1anderson 0
  • Using current zoom factor in Annotations

    Using current zoom factor in Annotations

    I'd like to display Annotations differently depending on the current zoom factor. Is there a way to access the current zoom factor? I couldn't find it... Thanks !

    opened by rderimay 0
Owner
Paul
Paul
360° flyover on a MKMapView 🚁

FlyoverKit enables you to present stunning 360Β° flyover views on an MKMapView with zero effort while maintaining full configuration possibilities. Fly

Sven Tiigi 693 Jan 3, 2023
A Swift wrapper for forward and reverse geocoding of OpenStreetMap data

Nominatim NominatimKit is a Swift wrapper for forward and reverse geocoding of OpenStreetMap data. Why? Geocoding location data on iOS requires the us

Josef Moser 53 Feb 5, 2022
MapKit, but for SwiftUI

MapKit, but for SwiftUI

Lawrence Bensaid 2 Jul 1, 2022
Demo in SwiftUI of Apple Map, Google Map, and Mapbox map

SNMapServices SNMapServices is a serices for iOS written in SwiftUI. It provides Map capability by subclassing Mapbox, Google map and Apple map. This

Softnoesis 3 Dec 16, 2022
Use any custom view as custom callout view for MKMapView with cool animations. Use any image as annotation view.

MapViewPlus About MapViewPlus gives you the missing methods of MapKit which are: imageForAnnotation and calloutViewForAnnotationView delegate methods.

Okhan Okbay 162 Nov 16, 2022
SwiftUI wrapper for MapKit's MKMapView (UIKit).

SwiftUIMKMapView SwiftUI wrapper for MapKit's MKMapView (UIKit). ▢️ Usage Add as a dependecy to your project using Swift Package Manager. Embed map vi

Dariusz Rybicki 1 Apr 4, 2022
360° flyover on a MKMapView 🚁

FlyoverKit enables you to present stunning 360Β° flyover views on an MKMapView with zero effort while maintaining full configuration possibilities. Fly

Sven Tiigi 693 Jan 3, 2023
Request the Location Services via a 3D 360Β° flyover MKMapView πŸ—Ί

STLocationRequest STLocationRequest is a simple and elegant way to request the users location services at the very first time. The STLocationRequestCo

Sven Tiigi 640 Dec 6, 2022
Drag and drop between your apps in split view mode on iOS 9

SplitViewDragAndDrop Easily add drag and drop to pass data between your apps Setup Add pod 'SplitViewDragAndDrop' to your Podfile or copy the "SplitVi

Mario Iannotta 324 Nov 22, 2022
Drop in GIF Collection View. Uses Tenor as default GIFs provider.

Drop in GIF Collection View. Uses Tenor as default GIFs provider. This will allow you to easily integrate GIF image search into your app or you can use this as a GIF keyboard for your messaging needs.

null 5 May 7, 2022
Objective-C library for drag-n-drop of UITableViewCells in a navigation hierarchy of view controllers.

ios-dragable-table-cells Support for drag-n-drop of UITableViewCells in a navigation hierarchy of view controllers. You drag cells by tapping and hold

Anders Borum 49 Aug 23, 2022
A drop-in UITableView/UICollectionView superclass category for showing empty datasets whenever the view has no content to display

DZNEmptyDataSet Projects using this library Add your project to the list here and provide a (320px wide) render of the result. The Empty Data Set Patt

Ignacio Romero Zurbuchen 12.1k Jan 8, 2023
A drop-in mosaic collection view layout with a focus on simple customizations.

FMMosaicLayout is a mosiac collection view layout. There are a great number of media-based iOS applications that use UICollectionViewFlowLayout withou

Fluid Media Inc. 591 Dec 14, 2022
A few drop-in SwiftUI components for easily importing and thumb-nailing files

FilesUI A few drop-in SwiftUI components for easily importing and thumb-nailing files Usage 1. Import Files To import files you can use the FileImport

Brianna Zamora 3 Oct 19, 2022
A Drag-and-Drop library in pure SwiftUI.

SwiftUI Drag-and-Drop Drag-and-drop is an intuitive gesture and can improve the UX of an app. Chess Emoji Art Card Game To Do List Documentation Docum

Joel T. Huber 11 Nov 28, 2022
Drag & drop to reorder items in SwiftUI.

SwiftUIReorderableForEach Easily implement drag & drop to reorder items in SwiftUI. This package contains a generic ReoderableForEach component, which

Gordan GlavaΕ‘ 7 Dec 15, 2022
RxSwift reactive wrapper for view gestures

RxGesture Usage To run the example project, clone the repo, in the Example folder open RxGesture.xcworkspace. You might need to run pod install from t

RxSwift Community 1.3k Dec 30, 2022
MessengerKit is a drop-in UI for messenger interfaces on iOS built in Swift

About MessengerKit is a drop-in UI for messenger interfaces on iOS built in Swift. Centred around a single UIViewController, MessengerKit is themeable

Stephen Radford 1.5k Jan 2, 2023
Throttle massive number of inputs in a single drop of one line API.

Icon credits: Lorc, Delapouite & contributors Throttler Throttler is a library that throttles unnecessarily repeated and massive inputs until the last

Jang Seoksoon 73 Nov 16, 2022
Codeless drop-in universal library allows to prevent issues of keyboard sliding up and cover UITextField/UITextView. Neither need to write any code nor any setup required and much more.

IQKeyboardManager While developing iOS apps, we often run into issues where the iPhone keyboard slides up and covers the UITextField/UITextView. IQKey

Mohd Iftekhar Qurashi 15.9k Jan 8, 2023