Repository: sgade/swiftui-mapview Branch: main Commit: b29e45510019 Files: 21 Total size: 38.7 KB Directory structure: gitextract_pa7cjpmw/ ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MapViewExample/ │ ├── MapViewExample/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── CLLocationCoordinate2D+Examples.swift │ │ ├── ContentView.swift │ │ ├── ExampleAnnotation.swift │ │ └── MapViewExampleApp.swift │ └── MapViewExample.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ └── contents.xcworkspacedata ├── Package.swift ├── README.md └── Sources/ └── SwiftUIMapView/ ├── AnnotationViews/ │ ├── MapAnnotationClusterView.swift │ └── MapAnnotationView.swift ├── Annotations/ │ ├── MKMapView+MapViewAnnotation.swift │ └── MapViewAnnotation.swift ├── CoreLocation+Equatable.swift ├── MapView+Xcode.swift └── MapView.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store /.build /Packages xcuserdata/ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to SwiftUI MapView You're welcome to contribute improvements and fixes to the project. Thank you for your time investment! Please read the following guide to ensure consistency and a shared level of quality for your contribution. The goal is to have everything ready so that the process is as straight forward as possible for everyone. If there are instructions missing, feel free to contribute changes to this guide as well. **Before you start contributing** please make sure of the following: 1. Ensure that you have a clear goal in mind. Proposing or implementing changes should not divert from the project's goal. 2. When unsure of whether your changes meet the project's goal open an issue to discuss your intentions and its consequences. **When opening issues or pull requests** please make sure of the following: 1. Describe the specific goal of the issue or pull request. Focus on one topic so that it can be worked on in isolation. 2. Add your thoughts and questions to the description so that issues can be addressed quickly. 3. Highlight important sections in code that need attention and are critical to understanding the changes. 4. Avoid mixing multiple changes or ideas in one contribution. Each one should be one logical unit (i.e. do not mix new features with fixes to existing ones). Once you feel comfortable with these guidelines feel free to open [issues](https://github.com/sgade/swiftui-mapview/issues) or [pull requests](https://github.com/sgade/swiftui-mapview/pulls) on GitHub. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Sören Gade Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MapViewExample/MapViewExample/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MapViewExample/MapViewExample/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MapViewExample/MapViewExample/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MapViewExample/MapViewExample/CLLocationCoordinate2D+Examples.swift ================================================ // // CLLocationCoordinate2D+Examples.swift // MapViewExample // // Created by Sören Gade on 21.02.20. // import CoreLocation extension CLLocationCoordinate2D { static let inifiniteLoop = CLLocationCoordinate2D( latitude: 37.331836, longitude: -122.029604 ) static let applePark = CLLocationCoordinate2D( latitude: 37.334780, longitude: -122.009073 ) } ================================================ FILE: MapViewExample/MapViewExample/ContentView.swift ================================================ // // ContentView.swift // MapViewExample // // Created by Sören Gade on 21.02.20. // import SwiftUI import SwiftUIMapView import CoreLocation import MapKit struct ContentView: View { @State private var region: MKCoordinateRegion? = MKCoordinateRegion( center: .applePark, span: MKCoordinateSpan( latitudeDelta: 0.05, longitudeDelta: 0.05 ) ) @State private var selectedAnnotations: [MapViewAnnotation] = [] private let locationManager = CLLocationManager() var body: some View { VStack { MapView( mapType: .standard, region: $region, showsUserLocation: true, userTrackingMode: .none, annotations: [ExampleAnnotation].examples, selectedAnnotations: $selectedAnnotations ) .edgesIgnoringSafeArea(.all) ForEach(selectedAnnotations.compactMap { $0 as? ExampleAnnotation }) { annotation in Text("\( annotation.title ?? "" )") } if let region { Text("\(region.center.latitude), \(region.center.longitude)") } } .onAppear { // this is required to display the user's current location locationManager.requestWhenInUseAuthorization() } } } // MARK: - Previews #Preview { ContentView() } ================================================ FILE: MapViewExample/MapViewExample/ExampleAnnotation.swift ================================================ // // ExampleAnnotation.swift // MapViewExample // // Created by Sören Gade on 21.02.20. // import SwiftUIMapView import MapKit class ExampleAnnotation: NSObject, MapViewAnnotation, Identifiable { let coordinate: CLLocationCoordinate2D let title: String? let id = UUID() let clusteringIdentifier: String? = "exampleCluster" let glyphImage: UIImage? = UIImage(systemName: "e.circle.fill") let tintColor: UIColor? = .green init(title: String, coordinate: CLLocationCoordinate2D) { self.title = title self.coordinate = coordinate } } // MARK: - Array+ExampleAnnotation extension Array where Element == ExampleAnnotation { static let examples: [ExampleAnnotation] = [ ExampleAnnotation(title: "Apple Park", coordinate: .applePark), ExampleAnnotation(title: "Infinite Loop", coordinate: .inifiniteLoop), ] } ================================================ FILE: MapViewExample/MapViewExample/MapViewExampleApp.swift ================================================ // // MapViewExampleApp.swift // MapViewExample // // Created by Sören Gade on 16.12.25. // import SwiftUI @main struct MapViewExampleApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: MapViewExample/MapViewExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 45DDF4992EF1CFE5001077C5 /* SwiftUIMapView in Frameworks */ = {isa = PBXBuildFile; productRef = 45DDF4982EF1CFE5001077C5 /* SwiftUIMapView */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 45DDF4892EF1CF73001077C5 /* MapViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MapViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 45DDF48B2EF1CF73001077C5 /* MapViewExample */ = { isa = PBXFileSystemSynchronizedRootGroup; path = MapViewExample; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ 45DDF4862EF1CF73001077C5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 45DDF4992EF1CFE5001077C5 /* SwiftUIMapView in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 45DDF4802EF1CF73001077C5 = { isa = PBXGroup; children = ( 45DDF48B2EF1CF73001077C5 /* MapViewExample */, 45DDF48A2EF1CF73001077C5 /* Products */, ); sourceTree = ""; }; 45DDF48A2EF1CF73001077C5 /* Products */ = { isa = PBXGroup; children = ( 45DDF4892EF1CF73001077C5 /* MapViewExample.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 45DDF4882EF1CF73001077C5 /* MapViewExample */ = { isa = PBXNativeTarget; buildConfigurationList = 45DDF4942EF1CF74001077C5 /* Build configuration list for PBXNativeTarget "MapViewExample" */; buildPhases = ( 45DDF4852EF1CF73001077C5 /* Sources */, 45DDF4862EF1CF73001077C5 /* Frameworks */, 45DDF4872EF1CF73001077C5 /* Resources */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( 45DDF48B2EF1CF73001077C5 /* MapViewExample */, ); name = MapViewExample; packageProductDependencies = ( 45DDF4982EF1CFE5001077C5 /* SwiftUIMapView */, ); productName = MapViewExample; productReference = 45DDF4892EF1CF73001077C5 /* MapViewExample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 45DDF4812EF1CF73001077C5 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2620; LastUpgradeCheck = 2620; TargetAttributes = { 45DDF4882EF1CF73001077C5 = { CreatedOnToolsVersion = 26.2; }; }; }; buildConfigurationList = 45DDF4842EF1CF73001077C5 /* Build configuration list for PBXProject "MapViewExample" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 45DDF4802EF1CF73001077C5; minimizedProjectReferenceProxies = 1; packageReferences = ( 45DDF4972EF1CFE5001077C5 /* XCLocalSwiftPackageReference "../../swiftui-mapview" */, ); preferredProjectObjectVersion = 77; productRefGroup = 45DDF48A2EF1CF73001077C5 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 45DDF4882EF1CF73001077C5 /* MapViewExample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 45DDF4872EF1CF73001077C5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 45DDF4852EF1CF73001077C5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 45DDF4922EF1CF74001077C5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 45DDF4932EF1CF74001077C5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; name = Release; }; 45DDF4952EF1CF74001077C5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "The app shows the user's location."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.sgade.MapViewExample; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 45DDF4962EF1CF74001077C5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "The app shows the user's location."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.sgade.MapViewExample; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 45DDF4842EF1CF73001077C5 /* Build configuration list for PBXProject "MapViewExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 45DDF4922EF1CF74001077C5 /* Debug */, 45DDF4932EF1CF74001077C5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 45DDF4942EF1CF74001077C5 /* Build configuration list for PBXNativeTarget "MapViewExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 45DDF4952EF1CF74001077C5 /* Debug */, 45DDF4962EF1CF74001077C5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ 45DDF4972EF1CFE5001077C5 /* XCLocalSwiftPackageReference "../../swiftui-mapview" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../swiftui-mapview"; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 45DDF4982EF1CFE5001077C5 /* SwiftUIMapView */ = { isa = XCSwiftPackageProductDependency; productName = SwiftUIMapView; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 45DDF4812EF1CF73001077C5 /* Project object */; } ================================================ FILE: MapViewExample/MapViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Package.swift ================================================ // swift-tools-version: 6.2 import PackageDescription let package = Package( name: "swiftui-mapview", platforms: [ .iOS(.v13) ], products: [ .library( name: "SwiftUIMapView", targets: ["SwiftUIMapView"] ), ], dependencies: [], targets: [ .target( name: "SwiftUIMapView", dependencies: [], swiftSettings: [ .defaultIsolation(MainActor.self) ] ) ] ) ================================================ FILE: README.md ================================================ # swiftui-mapview ![Swift Version](https://img.shields.io/badge/Swift-6.2-orange.svg?logo=swift) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsgade%2Fswiftui-mapview%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/sgade/swiftui-mapview) ![Platforms](https://img.shields.io/badge/Platforms-iOS-yellow.svg?logo=apple) [MKMapView](https://developer.apple.com/documentation/mapkit/mkmapview) in SwiftUI. **For iOS 17 and later, I recommend you use Apple's official [Map](https://developer.apple.com/documentation/mapkit/map) view.** [![Social Preview](assets/social-preview.png)](assets/screen.png) (Click the image to see the full screenshot) ## Install Using Xcode, select `File` -> `Swift Packages` -> `Add Package Dependency` and enter `https://github.com/sgade/swiftui-mapview`. ## Usage In your view, add the map. See the [example project](MapViewExample) for how to integrate the map view. ```swift import SwiftUIMapView struct ContentView: View { var body: some View { MapView() } } ``` ## Configuration ### Map type ```swift MapView(mapType: .standard) ``` ### User location ### Showing the current user location The current location can be shown on the map. Note that the application requires permission to access the current user location. See the documentation on `MapView.showsUserLocation` for more information. ```swift MapView(showsUserLocation: true) ``` ### Tracking the user's location ```swift MapView(userTrackingMode: .follow) ``` ### Setting the visible region The binding passed in for `region` defines the visible map region. Use it to define the visible center and zoom. Setting it to `nil` will use the map's default region when loaded. It is also updated when the visible region changes. ```swift @State var region: MKCoordinateRegion? MapView(region: $region) ``` ### Annotations #### Adding annotations Annotations are represented as objects of a custom class that implements the `MapViewAnnotation` protocol. It might be helpful to subclass from existing classes like `MKPlacemark`. ```swift let annotations: [MapViewAnnotation] = ... MapView(annotations: annotations) ``` #### Selecting annotations A list of selected annotations can be passed in via a binding. ```swift @State var selectedAnnotations: [MapViewAnnotation] = [] MapView(selectedAnnotations: $selectedAnnotations) ``` ## Contributing See the [contributing guide](CONTRIBUTING.md). ## License This project is licensed unter the terms of the MIT license. See [LICENSE](./LICENSE) for more information. ================================================ FILE: Sources/SwiftUIMapView/AnnotationViews/MapAnnotationClusterView.swift ================================================ // // MapAnnotationClusterView.swift // SwiftUIMapView // // Created by Sören Gade on 19.02.20. // Copyright © 2020 Sören Gade. All rights reserved. // import Foundation import MapKit /// Custom annotation view for ``MapAnnotation`` view clusters. /// /// This view draws its content itself and therefore has a pretty custom style. /// /// - Seealso: ``MapAnnotationView`` class MapAnnotationClusterView: MKAnnotationView { private static let annotationSize: CGFloat = 32 override var annotation: MKAnnotation? { didSet { guard let clusterAnnotation = annotation as? MKClusterAnnotation else { return } let mapAnnotations = clusterAnnotation.memberAnnotations.compactMap { $0 as? MapViewAnnotation } guard let mapAnnotation = mapAnnotations.first else { return } collisionMode = .circle image = drawGlyph( sized: CGSize( width: Self.annotationSize, height: Self.annotationSize ), colored: mapAnnotation.tintColor, withCount: mapAnnotations.count ) } } } // MARK: Drawing glyphs private extension MapAnnotationClusterView { /// Draws the annotation's glyph. func drawGlyph( sized size: CGSize, colored tintColor: UIColor?, withCount count: Int ) -> UIImage { let renderer = UIGraphicsImageRenderer(size: size) return renderer.image { _ in // draw background tintColor?.setFill() UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: size.width, height: size.height)).fill() if let image = UIImage(systemName: "\(count).circle")?.withTintColor(.white) { // draw glyph let imageRect = CGRect(x: size.width / 2 - image.size.width / 2, y: size.height / 2 - image.size.height / 2, width: image.size.width, height: image.size.height) image.draw(in: imageRect) } else { // draw text let fontSize: CGFloat = 24 let textAttributes: [NSAttributedString.Key: NSObject] = [ NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize) ] let text = "\(count)" as NSString let textSize = text.size(withAttributes: textAttributes) let textRect = CGRect(x: size.width / 2 - textSize.width / 2, y: size.height / 2 - textSize.height / 2, width: textSize.width, height: textSize.height) text.draw(in: textRect, withAttributes: textAttributes) } } } } ================================================ FILE: Sources/SwiftUIMapView/AnnotationViews/MapAnnotationView.swift ================================================ // // MapAnnotationView.swift // SwiftUIMapView // // Created by Sören Gade on 19.02.20. // Copyright © 2020 Sören Gade. All rights reserved. // import Foundation import MapKit /// Custom annotation view for ``MapAnnotation`` objects. /// /// Sets the view's `glyphImage` and `markerTintColor` according to values of the annotation. /// Automatically takes advantage of clustering via an optionally set `clusteringIdentifier`. class MapAnnotationView: MKMarkerAnnotationView { override var annotation: MKAnnotation? { didSet { guard let mapAnnotation = annotation as? MapViewAnnotation else { return } clusteringIdentifier = mapAnnotation.clusteringIdentifier markerTintColor = mapAnnotation.tintColor glyphImage = mapAnnotation.glyphImage } } } ================================================ FILE: Sources/SwiftUIMapView/Annotations/MKMapView+MapViewAnnotation.swift ================================================ // // MKMapView+MapViewAnnotation.swift // SwiftUIMapView // // Created by Sören Gade on 21.02.20. // import MapKit extension MKMapView { /// All ``MapViewAnnotation``s set on the map view. var mapViewAnnotations: [MapViewAnnotation] { annotations.compactMap { $0 as? MapViewAnnotation } } /// All ``MapViewAnnotation``s selected on the map view. var selectedMapViewAnnotations: [MapViewAnnotation] { selectedAnnotations.compactMap { $0 as? MapViewAnnotation } } } ================================================ FILE: Sources/SwiftUIMapView/Annotations/MapViewAnnotation.swift ================================================ // // MapViewAnnotation.swift // SwiftUIMapView // // Created by Sören Gade on 19.02.20. // Copyright © 2020 Sören Gade. All rights reserved. // import Foundation import MapKit /// An annotation on a ``MapView``. /// /// Annotations can be visualized using their `title`, `subtitle`, `glyphImage` and `tintColor`. /// /// To support automatic clustering of annotations, specify a `clusterIdentifier`. /// /// Note: `MapAnnotation` provides a custom view class implementation for displaying the annotation data on the map. /// Custom views are currently not supported. public protocol MapViewAnnotation: MKAnnotation { /// Identifier for clustering annotations. /// Setting to a non-`nil` value marks the annotation as participant in clustering. /// /// - Seealso: MKAnnotationView.clusteringIdentifier var clusteringIdentifier: String? { get } /// The image to display as a glyph in the annotation's view. var glyphImage: UIImage? { get } /// The tint color of the annotations's view. var tintColor: UIColor? { get } } ================================================ FILE: Sources/SwiftUIMapView/CoreLocation+Equatable.swift ================================================ // // CoreLocation+Equatable.swift // SwiftUIMapView // // Created by Sören Gade on 24.02.20. // import Foundation import CoreLocation import MapKit // MARK: - CLLocationCoordinate2D extension CLLocationCoordinate2D: @retroactive Equatable { public static func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude } } // MARK: - MKCoordinateSpan extension MKCoordinateSpan: @retroactive Equatable { public static func ==(lhs: MKCoordinateSpan, rhs: MKCoordinateSpan) -> Bool { lhs.latitudeDelta == rhs.latitudeDelta && lhs.longitudeDelta == rhs.longitudeDelta } } ================================================ FILE: Sources/SwiftUIMapView/MapView+Xcode.swift ================================================ // // MapView+Xcode.swift // SwiftUIMapView // // Created by Sören Gade on 26.06.20. // #if canImport(DeveloperToolsSupport) import DeveloperToolsSupport @available(iOS 14.0, *) struct LibraryViewContent: LibraryContentProvider { var views: [LibraryItem] { LibraryItem(MapView()) } } #endif ================================================ FILE: Sources/SwiftUIMapView/MapView.swift ================================================ // // MapView.swift // SwiftUIMapView // // Created by Sören Gade on 14.01.20. // Copyright © 2020 Sören Gade. All rights reserved. // import SwiftUI import MapKit import Combine import UIKit /// Displays a map. /// The contents of the map are provided by the Apple Maps service. /// /// See the [official documentation](https://developer.apple.com/documentation/mapkit/mkmapview) for more information /// on the possibilities provided by the underlying service. public struct MapView: UIViewRepresentable { /// The region that is displayed. /// /// Note: The region might not be used as-is, as it might need to be fitted to the view's bounds. /// See [regionThatFits(_:)](https://developer.apple.com/documentation/mapkit/mkmapview/1452371-regionthatfits). @Binding private var region: MKCoordinateRegion? /// The currently selected annotations. /// /// When the user selects annotations on the map the value of this binding changes. /// Likewise, setting the value of this binding to a value selects the given annotations. @Binding private var selectedAnnotations: [MapViewAnnotation] /// Annotations that are displayed on the map. /// /// /// See the `selectedAnnotation` binding for more information about user selection of annotations. private let annotations: [MapViewAnnotation] /// The map type that is displayed. private let mapType: MKMapType /// Determines whether the map can be zoomed. private let isZoomEnabled: Bool /// Determines whether the map can be scrolled. private let isScrollEnabled: Bool /// Determines whether the map can be rotated. private let isRotateEnabled: Bool /// Determines whether the current user location is displayed. /// /// This requires the `NSLocationWhenInUseUsageDescription` key in the Info.plist to be set. /// In addition, you need to call [`CLLocationManager.requestWhenInUseAuthorization()`](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620562-requestwheninuseauthorization) /// to request for permission. private let showsUserLocation: Bool /// Sets the map's user tracking mode. private let userTrackingMode: MKUserTrackingMode /// Creates a new MapView. /// /// - Parameters: /// - mapType: The map type to display. /// - region: The region to display. /// - isZoomEnabled: Whether the map can be zoomed. /// - isScrollEnabled: Whether the map can be scrolled. /// - isRotateEnabled: Whether the map can be rotated. /// - showsUserLocation: Whether to display the user's current location. /// - userTrackingMode: The user tracking mode. /// - annotations: A list of `MapAnnotation`s that should be displayed on the map. /// - selectedAnnotation: A binding to the currently selected annotation, or `nil`. public init( mapType: MKMapType = .standard, region: Binding = .constant(nil), isZoomEnabled: Bool = true, isScrollEnabled: Bool = true, isRotateEnabled: Bool = true, showsUserLocation: Bool = false, userTrackingMode: MKUserTrackingMode = .none, annotations: [MapViewAnnotation] = [], selectedAnnotations: Binding<[MapViewAnnotation]> = .constant([]) ) { self.mapType = mapType self._region = region self.isZoomEnabled = isZoomEnabled self.isScrollEnabled = isScrollEnabled self.isRotateEnabled = isRotateEnabled self.showsUserLocation = showsUserLocation self.userTrackingMode = userTrackingMode self.annotations = annotations self._selectedAnnotations = selectedAnnotations } } // MARK: - UIViewRepresentable extension MapView { public func makeCoordinator() -> Coordinator { Coordinator() } public func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator // register custom annotation view classes mapView.register( MapAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier ) mapView.register( MapAnnotationClusterView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier ) // perform initial view state configure( mapView, context: context, animated: false ) return mapView } public func updateUIView( _ mapView: MKMapView, context: Context ) { configure( mapView, context: context, animated: true ) } } // MARK: Configuring view state private extension MapView { /// Configures the `mapView`'s state according to the current view state. func configure( _ mapView: MKMapView, context: Context, animated: Bool ) { if let region { let region = mapView.regionThatFits(region) if region.center != mapView.region.center || region.span != mapView.region.span { mapView.setRegion(region, animated: animated) } } mapView.mapType = mapType mapView.isZoomEnabled = isZoomEnabled mapView.isScrollEnabled = isScrollEnabled mapView.isRotateEnabled = isRotateEnabled mapView.showsUserLocation = showsUserLocation mapView.userTrackingMode = userTrackingMode // annotation configuration updateAnnotations(in: mapView) updateSelectedAnnotation( in: mapView, animated: animated ) // update coordinator with new view instance context.coordinator.onRegionChanged = { region in Task { // avoid modifying state during view update self.region = region } } context.coordinator.onAnnotationSelected = { annotation in selectedAnnotations.append(annotation) } context.coordinator.onAnnotationDeselected = { annotation in guard let index = selectedAnnotations.firstIndex(where: { annotation.isEqual($0) }) else { return } selectedAnnotations.remove(at: index) } } /// Updates the annotation property of the `mapView`. /// Calculates the difference between the current and new states and only executes changes on those diff sets. /// /// - Parameter mapView: The ``MKMapView`` to configure. func updateAnnotations(in mapView: MKMapView) { let currentAnnotations = mapView.mapViewAnnotations // remove old annotations let obsoleteAnnotations = currentAnnotations.filter { mapAnnotation in !annotations.contains { $0.isEqual(mapAnnotation) } } mapView.removeAnnotations(obsoleteAnnotations) // add new annotations let newAnnotations = annotations.filter { mapViewAnnotation in !currentAnnotations.contains { $0.isEqual(mapViewAnnotation) } } mapView.addAnnotations(newAnnotations) } /// Updates the selection annotations of the `mapView`. /// Calculates the difference between the current and new selection states and only executes changes on those diff sets. /// /// - Parameters: /// - mapView: The ``MKMapView`` to configure. /// - animated: Whether to animate the change. func updateSelectedAnnotation( in mapView: MKMapView, animated: Bool ) { // deselect annotations that are not currently selected let oldSelections = mapView.selectedMapViewAnnotations.filter { oldSelection in !selectedAnnotations.contains { oldSelection.isEqual($0) } } for annotation in oldSelections { mapView.deselectAnnotation(annotation, animated: false) } // select all new annotations let newSelections = selectedAnnotations.filter { selection in !mapView.selectedMapViewAnnotations.contains { selection.isEqual($0) } } for annotation in newSelections { mapView.selectAnnotation(annotation, animated: animated) } } } // MARK: - Coordinator public extension MapView { final class Coordinator: NSObject, MKMapViewDelegate { var onRegionChanged: (MKCoordinateRegion) -> Void = { _ in } var onAnnotationSelected: (MapViewAnnotation) -> Void = { _ in } var onAnnotationDeselected: (MapViewAnnotation) -> Void = { _ in } } } // MARK: MKMapViewDelegate extension MapView.Coordinator { public func mapView( _ mapView: MKMapView, didSelect view: MKAnnotationView ) { guard let mapAnnotation = view.annotation as? MapViewAnnotation else { return } onAnnotationSelected(mapAnnotation) } public func mapView( _ mapView: MKMapView, didDeselect view: MKAnnotationView ) { guard let mapAnnotation = view.annotation as? MapViewAnnotation else { return } onAnnotationDeselected(mapAnnotation) } public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { onRegionChanged(mapView.region) } } // MARK: - Previews #Preview { MapView() }