Skip to content

Commit

Permalink
Merge branch 'main' into dynamic-overlays
Browse files Browse the repository at this point in the history
  • Loading branch information
pauljohanneskraft authored Jul 3, 2022
2 parents d56610a + 4f9a37b commit 0086b7b
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 64 deletions.
29 changes: 29 additions & 0 deletions Map.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Pod::Spec.new do |spec|

spec.name = 'Map'
spec.version = '0.1.0'
spec.license = { :type => 'MIT' }
spec.homepage = 'https://github.com/pauljohanneskraft/Map'
spec.authors = { 'Paul Kraft' => '[email protected]' }
spec.summary = 'More capable MKMapView wrapper for SwiftUI as drop-in to MapKit\'s SwiftUI view.'
spec.description = 'MKMapView wrapper for SwiftUI as drop-in to MapKit\'s SwiftUI view. Easily extensible annotations and overlays, iOS 13 support
and backwards compatible with MKAnnotation and MKOverlay!'
spec.source = { :git =>
'https://github.com/pauljohanneskraft/Map.git', :tag => '0.1.0' }
spec.module_name = 'Map'
spec.swift_version = '5.1'

spec.ios.deployment_target = '13.0'
spec.osx.deployment_target = '10.15'
spec.tvos.deployment_target = '13.0'
spec.watchos.deployment_target = '6.0'

spec.source_files = 'Sources/**/*.swift'

spec.framework = 'SwiftUI'
spec.ios.framework = 'UIKit'
spec.osx.framework = 'AppKit'
spec.tvos.framework = 'UIKit'
spec.watchos.framework = 'WatchKit'

end
107 changes: 58 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
# Map
![Map](https://user-images.githubusercontent.com/15239005/165400895-182eb850-f05a-4aa5-b525-866efd5628c5.png)

MapKit's SwiftUI implementation of [Map](https://developer.apple.com/documentation/mapkit/map) (UIKit: [MKMapView](https://developer.apple.com/documentation/mapkit/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](https://developer.apple.com/documentation/mapkit/map) and gives you so much more features and control:

## Features

- Annotations
- Create annotations from annotationItems as in the default MapKit SwiftUI implementation.
- Create annotations from a list of [MKAnnotation](https://developer.apple.com/documentation/mapkit/mkannotation) objects - you can even use your existing [MKAnnotationView](https://developer.apple.com/documentation/mapkit/mkannotationview) implementations!
- Overlays
- Option 1: Use a SwiftUI-style API based on `Identifiable` with overlay items and a closure to create overlays from these items
- Option 2: Use existing [MKOverlay](https://developer.apple.com/documentation/mapkit/mkoverlay) / [MKOverlayRenderer](https://developer.apple.com/documentation/mapkit/mkoverlayrenderer) objects
- Appearance / Behavior Customization
- Map type ([MKMapType](https://developer.apple.com/documentation/mapkit/mkmaptype))
- User tracking mode ([MKUserTrackingMode](https://developer.apple.com/documentation/mapkit/mkusertrackingmode))
- Interaction modes (rotation, pitch, zoom and pan)
- Point of interest filter ([MKPointOfInterestFilter](https://developer.apple.com/documentation/mapkit/mkpointofinterestfilter)).
- Adapt visibility of:
- Buildings
- Compass
- Pitch control
- Scale
- Traffic
- User heading
- User location
- Zoom controls
- Custom controls
- `MapCompass` for [MKCompassButton](https://developer.apple.com/documentation/mapkit/mkcompass)
- `MapPitchControl` for [MKPitchControl](https://developer.apple.com/documentation/mapkit/mkpitchcontrol)
- `MapScale` for [MKScaleView](https://developer.apple.com/documentation/mapkit/mkscaleview)
- `MapZoomControl` for [MKZoomControl](https://developer.apple.com/documentation/mapkit/mkzoomcontrol)

## Supported Platforms

- iOS 13+
- macOS 10.15+
- tvOS 13+
- watchOS 6+
## 🚀 Features

### 📍 Annotations
- Create annotations from annotationItems as in the default MapKit SwiftUI implementation.
- Or: Create annotations from a list of [MKAnnotation](https://developer.apple.com/documentation/mapkit/mkannotation) objects - you can even use your existing [MKAnnotationView](https://developer.apple.com/documentation/mapkit/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](https://developer.apple.com/documentation/mapkit/mkoverlay) / [MKOverlayRenderer](https://developer.apple.com/documentation/mapkit/mkoverlayrenderer) objects

### 🛠 Appearance / Behavior Customization
- Map type ([MKMapType](https://developer.apple.com/documentation/mapkit/mkmaptype))
- User tracking mode ([MKUserTrackingMode](https://developer.apple.com/documentation/mapkit/mkusertrackingmode))
- Interaction modes (rotation, pitch, zoom and pan)
- Point of interest filter ([MKPointOfInterestFilter](https://developer.apple.com/documentation/mapkit/mkpointofinterestfilter)).

### 👀 Adapt visibility of:
- Buildings
- Compass
- Pitch control
- Scale
- Traffic
- User heading
- User location
- Zoom controls

### 🪄 Custom controls
- `MapCompass` for [MKCompassButton](https://developer.apple.com/documentation/mapkit/mkcompass)
- `MapPitchControl` for [MKPitchControl](https://developer.apple.com/documentation/mapkit/mkpitchcontrol)
- `MapScale` for [MKScaleView](https://developer.apple.com/documentation/mapkit/mkscaleview)
- `MapZoomControl` for [MKZoomControl](https://developer.apple.com/documentation/mapkit/mkzoomcontrol)

## 💻 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
## 🧑🏽‍💻 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](https://developer.apple.com/documentation/mapkit/mkmaptype)), a pointOfInterestFilter ([MKPointOfInterestFilter](https://developer.apple.com/documentation/mapkit/mkpointofinterestfilter)), interactions Modes (with values: .none, .pitch, .pan, .zoon, .rotate and .all - which can be combined as you wish) and showsUserLocation.

Expand Down Expand Up @@ -95,7 +100,7 @@ struct MyMapView: View {
}
```

### Annotations: The modern approach
### 📍 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`).

Expand All @@ -119,7 +124,7 @@ Map(
)
```

### Annotations: The old-fashioned approach
### 📌 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](https://developer.apple.com/documentation/mapkit/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](https://developer.apple.com/documentation/mapkit/mkannotationview) implementations. Simply create a struct conforming to the following protocol and you are good to go.

Expand All @@ -139,7 +144,7 @@ In `registerView(on:)`, your custom annotation implementation can register a cel

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
### 🌃 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:

Expand All @@ -157,7 +162,7 @@ In your implementation, the `renderer(for:)` method creates a renderer for the o

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
### 🖼 Overlays: The old-fashioned approach

Especially when working with [MKDirections](https://developer.apple.com/documentation/mapkit/mkdirections) or when more customization to the [MKOverlayRenderer](https://developer.apple.com/documentation/mapkit/mkoverlayrenderer) is necessary, you can also provide an array of [MKOverlay](https://developer.apple.com/documentation/mapkit/mkoverlay) objects and use your own [MKOverlayRenderer](https://developer.apple.com/documentation/mapkit/mkoverlayrenderer).

Expand All @@ -182,31 +187,31 @@ Map(
)
```

### Custom Map Controls
### 🪄 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.

```swift
struct MyMapKey: Hashable {}

struct MyMapView: View {

@Binding var region: MKCoordinateRegion

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

## Usage on watchOS
## ⌚️ 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).

Expand All @@ -221,17 +226,21 @@ Map(
annotationContent: { item in
if <first condition> {
ImageAnnotation(coordinate: item.coordinate, image: UIImage(...), centerOffset: CGPoint(x: 0, y: -2)
} else if <second condition> {
} else {
MapPin(coordinate: item.coordinate, color: .red) // color can only be red, green or purple
}
}
)
```

## Author
## 🔩 Installation

Map is currently only available via Swift Package Manager. See [this tutorial by Apple](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) on how to add a package dependency to your Xcode project.

## ✍️ Author

Paul Kraft

## License
## 📄 License

Map is available under the MIT license. See the LICENSE file for more info.
61 changes: 61 additions & 0 deletions Sources/Extensions/MapKit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// MapKit.swift
// Map
//
// Created by Paul Kraft on 29.04.22.
//

import MapKit

extension CLLocationCoordinate2D {

func equals(to other: CLLocationCoordinate2D) -> Bool {
latitude == other.latitude
&& longitude == other.longitude
}

}

extension MKCoordinateRegion {

func equals(to other: MKCoordinateRegion) -> Bool {
center.equals(to: other.center)
&& span.equals(to: other.span)
}

}

extension MKCoordinateSpan {

func equals(to other: MKCoordinateSpan) -> Bool {
latitudeDelta == other.latitudeDelta
&& longitudeDelta == other.longitudeDelta
}

}

extension MKMapPoint {

func equals(to other: MKMapPoint) -> Bool {
x == other.x
&& y == other.y
}

}
extension MKMapRect {

func equals(to other: MKMapRect) -> Bool {
origin.equals(to: other.origin)
&& size.equals(to: other.size)
}

}

extension MKMapSize {

func equals(to other: MKMapSize) -> Bool {
width == other.width
&& height == other.height
}

}
26 changes: 11 additions & 15 deletions Sources/Map/Map+Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ extension Map {
defer { view = newView }
let animation = context.transaction.animation
updateAnnotations(on: mapView, from: view, to: newView)
updateInformationVisibility(on: mapView, from: view, to: newView)
updateInteractionModes(on: mapView, from: view, to: newView)
updateOverlays(on: mapView, from: view, to: newView)
updatePointOfInterestFilter(on: mapView, from: view, to: newView)
Expand Down Expand Up @@ -188,25 +189,21 @@ extension Map {
}

if newView.usesRegion {
let currentRegion = mapView.region
let newRegion = mapView.regionThatFits(newView.coordinateRegion)
if newRegion.center.latitude != currentRegion.center.latitude
|| newRegion.center.longitude != currentRegion.center.longitude
|| newRegion.span.latitudeDelta != currentRegion.span.latitudeDelta
|| newRegion.span.longitudeDelta != currentRegion.span.longitudeDelta {
let newRegion = newView.coordinateRegion
guard !mapView.region.equals(to: newRegion) else {
return
}
DispatchQueue.main.async {
mapView.setRegion(newRegion, animated: animated)
}
} else {
let visibleMapRect = mapView.visibleMapRect
let newRect = newView.mapRect
if visibleMapRect.origin.x != newRect.origin.x
|| visibleMapRect.origin.y != newRect.origin.y
|| visibleMapRect.height != newRect.height
|| visibleMapRect.width != newRect.width {
let newRectThatFits = mapView.mapRectThatFits(newRect)
mapView.setVisibleMapRect(newRectThatFits, animated: animated)
guard !mapView.visibleMapRect.equals(to: newRect) else {
return
}
DispatchQueue.main.async {
mapView.setVisibleMapRect(newRect, animated: animated)
}

}
}

Expand Down Expand Up @@ -273,7 +270,6 @@ extension Map {

public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let content = annotationContentByObject[ObjectIdentifier(annotation)] else {
assertionFailure("Somehow an unknown annotation appeared.")
return nil
}
return content.view(for: mapView)
Expand Down

0 comments on commit 0086b7b

Please sign in to comment.