├── .github
└── workflows
│ └── swift.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── AdvancedMap.xcscheme
├── Example
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── contents.xcworkspacedata
├── Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Example.xcscheme
├── Example
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ConfigurationView.swift
│ ├── ContentView.swift
│ ├── Example.entitlements
│ ├── ExampleApp.swift
│ ├── Info.plist
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ └── READMESampleMap.swift
└── Package.swift
├── LICENSE
├── Package.swift
├── README.md
├── Resources
├── la-sat.png
├── lincoln.png
└── pointsAndOverlays.png
└── Sources
└── AdvancedMap
├── AdvancedMap+ViewRepresentable.swift
├── AdvancedMap.swift
├── AdvancedMapModifiers.swift
├── AnnotationViewFactory.swift
├── Configuration.swift
├── CrossPlatformBridging.swift
├── Logger.swift
├── MKUserLocation+AnnotationViewFactory.swift
├── MapKit+Equatable.swift
├── MapViewCoordinator.swift
├── OverlayRendererFactory.swift
├── SwiftUIView.swift
└── XViewRepresentableContext+Extras.swift
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Swift project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
3 |
4 | name: Swift
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: macos-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Select Xcode
20 | run: sudo xcode-select -s /Applications/Xcode_14.1.app/Contents/Developer
21 | - name: Clean Package
22 | run: swift package clean
23 | - name: Build Package
24 | run: swift build
25 | - name: Check SDKs
26 | run: xcodebuild -showsdks
27 | - name: Build Example Project macOS
28 | run: cd Example && xcodebuild -configuration Release -scheme Example -sdk macosx CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
29 | - name: Build iOS
30 | run: cd Example && xcodebuild -configuration Release -scheme Example -sdk iphoneos CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/AdvancedMap.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Example/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A66EA5FD298D52A300F9E5F9 /* READMESampleMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A66EA5FC298D52A300F9E5F9 /* READMESampleMap.swift */; };
11 | A6982BBC297C822200F792BE /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6982BBB297C822200F792BE /* ConfigurationView.swift */; };
12 | A6E5DD4B29524A100017FDB9 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E5DD4A29524A100017FDB9 /* ExampleApp.swift */; };
13 | A6E5DD4D29524A100017FDB9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E5DD4C29524A100017FDB9 /* ContentView.swift */; };
14 | A6E5DD4F29524A110017FDB9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6E5DD4E29524A110017FDB9 /* Assets.xcassets */; };
15 | A6E5DD5329524A110017FDB9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6E5DD5229524A110017FDB9 /* Preview Assets.xcassets */; };
16 | A6E6C8ED29524B2D001DD9FD /* AdvancedMap in Frameworks */ = {isa = PBXBuildFile; productRef = A6E6C8EC29524B2D001DD9FD /* AdvancedMap */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | A66EA5FC298D52A300F9E5F9 /* READMESampleMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = READMESampleMap.swift; sourceTree = ""; };
21 | A6982BBB297C822200F792BE /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = ""; };
22 | A6E5DD4729524A100017FDB9 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
23 | A6E5DD4A29524A100017FDB9 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
24 | A6E5DD4C29524A100017FDB9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
25 | A6E5DD4E29524A110017FDB9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
26 | A6E5DD5029524A110017FDB9 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; };
27 | A6E5DD5229524A110017FDB9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
28 | A6E5DD5929524A1F0017FDB9 /* SwiftUIAdvancedMap */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftUIAdvancedMap; path = ..; sourceTree = ""; };
29 | A6E6C8EB29524B28001DD9FD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
30 | /* End PBXFileReference section */
31 |
32 | /* Begin PBXFrameworksBuildPhase section */
33 | A6E5DD4429524A100017FDB9 /* Frameworks */ = {
34 | isa = PBXFrameworksBuildPhase;
35 | buildActionMask = 2147483647;
36 | files = (
37 | A6E6C8ED29524B2D001DD9FD /* AdvancedMap in Frameworks */,
38 | );
39 | runOnlyForDeploymentPostprocessing = 0;
40 | };
41 | /* End PBXFrameworksBuildPhase section */
42 |
43 | /* Begin PBXGroup section */
44 | A6E5DD3E29524A100017FDB9 = {
45 | isa = PBXGroup;
46 | children = (
47 | A6E5DD5929524A1F0017FDB9 /* SwiftUIAdvancedMap */,
48 | A6E5DD4929524A100017FDB9 /* Example */,
49 | A6E5DD4829524A100017FDB9 /* Products */,
50 | A6E5DD5A29524A470017FDB9 /* Frameworks */,
51 | );
52 | sourceTree = "";
53 | };
54 | A6E5DD4829524A100017FDB9 /* Products */ = {
55 | isa = PBXGroup;
56 | children = (
57 | A6E5DD4729524A100017FDB9 /* Example.app */,
58 | );
59 | name = Products;
60 | sourceTree = "";
61 | };
62 | A6E5DD4929524A100017FDB9 /* Example */ = {
63 | isa = PBXGroup;
64 | children = (
65 | A6E6C8EB29524B28001DD9FD /* Info.plist */,
66 | A66EA5FC298D52A300F9E5F9 /* READMESampleMap.swift */,
67 | A6E5DD4A29524A100017FDB9 /* ExampleApp.swift */,
68 | A6E5DD4C29524A100017FDB9 /* ContentView.swift */,
69 | A6E5DD4E29524A110017FDB9 /* Assets.xcassets */,
70 | A6E5DD5029524A110017FDB9 /* Example.entitlements */,
71 | A6E5DD5129524A110017FDB9 /* Preview Content */,
72 | A6982BBB297C822200F792BE /* ConfigurationView.swift */,
73 | );
74 | path = Example;
75 | sourceTree = "";
76 | };
77 | A6E5DD5129524A110017FDB9 /* Preview Content */ = {
78 | isa = PBXGroup;
79 | children = (
80 | A6E5DD5229524A110017FDB9 /* Preview Assets.xcassets */,
81 | );
82 | path = "Preview Content";
83 | sourceTree = "";
84 | };
85 | A6E5DD5A29524A470017FDB9 /* Frameworks */ = {
86 | isa = PBXGroup;
87 | children = (
88 | );
89 | name = Frameworks;
90 | sourceTree = "";
91 | };
92 | /* End PBXGroup section */
93 |
94 | /* Begin PBXNativeTarget section */
95 | A6E5DD4629524A100017FDB9 /* Example */ = {
96 | isa = PBXNativeTarget;
97 | buildConfigurationList = A6E5DD5629524A110017FDB9 /* Build configuration list for PBXNativeTarget "Example" */;
98 | buildPhases = (
99 | A6E5DD4329524A100017FDB9 /* Sources */,
100 | A6E5DD4429524A100017FDB9 /* Frameworks */,
101 | A6E5DD4529524A100017FDB9 /* Resources */,
102 | );
103 | buildRules = (
104 | );
105 | dependencies = (
106 | );
107 | name = Example;
108 | packageProductDependencies = (
109 | A6E6C8EC29524B2D001DD9FD /* AdvancedMap */,
110 | );
111 | productName = Example;
112 | productReference = A6E5DD4729524A100017FDB9 /* Example.app */;
113 | productType = "com.apple.product-type.application";
114 | };
115 | /* End PBXNativeTarget section */
116 |
117 | /* Begin PBXProject section */
118 | A6E5DD3F29524A100017FDB9 /* Project object */ = {
119 | isa = PBXProject;
120 | attributes = {
121 | BuildIndependentTargetsInParallel = 1;
122 | LastSwiftUpdateCheck = 1410;
123 | LastUpgradeCheck = 1420;
124 | TargetAttributes = {
125 | A6E5DD4629524A100017FDB9 = {
126 | CreatedOnToolsVersion = 14.1;
127 | };
128 | };
129 | };
130 | buildConfigurationList = A6E5DD4229524A100017FDB9 /* Build configuration list for PBXProject "Example" */;
131 | compatibilityVersion = "Xcode 14.0";
132 | developmentRegion = en;
133 | hasScannedForEncodings = 0;
134 | knownRegions = (
135 | en,
136 | Base,
137 | );
138 | mainGroup = A6E5DD3E29524A100017FDB9;
139 | productRefGroup = A6E5DD4829524A100017FDB9 /* Products */;
140 | projectDirPath = "";
141 | projectRoot = "";
142 | targets = (
143 | A6E5DD4629524A100017FDB9 /* Example */,
144 | );
145 | };
146 | /* End PBXProject section */
147 |
148 | /* Begin PBXResourcesBuildPhase section */
149 | A6E5DD4529524A100017FDB9 /* Resources */ = {
150 | isa = PBXResourcesBuildPhase;
151 | buildActionMask = 2147483647;
152 | files = (
153 | A6E5DD5329524A110017FDB9 /* Preview Assets.xcassets in Resources */,
154 | A6E5DD4F29524A110017FDB9 /* Assets.xcassets in Resources */,
155 | );
156 | runOnlyForDeploymentPostprocessing = 0;
157 | };
158 | /* End PBXResourcesBuildPhase section */
159 |
160 | /* Begin PBXSourcesBuildPhase section */
161 | A6E5DD4329524A100017FDB9 /* Sources */ = {
162 | isa = PBXSourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | A6E5DD4D29524A100017FDB9 /* ContentView.swift in Sources */,
166 | A6982BBC297C822200F792BE /* ConfigurationView.swift in Sources */,
167 | A66EA5FD298D52A300F9E5F9 /* READMESampleMap.swift in Sources */,
168 | A6E5DD4B29524A100017FDB9 /* ExampleApp.swift in Sources */,
169 | );
170 | runOnlyForDeploymentPostprocessing = 0;
171 | };
172 | /* End PBXSourcesBuildPhase section */
173 |
174 | /* Begin XCBuildConfiguration section */
175 | A6E5DD5429524A110017FDB9 /* Debug */ = {
176 | isa = XCBuildConfiguration;
177 | buildSettings = {
178 | ALWAYS_SEARCH_USER_PATHS = NO;
179 | CLANG_ANALYZER_NONNULL = YES;
180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
182 | CLANG_ENABLE_MODULES = YES;
183 | CLANG_ENABLE_OBJC_ARC = YES;
184 | CLANG_ENABLE_OBJC_WEAK = YES;
185 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
186 | CLANG_WARN_BOOL_CONVERSION = YES;
187 | CLANG_WARN_COMMA = YES;
188 | CLANG_WARN_CONSTANT_CONVERSION = YES;
189 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
190 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
191 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
192 | CLANG_WARN_EMPTY_BODY = YES;
193 | CLANG_WARN_ENUM_CONVERSION = YES;
194 | CLANG_WARN_INFINITE_RECURSION = YES;
195 | CLANG_WARN_INT_CONVERSION = YES;
196 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
197 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
198 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
200 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
201 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
202 | CLANG_WARN_STRICT_PROTOTYPES = YES;
203 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
204 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
205 | CLANG_WARN_UNREACHABLE_CODE = YES;
206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
207 | COPY_PHASE_STRIP = NO;
208 | DEAD_CODE_STRIPPING = YES;
209 | DEBUG_INFORMATION_FORMAT = dwarf;
210 | ENABLE_STRICT_OBJC_MSGSEND = YES;
211 | ENABLE_TESTABILITY = YES;
212 | GCC_C_LANGUAGE_STANDARD = gnu11;
213 | GCC_DYNAMIC_NO_PIC = NO;
214 | GCC_NO_COMMON_BLOCKS = YES;
215 | GCC_OPTIMIZATION_LEVEL = 0;
216 | GCC_PREPROCESSOR_DEFINITIONS = (
217 | "DEBUG=1",
218 | "$(inherited)",
219 | );
220 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
221 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
222 | GCC_WARN_UNDECLARED_SELECTOR = YES;
223 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
224 | GCC_WARN_UNUSED_FUNCTION = YES;
225 | GCC_WARN_UNUSED_VARIABLE = YES;
226 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
227 | MTL_FAST_MATH = YES;
228 | ONLY_ACTIVE_ARCH = YES;
229 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
230 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
231 | };
232 | name = Debug;
233 | };
234 | A6E5DD5529524A110017FDB9 /* Release */ = {
235 | isa = XCBuildConfiguration;
236 | buildSettings = {
237 | ALWAYS_SEARCH_USER_PATHS = NO;
238 | CLANG_ANALYZER_NONNULL = YES;
239 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
241 | CLANG_ENABLE_MODULES = YES;
242 | CLANG_ENABLE_OBJC_ARC = YES;
243 | CLANG_ENABLE_OBJC_WEAK = YES;
244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
245 | CLANG_WARN_BOOL_CONVERSION = YES;
246 | CLANG_WARN_COMMA = YES;
247 | CLANG_WARN_CONSTANT_CONVERSION = YES;
248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
250 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
251 | CLANG_WARN_EMPTY_BODY = YES;
252 | CLANG_WARN_ENUM_CONVERSION = YES;
253 | CLANG_WARN_INFINITE_RECURSION = YES;
254 | CLANG_WARN_INT_CONVERSION = YES;
255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
259 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
261 | CLANG_WARN_STRICT_PROTOTYPES = YES;
262 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
263 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
264 | CLANG_WARN_UNREACHABLE_CODE = YES;
265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
266 | COPY_PHASE_STRIP = NO;
267 | DEAD_CODE_STRIPPING = YES;
268 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
269 | ENABLE_NS_ASSERTIONS = NO;
270 | ENABLE_STRICT_OBJC_MSGSEND = YES;
271 | GCC_C_LANGUAGE_STANDARD = gnu11;
272 | GCC_NO_COMMON_BLOCKS = YES;
273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
275 | GCC_WARN_UNDECLARED_SELECTOR = YES;
276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
277 | GCC_WARN_UNUSED_FUNCTION = YES;
278 | GCC_WARN_UNUSED_VARIABLE = YES;
279 | MTL_ENABLE_DEBUG_INFO = NO;
280 | MTL_FAST_MATH = YES;
281 | SWIFT_COMPILATION_MODE = wholemodule;
282 | SWIFT_OPTIMIZATION_LEVEL = "-O";
283 | };
284 | name = Release;
285 | };
286 | A6E5DD5729524A110017FDB9 /* Debug */ = {
287 | isa = XCBuildConfiguration;
288 | buildSettings = {
289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
291 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements;
292 | CODE_SIGN_STYLE = Automatic;
293 | CURRENT_PROJECT_VERSION = 1;
294 | DEAD_CODE_STRIPPING = YES;
295 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
296 | ENABLE_PREVIEWS = YES;
297 | GENERATE_INFOPLIST_FILE = YES;
298 | INFOPLIST_FILE = Example/Info.plist;
299 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
300 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
301 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
302 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
303 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
304 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
305 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
306 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
307 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
308 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
309 | IPHONEOS_DEPLOYMENT_TARGET = 16.1;
310 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
311 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
312 | MACOSX_DEPLOYMENT_TARGET = 13.0;
313 | MARKETING_VERSION = 1.0;
314 | PRODUCT_BUNDLE_IDENTIFIER = com.msena.SwiftUIAdvancedMap.Example;
315 | PRODUCT_NAME = "$(TARGET_NAME)";
316 | SDKROOT = auto;
317 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
318 | SUPPORTS_MACCATALYST = NO;
319 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
320 | SWIFT_EMIT_LOC_STRINGS = YES;
321 | SWIFT_VERSION = 5.0;
322 | TARGETED_DEVICE_FAMILY = "1,2,3";
323 | };
324 | name = Debug;
325 | };
326 | A6E5DD5829524A110017FDB9 /* Release */ = {
327 | isa = XCBuildConfiguration;
328 | buildSettings = {
329 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
330 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
331 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements;
332 | CODE_SIGN_STYLE = Automatic;
333 | CURRENT_PROJECT_VERSION = 1;
334 | DEAD_CODE_STRIPPING = YES;
335 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
336 | ENABLE_PREVIEWS = YES;
337 | GENERATE_INFOPLIST_FILE = YES;
338 | INFOPLIST_FILE = Example/Info.plist;
339 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
340 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
341 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
342 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
343 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
344 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
345 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
346 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
347 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
348 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
349 | IPHONEOS_DEPLOYMENT_TARGET = 16.1;
350 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
351 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
352 | MACOSX_DEPLOYMENT_TARGET = 13.0;
353 | MARKETING_VERSION = 1.0;
354 | PRODUCT_BUNDLE_IDENTIFIER = com.msena.SwiftUIAdvancedMap.Example;
355 | PRODUCT_NAME = "$(TARGET_NAME)";
356 | SDKROOT = auto;
357 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
358 | SUPPORTS_MACCATALYST = NO;
359 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
360 | SWIFT_EMIT_LOC_STRINGS = YES;
361 | SWIFT_VERSION = 5.0;
362 | TARGETED_DEVICE_FAMILY = "1,2,3";
363 | };
364 | name = Release;
365 | };
366 | /* End XCBuildConfiguration section */
367 |
368 | /* Begin XCConfigurationList section */
369 | A6E5DD4229524A100017FDB9 /* Build configuration list for PBXProject "Example" */ = {
370 | isa = XCConfigurationList;
371 | buildConfigurations = (
372 | A6E5DD5429524A110017FDB9 /* Debug */,
373 | A6E5DD5529524A110017FDB9 /* Release */,
374 | );
375 | defaultConfigurationIsVisible = 0;
376 | defaultConfigurationName = Release;
377 | };
378 | A6E5DD5629524A110017FDB9 /* Build configuration list for PBXNativeTarget "Example" */ = {
379 | isa = XCConfigurationList;
380 | buildConfigurations = (
381 | A6E5DD5729524A110017FDB9 /* Debug */,
382 | A6E5DD5829524A110017FDB9 /* Release */,
383 | );
384 | defaultConfigurationIsVisible = 0;
385 | defaultConfigurationName = Release;
386 | };
387 | /* End XCConfigurationList section */
388 |
389 | /* Begin XCSwiftPackageProductDependency section */
390 | A6E6C8EC29524B2D001DD9FD /* AdvancedMap */ = {
391 | isa = XCSwiftPackageProductDependency;
392 | productName = AdvancedMap;
393 | };
394 | /* End XCSwiftPackageProductDependency section */
395 | };
396 | rootObject = A6E5DD3F29524A100017FDB9 /* Project object */;
397 | }
398 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/ConfigurationView.swift:
--------------------------------------------------------------------------------
1 | import AdvancedMap
2 | import MapKit
3 | import SwiftUI
4 |
5 |
6 | /// This example demonstrates changing the map's configuration at runtime.
7 | struct ConfigurationView: View {
8 |
9 | enum ConfigurationStyle: String, CaseIterable {
10 | case standard, hybrid, satellite
11 | }
12 |
13 | @State var selectedStyle: ConfigurationStyle = .standard
14 | var configuration: Configuration {
15 | switch selectedStyle {
16 | case .standard: return .standard(.default, .realistic, .includingAll, false)
17 | case .hybrid: return .hybrid(.realistic, .includingAll, false)
18 | case .satellite: return .imagery(.realistic)
19 | }
20 | }
21 | @State var mapVisibility: MapVisibility? = nil
22 |
23 | var body: some View {
24 | NavigationStack {
25 | ZStack {
26 | AdvancedMap(mapVisibility: $mapVisibility)
27 | .mapConfiguration(configuration)
28 | .ignoresSafeArea()
29 | .onAppear {
30 | CLLocationManager().requestWhenInUseAuthorization()
31 | #if !os(tvOS)
32 | CLLocationManager().startUpdatingLocation()
33 | #endif
34 | }
35 | }
36 | .toolbar {
37 | ToolbarItem(placement: .principal) {
38 | Picker(selection: $selectedStyle) {
39 | ForEach(ConfigurationStyle.allCases, id: \.self) { style in
40 | Text(style.rawValue.localizedCapitalized)
41 | }
42 | } label: {
43 | Text("Select a style")
44 | }.pickerStyle(.segmented)
45 | }
46 | }
47 | #if os(iOS)
48 | .navigationBarTitleDisplayMode(.inline)
49 | .toolbarBackground(.visible, for: .navigationBar)
50 | #endif
51 | }
52 | }
53 | }
54 | struct ConfigurationView_Previews: PreviewProvider {
55 | static var previews: some View {
56 | ConfigurationView()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Example/Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | import AdvancedMap
2 | import MapKit
3 | import SwiftUI
4 |
5 | extension MKMapRect {
6 | static let buschGardens = MKMapRect(
7 | origin: MKMapPoint(
8 | x: 77063948.43628144,
9 | y: 104261566.7753939
10 | ),
11 | size: MKMapSize(
12 | width: 5973.566551163793,
13 | height: 6648.14294731617
14 | )
15 | )
16 | }
17 |
18 |
19 | extension MKPointAnnotation {
20 |
21 | public override func isEqual(_ object: Any?) -> Bool {
22 | guard let other = object as? Self else { return false }
23 | return coordinate == other.coordinate && title == other.title && subtitle == other.subtitle
24 | }
25 |
26 | static let annotationViewFactory = AnnotationViewFactory(register: { mapView in
27 | mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: String(describing: MKPointAnnotation.self))
28 | }, view: { mapView, annotation in
29 | let view = mapView.dequeueReusableAnnotationView(withIdentifier: String(describing: MKPointAnnotation.self), for: annotation)
30 | #if os(iOS) || os(macOS)
31 | (view as? MKMarkerAnnotationView)?.isDraggable = true
32 | #endif
33 | return view
34 | })
35 | }
36 |
37 | struct ContentView: View {
38 |
39 | enum ConfigurationStyle: String, CaseIterable {
40 | case standard, hybrid, sattelite
41 | }
42 |
43 | @State var selectedStyle: ConfigurationStyle = .standard
44 | var configuration: Configuration {
45 | switch selectedStyle {
46 | case .standard: return .standard(.default, .realistic, .includingAll, false)
47 | case .hybrid: return .hybrid(.realistic, .includingAll, false)
48 | case .sattelite: return .imagery(.realistic)
49 | }
50 | }
51 | @State var mapVisibility: MapVisibility? = nil
52 | @State var overlays: [MKOverlay] = [MKOverlay]()
53 | @State var annotations: [MKPointAnnotation] = [MKPointAnnotation]()
54 | #if os(iOS) || os(macOS)
55 | @State var userTrackingMode: MKUserTrackingMode = .follow
56 | #endif
57 |
58 | var body: some View {
59 | NavigationStack {
60 | ZStack {
61 | AdvancedMap(mapVisibility: $mapVisibility)
62 | .mapConfiguration(configuration)
63 | .annotations(annotations, annotationViewFactory: annotationViewFacotry())
64 | .overlays(overlays, overlayRendererFactory: overlayRendererFactory())
65 | .onLongPressMapGesture(tapOrClickHandler)
66 | #if !os(tvOS)
67 | .annotationDragHandler(annotationDragHandler)
68 | #endif
69 | .ignoresSafeArea()
70 | .onAppear {
71 | CLLocationManager().requestWhenInUseAuthorization()
72 | #if !os(tvOS)
73 | CLLocationManager().startUpdatingLocation()
74 | #endif
75 | }
76 | }
77 | .toolbar {
78 | ToolbarItem {
79 | Picker(selection: $selectedStyle) {
80 | ForEach(ConfigurationStyle.allCases, id: \.self) { style in
81 | Text(style.rawValue.localizedCapitalized)
82 | }
83 | } label: {
84 | Text("Select a style")
85 | }
86 | }
87 | ToolbarItem {
88 | Button("New York Center") {
89 | mapVisibility = .centerCoordinate(.newYork)
90 | }
91 | }
92 | ToolbarItem {
93 | Button("Los Angeles Region (Animated)") {
94 | withAnimation {
95 | mapVisibility = .region(
96 | MKCoordinateRegion(
97 | center: .losAngeles,
98 | latitudinalMeters: .oneHundredKm,
99 | longitudinalMeters: .oneHundredKm
100 | )
101 | )
102 | }
103 | }
104 | }
105 | ToolbarItem {
106 | Button("Show Annotations") {
107 | mapVisibility = .annotations(self.annotations)
108 | }
109 | }
110 | ToolbarItem {
111 | Button("Lincoln Memorial") {
112 | mapVisibility = .camera(MKMapCamera(lookingAtCenter: CLLocationCoordinate2D(north: 38.88919130, west: 77.05026405), fromDistance: 110, pitch: 60, heading: 249))
113 | }
114 | }
115 | ToolbarItem {
116 | Button("Apple Park Point Region (Animated)") {
117 | withAnimation {
118 | mapVisibility = .visibleMapRect(
119 | MKMapRect(
120 | origin: MKMapPoint(.applePark),
121 | size: MKMapSize(
122 | width: MKMapPointsPerMeterAtLatitude(CLLocationCoordinate2D.applePark.latitude) * 800,
123 | height: MKMapPointsPerMeterAtLatitude(CLLocationCoordinate2D.applePark.latitude) * 1000
124 | )
125 | )
126 | )
127 | }
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | func updateOverlays() {
135 | if annotations.count >= 3 {
136 | let coordinates = annotations.map(\.coordinate)
137 | overlays = [MKPolygon(coordinates: coordinates, count: coordinates.count)]
138 | }
139 | }
140 |
141 | func annotationViewFacotry() -> AnnotationViewFactory {
142 | .combine(
143 | MKUserLocation.mkUserLocationViewFactory,
144 | MKPointAnnotation.annotationViewFactory
145 | )
146 | }
147 |
148 | func overlayRendererFactory() -> OverlayRendererFactory {
149 | .factory(for: MKPolygon.self) { polygon in
150 | let renderer = MKPolygonRenderer(polygon: polygon)
151 | renderer.strokeColor = .red
152 | renderer.lineWidth = 4
153 | renderer.fillColor = .red.withAlphaComponent(0.3)
154 | return renderer
155 | }
156 | }
157 |
158 | func tapOrClickHandler(location: CLLocationCoordinate2D) {
159 | let annotation = MKPointAnnotation()
160 | annotation.coordinate = location
161 | annotation.title = "A"
162 | annotations.append(annotation)
163 | updateOverlays()
164 | }
165 |
166 | #if !os(tvOS)
167 | func annotationDragHandler(
168 | annotation: MKAnnotation,
169 | location: CLLocationCoordinate2D,
170 | oldState: MKAnnotationView.DragState,
171 | newState: MKAnnotationView.DragState
172 | ) {
173 | guard let index = annotations.firstIndex(where: { pointAnnotation in
174 | pointAnnotation === annotation
175 | }) else { return }
176 | annotations[index].coordinate = location
177 | updateOverlays()
178 | }
179 | #endif
180 | }
181 |
182 | // MARK: - Previews
183 |
184 | struct ContentView_Previews: PreviewProvider {
185 | static var previews: some View {
186 | ContentView()
187 | }
188 | }
189 |
190 |
191 | extension CLLocationCoordinate2D {
192 | init(north: CLLocationDegrees, west: CLLocationDegrees) {
193 | self.init(latitude: north, longitude: -west)
194 | }
195 |
196 | static let newYork = CLLocationCoordinate2D(north: 40.74850, west: 73.98557)
197 | static let losAngeles = CLLocationCoordinate2D(north: 34.0, west: 118.2)
198 | static let applePark = CLLocationCoordinate2D(north: 37.33759, west: 122.01423)
199 | }
200 |
201 | extension CLLocationDistance {
202 | static let oneHundredKm = 100_000.0
203 | }
204 |
--------------------------------------------------------------------------------
/Example/Example/Example.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.personal-information.location
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Example/Example/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct ExampleApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSLocationWhenInUseUsageDescription
6 | Example App
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/READMESampleMap.swift:
--------------------------------------------------------------------------------
1 | import AdvancedMap
2 | import MapKit
3 | import SwiftUI
4 |
5 | struct TappableMapWithAnnotations: View {
6 |
7 | static let annotationViewFactory = AnnotationViewFactory(
8 | register: { mapView in
9 | mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: String(describing: MKPointAnnotation.self))
10 | },
11 | view: { mapView, annotation in
12 | mapView.dequeueReusableAnnotationView(withIdentifier: String(describing: MKPointAnnotation.self), for: annotation)
13 | }
14 | )
15 |
16 | @State var mapVisibility: MapVisibility?
17 | @State var annotations: [MKPointAnnotation] = [MKPointAnnotation]()
18 |
19 | var body: some View {
20 | AdvancedMap(mapVisibility: $mapVisibility)
21 | .annotations(annotations, annotationViewFactory: Self.annotationViewFactory)
22 | #if !os(tvOS)
23 | .onTapOrClickMapGesture { coordinate in
24 | let annotation = MKPointAnnotation()
25 | annotation.coordinate = coordinate
26 | annotations.append(annotation)
27 | }
28 | #endif
29 | }
30 | }
31 |
32 | struct TappableMap: View {
33 | var body: some View {
34 | // Uses MKMapView's behavior of starting the map over the
35 | // phone's current country. Still scrollable by default.
36 | AdvancedMap(mapVisibility: .constant(nil))
37 | #if !os(tvOS)
38 | .onTapOrClickMapGesture { coordinate in
39 | print("Tapped map at: \(coordinate)")
40 | }
41 | #endif
42 | }
43 | }
44 |
45 | struct READMESampleMap_Previews: PreviewProvider {
46 | static var previews: some View {
47 | TappableMap()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Example/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 |
3 | import PackageDescription
4 |
5 | // Leave blank. This is only here so that Xcode doesn't display it.
6 |
7 | let package = Package(
8 | name: "Example",
9 | products: [],
10 | targets: []
11 | )
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Michael Sena
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SwiftUIAdvancedMap",
7 | platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16)],
8 | products: [
9 | .library(
10 | name: "AdvancedMap",
11 | targets: ["AdvancedMap"]
12 | )
13 | ],
14 | dependencies: [],
15 | targets: [
16 | .target(
17 | name: "AdvancedMap",
18 | dependencies: []
19 | )
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚠️ Deprecated ⚠️
2 |
3 | This project was started before iOS 17's SwiftUI for MapKit which mostly obviates the need for this library. I will likely not be making further changes to this project. For more information see [this wwdc video](https://developer.apple.com/videos/play/wwdc2023/10043/).
4 |
5 |
6 |
7 | # SwiftUIAdvancedMap
8 |
9 | [](https://github.com/sena-mike/SwiftUIAdvancedMap/actions/workflows/swift.yml)
10 |
11 | A wrapper around MKMapView with more functionality than Map.
12 |
13 | | Points and Overlays | Camera | Styling |
14 | |:----------|:----------|:----------|
15 | |  |  |  |
16 |
17 |
18 |
19 | | Feature | `AdvancedMap` | `MapKit.Map` |
20 | |:----------|:----------|:----------|
21 | | Tap/Long Press Gestures with Map coordinates passed into the Handlers. | ✅ | ❌ |
22 | | Annotations with Drag and Drop support | ✅
(UIKit Annotation Views) | ✅
(SwiftUI Annotation Views) |
23 | | Overlays | ✅
(UIKit Overlay Views) | ❌ |
24 | | Specify EdgeInsets for UI overlays | ✅ | ❌ |
25 | | Display User Location | ✅
(via `AnnoatationViewFactory`) | ✅
(as an initialization parameter) |
26 | | Region State Changing Handler, a callback that informs when a map change Animation in progress. | ✅ | ❌ |
27 | | Binding to Optional map region so map is initially positioned around country bounding box. | ✅ | ❌ |
28 | | `MKMapCamera` support | ✅ | ❌ |
29 |
30 |
31 | ### Tap or Click Gesture with Coordinate on Map.
32 |
33 | ```swift
34 | struct TappableMap: View {
35 | var body: some View {
36 | // `.constant(nil)` uses MKMapView's behavior of starting the map over the phone's current country.
37 | // Still scrollable by default.
38 | AdvancedMap(mapRect: .constant(nil))
39 | .onTapOrClickMapGesture { coordinate in
40 | print("Tapped map at: \(coordinate)")
41 | }
42 | }
43 | }
44 | ```
45 |
46 | ### Rendering Annotations
47 |
48 | ```swift
49 | struct TappableMap: View {
50 |
51 | static let annotationViewFactory = AnnotationViewFactory(
52 | register: { mapView in
53 | mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: String(describing: MKPointAnnotation.self))
54 | },
55 | view: { mapView, annotation in
56 | mapView.dequeueReusableAnnotationView(withIdentifier: String(describing: MKPointAnnotation.self), for: annotation)
57 | }
58 | )
59 |
60 | @State var mapVisibility: MapVisibility?
61 | @State var annotations: [MKPointAnnotation] = [MKPointAnnotation]()
62 |
63 | var body: some View {
64 | AdvancedMap(mapVisibility: $mapVisibility)
65 | .annotations(annotations, annotationViewFactory: Self.annotationViewFactory)
66 | .onTapOrClickMapGesture { coordinate in
67 | let annotation = MKPointAnnotation()
68 | annotation.coordinate = coordinate
69 | annotations.append(annotation)
70 | }
71 | }
72 | }
73 | ```
74 |
75 | Inspired by, and sometimes stealing from, the following projects:
76 | * https://github.com/pauljohanneskraft/Map
77 | * https://github.com/darrarski/SwiftUIMKMapView
78 | * https://github.com/sgade/swiftui-mapview
79 |
--------------------------------------------------------------------------------
/Resources/la-sat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sena-mike/SwiftUIAdvancedMap/21bb29037e98905cea0328e2b6306445b07015ee/Resources/la-sat.png
--------------------------------------------------------------------------------
/Resources/lincoln.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sena-mike/SwiftUIAdvancedMap/21bb29037e98905cea0328e2b6306445b07015ee/Resources/lincoln.png
--------------------------------------------------------------------------------
/Resources/pointsAndOverlays.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sena-mike/SwiftUIAdvancedMap/21bb29037e98905cea0328e2b6306445b07015ee/Resources/pointsAndOverlays.png
--------------------------------------------------------------------------------
/Sources/AdvancedMap/AdvancedMap+ViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 | import SwiftUI
3 |
4 | extension AdvancedMap: XViewRepresentable {
5 |
6 | public func makeUIView(context: XViewRepresentableContext) -> MKMapView {
7 | _makeView(context: context)
8 | }
9 |
10 | public func makeNSView(context: Context) -> MKMapView {
11 | _makeView(context: context)
12 | }
13 |
14 | func _makeView(context: XViewRepresentableContext) -> MKMapView {
15 | logger.debug("Creating MKMapView")
16 | // Without providing a non-zero frame the user tracking mode is reset to `none`
17 | // in the delegate after setting on the map.
18 | // https://stackoverflow.com/questions/61262404/mkmapview-usertrackingmode-reset-in-swiftui
19 | let newMapView = MKMapView(frame: .init(x: 0, y: 0, width: 1, height: 1))
20 |
21 | context.coordinator.mapView = newMapView
22 | newMapView.delegate = context.coordinator
23 | if tapOrClickHandler != nil {
24 | context.coordinator.addTapOrClickGestureRecognizer(mapView: newMapView)
25 | }
26 |
27 | if longPressHandler != nil {
28 | context.coordinator.addLongTapOrClickGestureRecognizer(mapView: newMapView)
29 | }
30 |
31 | annotationViewFactory?.register(in: newMapView)
32 | return newMapView
33 | }
34 |
35 | public func updateNSView(
36 | _ mapView: MKMapView,
37 | context: XViewRepresentableContext
38 | ) {
39 | update(mapView, context: context)
40 | }
41 |
42 | public func updateUIView(
43 | _ mapView: MKMapView,
44 | context: XViewRepresentableContext
45 | ) {
46 | update(mapView, context: context)
47 | }
48 |
49 | public func makeCoordinator() -> Coordinator {
50 | return Coordinator(advancedMap: self)
51 | }
52 |
53 | func update(_ mapView: MKMapView, context: Context) {
54 | if let mapVisibility, !context.coordinator.isChangingRegion {
55 | switch mapVisibility {
56 | case .centerCoordinate(let coordinate):
57 | if mapView.centerCoordinate != coordinate {
58 | mapView.setCenter(coordinate, animated: context.shouldAnimateChanges)
59 | }
60 | case .region(let region):
61 | if mapView.region != region {
62 | mapView.setRegion(region, animated: context.shouldAnimateChanges)
63 | }
64 | case .visibleMapRect(let mapRect):
65 | if mapView.visibleMapRect != mapRect {
66 | mapView.setVisibleMapRect(mapRect, edgePadding: edgeInsets, animated: context.shouldAnimateChanges)
67 | }
68 | case .annotations(let annotations):
69 | mapView.showAnnotations(annotations, animated: context.shouldAnimateChanges)
70 | case .camera(let camera):
71 | if mapView.camera != camera {
72 | mapView.setCamera(camera, animated: context.shouldAnimateChanges)
73 | }
74 | }
75 | }
76 |
77 | switch mapConfiguration {
78 | case let .standard(emphasisStyle, elevationStyle, pointOfInterestFilter, showsTraffic):
79 | let style = MKStandardMapConfiguration(elevationStyle: elevationStyle, emphasisStyle: emphasisStyle)
80 | style.pointOfInterestFilter = pointOfInterestFilter
81 | style.showsTraffic = showsTraffic
82 | mapView.preferredConfiguration = style
83 | case let .hybrid(elevationStyle, pointOfInterestFilter, showsTraffic):
84 | let style = MKHybridMapConfiguration(elevationStyle: elevationStyle)
85 | style.pointOfInterestFilter = pointOfInterestFilter
86 | style.showsTraffic = showsTraffic
87 | mapView.preferredConfiguration = style
88 | case let .imagery(elevationStyle):
89 | let style = MKImageryMapConfiguration(elevationStyle: elevationStyle)
90 | mapView.preferredConfiguration = style
91 | case .none:
92 | break
93 | }
94 |
95 | // Commmon
96 | mapView.showsUserLocation = showsUserLocation
97 | mapView.isZoomEnabled = isZoomEnabled
98 | mapView.isScrollEnabled = isScrollEnabled
99 |
100 | // iOS or macOS
101 | #if os(iOS) || os(macOS)
102 | mapView.isRotateEnabled = isRotationEnabled
103 | mapView.isPitchEnabled = isPitchEnabled
104 | mapView.showsCompass = showsCompass
105 | if mapView.userTrackingMode != userTrackingMode {
106 | mapView.setUserTrackingMode(userTrackingMode, animated: false)
107 | }
108 | #endif
109 |
110 | // iOS or tvOS
111 | #if os(iOS) || os(tvOS)
112 | // Setting this to `true` crashes on macOS.. as of Xcode 14.1
113 | mapView.showsScale = showsScale
114 | #endif
115 |
116 | // macOS Only
117 | #if os(macOS)
118 | mapView.showsPitchControl = showsPitchControl
119 | mapView.showsZoomControls = showsZoomControls
120 | #endif
121 |
122 | mapView.annotations.forEach { annotation in
123 | if !annotations.contains(where: { $0.isEqual(annotation) }) {
124 | mapView.removeAnnotation(annotation)
125 | }
126 | }
127 | annotations.forEach { annotation in
128 | if !mapView.annotations.contains(where: { $0.isEqual(annotation) }) {
129 | mapView.addAnnotation(annotation)
130 | }
131 | }
132 |
133 | mapView.overlays.forEach { overlay in
134 | if !overlays.contains(where: { $0.isEqual(overlay) }) {
135 | mapView.removeOverlay(overlay)
136 | }
137 | }
138 | overlays.forEach { overlay in
139 | if !mapView.overlays.contains(where: { $0.isEqual(overlay) }) {
140 | mapView.addOverlay(overlay)
141 | }
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/AdvancedMap.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 | import MapKit
3 | import SwiftUI
4 |
5 | public enum MapVisibility {
6 | case region(MKCoordinateRegion)
7 | case centerCoordinate(CLLocationCoordinate2D)
8 | case visibleMapRect(MKMapRect)
9 | case annotations([MKAnnotation])
10 | case camera(MKMapCamera)
11 | }
12 |
13 | public struct AdvancedMap {
14 |
15 | public typealias DidTapOrClickMapHandler = (CLLocationCoordinate2D) -> Void
16 | public typealias LongPressMapHandler = DidTapOrClickMapHandler
17 | public typealias RegionChangingHandler = (_ changing: Bool, _ animated: Bool) -> Void
18 |
19 | #if os(iOS) || os(macOS)
20 | public typealias AnnotationDragHandler = (
21 | _ annotation: MKAnnotation,
22 | _ location: CLLocationCoordinate2D,
23 | _ oldState: MKAnnotationView.DragState,
24 | _ newState: MKAnnotationView.DragState
25 | ) -> Void
26 | #endif
27 |
28 | @Environment(\.mapConfiguration) var mapConfiguration
29 |
30 | /// Use `mapVisibility` to control what is currently visible on the map. Set values to this binding to change the map
31 | /// programmatically, when the user pans around, zooms or tilts the map the binding is written to by the Map.
32 | ///
33 | /// Currently there is no way to fetch a different kind of `MapVisibility` value from the map, the map will only write in the same
34 | /// case of the enum it received. However, if `nil` is passed initially the map **will** begin writing out the `.centerCoordinate`
35 | /// case as the user moves the map.
36 | @Binding public var mapVisibility: MapVisibility?
37 |
38 | #if os(iOS) || os(macOS)
39 | @Binding public var userTrackingMode: MKUserTrackingMode
40 | #endif
41 |
42 | @Environment(\.edgeInsets) var edgeInsets
43 | @Environment(\.showsUserLocation) var showsUserLocation
44 | @Environment(\.isZoomEnabled) var isZoomEnabled
45 | @Environment(\.isScrollEnabled) var isScrollEnabled
46 | @Environment(\.isRotationEnabled) var isRotationEnabled
47 | @Environment(\.isPitchEnabled) var isPitchEnabled
48 | #if os(macOS)
49 | @Environment(\.showsPitchControl) var showsPitchControl
50 | @Environment(\.showsZoomControls) var showsZoomControls
51 | #endif
52 | @Environment(\.showsCompass) var showsCompass
53 | @Environment(\.showsScale) var showsScale
54 | @Environment(\.mapAnnotations) var annotations
55 | @Environment(\.annotationViewFactory) var annotationViewFactory
56 | @Environment(\.mapOverlays) var overlays
57 | @Environment(\.overlayRendererFactory) var overlayRendererFactory
58 | @Environment(\.onTapOrClickGesture) var tapOrClickHandler
59 | @Environment(\.onLongPressMapGesture) var longPressHandler
60 | #if os(iOS) || os(macOS)
61 | @Environment(\.onAnnotationDragGesture) var annotationDragHandler
62 | #endif
63 | @Environment(\.mapRegionChangingHandler) var regionChangingHandler
64 | }
65 |
66 | extension AdvancedMap {
67 | #if os(tvOS)
68 | public init(mapVisibility: Binding) {
69 | self._mapVisibility = mapVisibility
70 | }
71 | #else
72 | public init(
73 | mapVisibility: Binding,
74 | userTrackingMode: Binding = .constant(MKUserTrackingMode.none)
75 | ) {
76 | self._mapVisibility = mapVisibility
77 | self._userTrackingMode = userTrackingMode
78 | }
79 | #endif
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/AdvancedMapModifiers.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 | import SwiftUI
3 |
4 | struct ConfigurationKey: EnvironmentKey {
5 | static let defaultValue: Configuration? = nil
6 | }
7 |
8 | struct EdgeInsetsKey: EnvironmentKey {
9 | static let defaultValue: XEdgeInsets = .init()
10 | }
11 |
12 | struct ShowsUserLocationKey: EnvironmentKey {
13 | static let defaultValue: Bool = false
14 | }
15 |
16 | struct IsZoomEnabledKey: EnvironmentKey {
17 | static let defaultValue: Bool = true
18 | }
19 |
20 | struct IsScrollEnabledKey: EnvironmentKey {
21 | static let defaultValue: Bool = true
22 | }
23 |
24 | struct IsRotationEnabledKey: EnvironmentKey {
25 | static let defaultValue: Bool = true
26 | }
27 |
28 | struct IsPitchEnabledKey: EnvironmentKey {
29 | static let defaultValue: Bool = true
30 | }
31 |
32 | struct ShowsPitchControlKey: EnvironmentKey {
33 | static let defaultValue: Bool = false
34 | }
35 |
36 | struct ShowsZoomControlsKey: EnvironmentKey {
37 | static let defaultValue: Bool = false
38 | }
39 |
40 | struct ShowsCompassKey: EnvironmentKey {
41 | static let defaultValue: Bool = false
42 | }
43 |
44 | struct ShowsScaleKey: EnvironmentKey {
45 | static let defaultValue: Bool = false
46 | }
47 |
48 | struct MapAnnotationsKey: EnvironmentKey {
49 | static let defaultValue: [MKAnnotation] = []
50 | }
51 |
52 | struct MapAnnotationViewFactoryKey: EnvironmentKey {
53 | static let defaultValue: AnnotationViewFactory? = nil
54 | }
55 |
56 | struct MapOverlaysKey: EnvironmentKey {
57 | static let defaultValue: [MKOverlay] = []
58 | }
59 |
60 | struct MapOverlayRendererFactoryKey: EnvironmentKey {
61 | static let defaultValue: OverlayRendererFactory? = nil
62 | }
63 |
64 | struct OnTapOrClickMapGestureKey: EnvironmentKey {
65 | static let defaultValue: AdvancedMap.DidTapOrClickMapHandler? = nil
66 | }
67 |
68 | struct OnLongPressMapGestureKey: EnvironmentKey {
69 | static let defaultValue: AdvancedMap.LongPressMapHandler? = nil
70 | }
71 |
72 | #if os(iOS) || os(macOS)
73 | struct OnAnnotationDragGestureKey: EnvironmentKey {
74 | static let defaultValue: AdvancedMap.AnnotationDragHandler? = nil
75 | }
76 | #endif
77 |
78 | struct OnMapRegionChangingHandlerKey: EnvironmentKey {
79 | static let defaultValue: AdvancedMap.RegionChangingHandler? = nil
80 | }
81 |
82 | extension EnvironmentValues {
83 | var mapConfiguration: Configuration? {
84 | get { self[ConfigurationKey.self] }
85 | set { self[ConfigurationKey.self] = newValue }
86 | }
87 |
88 | var edgeInsets: XEdgeInsets {
89 | get { self[EdgeInsetsKey.self] }
90 | set { self[EdgeInsetsKey.self] = newValue }
91 | }
92 | var showsUserLocation: Bool {
93 | get { self[ShowsUserLocationKey.self] }
94 | set { self[ShowsUserLocationKey.self] = newValue }
95 | }
96 |
97 | var isZoomEnabled: Bool {
98 | get { self[IsZoomEnabledKey.self] }
99 | set { self[IsZoomEnabledKey.self] = newValue }
100 | }
101 |
102 | var isScrollEnabled: Bool {
103 | get { self[IsScrollEnabledKey.self] }
104 | set { self[IsScrollEnabledKey.self] = newValue }
105 | }
106 |
107 | var isRotationEnabled: Bool {
108 | get { self[IsRotationEnabledKey.self] }
109 | set { self[IsRotationEnabledKey.self] = newValue }
110 | }
111 |
112 | var isPitchEnabled: Bool {
113 | get { self[IsPitchEnabledKey.self] }
114 | set { self[IsPitchEnabledKey.self] = newValue }
115 | }
116 |
117 | var showsPitchControl: Bool {
118 | get { self[ShowsPitchControlKey.self] }
119 | set { self[ShowsPitchControlKey.self] = newValue }
120 | }
121 |
122 | var showsZoomControls: Bool {
123 | get { self[ShowsZoomControlsKey.self] }
124 | set { self[ShowsZoomControlsKey.self] = newValue }
125 | }
126 |
127 | var showsCompass: Bool {
128 | get { self[ShowsCompassKey.self] }
129 | set { self[ShowsCompassKey.self] = newValue }
130 | }
131 |
132 | var showsScale: Bool {
133 | get { self[ShowsScaleKey.self] }
134 | set { self[ShowsScaleKey.self] = newValue }
135 | }
136 |
137 | var mapAnnotations: [MKAnnotation] {
138 | get { self[MapAnnotationsKey.self] }
139 | set { self[MapAnnotationsKey.self] = newValue }
140 | }
141 |
142 | var annotationViewFactory: AnnotationViewFactory? {
143 | get { self[MapAnnotationViewFactoryKey.self] }
144 | set { self[MapAnnotationViewFactoryKey.self] = newValue }
145 | }
146 |
147 | var mapOverlays: [MKOverlay] {
148 | get { self[MapOverlaysKey.self] }
149 | set { self[MapOverlaysKey.self] = newValue }
150 | }
151 |
152 | var overlayRendererFactory: OverlayRendererFactory? {
153 | get { self[MapOverlayRendererFactoryKey.self] }
154 | set { self[MapOverlayRendererFactoryKey.self] = newValue }
155 | }
156 |
157 | var onTapOrClickGesture: AdvancedMap.DidTapOrClickMapHandler? {
158 | get { self[OnTapOrClickMapGestureKey.self] }
159 | set { self[OnTapOrClickMapGestureKey.self] = newValue }
160 | }
161 |
162 | var onLongPressMapGesture: AdvancedMap.LongPressMapHandler? {
163 | get { self[OnLongPressMapGestureKey.self] }
164 | set { self[OnLongPressMapGestureKey.self] = newValue }
165 | }
166 |
167 | #if os(iOS) || os(macOS)
168 | var onAnnotationDragGesture: AdvancedMap.AnnotationDragHandler? {
169 | get { self[OnAnnotationDragGestureKey.self] }
170 | set { self[OnAnnotationDragGestureKey.self] = newValue }
171 | }
172 | #endif
173 |
174 | var mapRegionChangingHandler: AdvancedMap.RegionChangingHandler? {
175 | get { self[OnMapRegionChangingHandlerKey.self] }
176 | set { self[OnMapRegionChangingHandlerKey.self] = newValue }
177 | }
178 | }
179 |
180 | extension View {
181 | public func mapConfiguration(_ configuration: Configuration) -> some View {
182 | environment(\.mapConfiguration, configuration)
183 | }
184 |
185 | public func mapEdgeInsets(_ edgeInsets: XEdgeInsets) -> some View {
186 | environment(\.edgeInsets, edgeInsets)
187 | }
188 |
189 | public func showsUserLocation(_ showsUserLocation: Bool) -> some View {
190 | environment(\.showsUserLocation, showsUserLocation)
191 | }
192 |
193 | public func isZoomEnabled(_ isZoomEnabled: Bool) -> some View {
194 | environment(\.isZoomEnabled, isZoomEnabled)
195 | }
196 |
197 | public func isScrollEnabled(_ isScrollEnabled: Bool) -> some View {
198 | environment(\.isScrollEnabled, isScrollEnabled)
199 | }
200 |
201 | public func isRotationEnabled(_ isRotationEnabled: Bool) -> some View {
202 | environment(\.isRotationEnabled, isRotationEnabled)
203 | }
204 |
205 | public func isPitchEnabled(_ isPitchEnabled: Bool) -> some View {
206 | environment(\.isPitchEnabled, isPitchEnabled)
207 | }
208 |
209 | #if os(macOS)
210 | public func showsPitchControl(_ showsPitchControl: Bool) -> some View {
211 | environment(\.showsPitchControl, showsPitchControl)
212 | }
213 |
214 | public func showsZoomControls(_ showsZoomControls: Bool) -> some View {
215 | environment(\.showsZoomControls, showsZoomControls)
216 | }
217 | #endif
218 |
219 | public func showsCompass(_ showsCompass: Bool) -> some View {
220 | environment(\.showsCompass, showsCompass)
221 | }
222 |
223 | public func showsScale(_ showsScale: Bool) -> some View {
224 | environment(\.showsScale, showsScale)
225 | }
226 |
227 | public func annotations(
228 | _ annotations: [MKAnnotation],
229 | annotationViewFactory: AnnotationViewFactory
230 | ) -> some View {
231 | environment(\.mapAnnotations, annotations)
232 | .environment(\.annotationViewFactory, annotationViewFactory)
233 | }
234 |
235 | public func overlays(
236 | _ overlays: [MKOverlay],
237 | overlayRendererFactory: OverlayRendererFactory
238 | ) -> some View {
239 | environment(\.mapOverlays, overlays)
240 | .environment(\.overlayRendererFactory, overlayRendererFactory)
241 | }
242 |
243 | #if os(iOS)
244 |
245 | public func onTapMapGesture(
246 | _ tapOrClickHandler: @escaping AdvancedMap.DidTapOrClickMapHandler
247 | ) -> some View {
248 | environment(\.onTapOrClickGesture, tapOrClickHandler)
249 | }
250 |
251 | #elseif os(macOS)
252 |
253 | public func onClickMapGesture(
254 | _ tapOrClickHandler: @escaping AdvancedMap.DidTapOrClickMapHandler
255 | ) -> some View {
256 | environment(\.onTapOrClickGesture, tapOrClickHandler)
257 | }
258 |
259 | #endif
260 |
261 | #if os(iOS) || os(macOS)
262 | public func onTapOrClickMapGesture(
263 | _ tapOrClickHandler: @escaping AdvancedMap.DidTapOrClickMapHandler
264 | ) -> some View {
265 | environment(\.onTapOrClickGesture, tapOrClickHandler)
266 | }
267 | #endif
268 |
269 | public func onLongPressMapGesture(
270 | _ longPressHandler: @escaping AdvancedMap.LongPressMapHandler
271 | ) -> some View {
272 | environment(\.onLongPressMapGesture, longPressHandler)
273 | }
274 |
275 | #if os(iOS) || os(macOS)
276 |
277 | public func annotationDragHandler(
278 | _ annotationDragHandler: @escaping AdvancedMap.AnnotationDragHandler
279 | ) -> some View {
280 | environment(\.onAnnotationDragGesture, annotationDragHandler)
281 | }
282 |
283 | #endif
284 |
285 | public func mapRegionChangingHandler(
286 | _ regionChangingHandler: @escaping AdvancedMap.RegionChangingHandler
287 | ) -> some View {
288 | environment(\.mapRegionChangingHandler, regionChangingHandler)
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/AnnotationViewFactory.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 | /// Stolen from: https://github.com/darrarski/SwiftUIMKMapView
4 |
5 | /// Provides the view associated with the specified annotation object.
6 | public struct AnnotationViewFactory {
7 | /// - Parameters:
8 | /// - register: A closure that registers an annotation view class that the map can create automatically.
9 | /// - view: A closure that returns the view associated with the specified annotation object.
10 | public init(
11 | register: @escaping (MKMapView) -> Void,
12 | view: @escaping (MKMapView, MKAnnotation) -> MKAnnotationView?
13 | ) {
14 | self.register = register
15 | self.view = view
16 | }
17 |
18 | var register: (MKMapView) -> Void
19 | var view: (MKMapView, MKAnnotation) -> MKAnnotationView?
20 |
21 | func register(in mapView: MKMapView) {
22 | register(mapView)
23 | }
24 |
25 | func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
26 | view(mapView, annotation)
27 | }
28 | }
29 |
30 | extension AnnotationViewFactory {
31 | /// An empty factory that does nothing and does not return views.
32 | public static let empty = Self(register: { _ in }, view: { _, _ in nil })
33 |
34 | /// Combines multiple factories into a single one.
35 | ///
36 | /// The combined factory returns first non-`nil` view returned by the provided factories.
37 | ///
38 | /// - Parameter factories: Factories to combine.
39 | /// - Returns: Single, combined factory.
40 | public static func combine(_ factories: [Self]) -> Self {
41 | .init(register: { mapView in
42 | factories.forEach { $0.register(in: mapView) }
43 | }, view: { mapView, annotation in
44 | for factory in factories {
45 | if let view = factory.mapView(mapView, viewFor: annotation) {
46 | return view
47 | }
48 | }
49 | return nil
50 | })
51 | }
52 |
53 | /// Combines multiple factories into one.
54 | ///
55 | /// The combined factory returns first non-`nil` view returned by the provided factories.
56 | ///
57 | /// - Parameter factories: Factories to combine
58 | /// - Returns: Single, combined factory.
59 | public static func combine(_ factories: Self...) -> Self {
60 | .combine(factories)
61 | }
62 |
63 | /// Creates a factory that registers and dequeues views of provided class for provided annotation class.
64 | /// - Parameters:
65 | /// - annotationClass: The annotation class (conforming to MKAnnotation).
66 | /// - viewClass: The view class (subclass of MKAnnotationView).
67 | /// - Returns: Factory.
68 | public static func factory(
69 | for annotationClass: Annotation.Type,
70 | _ viewClass: View.Type
71 | ) -> Self
72 | where Annotation: MKAnnotation,
73 | View: MKAnnotationView
74 | {
75 | let reuseIdentifier = [annotationClass, viewClass]
76 | .map(String.init(describing:))
77 | .joined(separator: "___")
78 |
79 | return .init(register: { mapView in
80 | mapView.register(
81 | viewClass.self,
82 | forAnnotationViewWithReuseIdentifier: reuseIdentifier
83 | )
84 | }, view: { mapView, annotation in
85 | guard let annotation = annotation as? Annotation else { return nil }
86 | let reusableView = mapView.dequeueReusableAnnotationView(
87 | withIdentifier: reuseIdentifier,
88 | for: annotation
89 | )
90 | return reusableView as? View
91 | })
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/Configuration.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 | public enum Configuration: Hashable {
4 | case standard(
5 | _ emphasisStyle: MKStandardMapConfiguration.EmphasisStyle,
6 | _ elevationStyle: MKMapConfiguration.ElevationStyle,
7 | _ pointOfInterestFilter: MKPointOfInterestFilter,
8 | _ showsTraffic: Bool
9 | )
10 | case hybrid(
11 | _ elevationStyle: MKMapConfiguration.ElevationStyle,
12 | _ pointOfInterestFilter: MKPointOfInterestFilter,
13 | _ showsTraffic: Bool
14 | )
15 | case imagery(
16 | _ elevationStyle: MKMapConfiguration.ElevationStyle
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/CrossPlatformBridging.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | #if os(iOS) || os(tvOS)
5 | public typealias XLegacyView = UIView
6 | public typealias XEdgeInsets = UIEdgeInsets
7 | public typealias XViewRepresentable = UIViewRepresentable
8 | public typealias XViewRepresentableContext = UIViewRepresentableContext
9 | public typealias XGestureRecognizer = UIGestureRecognizer
10 | public typealias XTapOrClickGestureRecognizer = UITapGestureRecognizer
11 | public typealias XLongTapOrClickGestureRecognizer = UIGestureRecognizer
12 | #else
13 | public typealias XLegacyView = NSView
14 | public typealias XEdgeInsets = NSEdgeInsets
15 | public typealias XViewRepresentable = NSViewRepresentable
16 | public typealias XViewRepresentableContext = NSViewRepresentableContext
17 | public typealias XGestureRecognizer = NSGestureRecognizer
18 | public typealias XTapOrClickGestureRecognizer = NSClickGestureRecognizer
19 | public typealias XLongTapOrClickGestureRecognizer = NSPressGestureRecognizer
20 | #endif
21 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/Logger.swift:
--------------------------------------------------------------------------------
1 | import OSLog
2 |
3 | let logger = Logger(
4 | subsystem: "com.msena.SwiftUIAdvancedMap",
5 | category: "AdvancedMap"
6 | )
7 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/MKUserLocation+AnnotationViewFactory.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 | extension MKUserLocation {
4 |
5 | public override func isEqual(_ object: Any?) -> Bool {
6 | guard let other = object as? Self else { return false }
7 | #if os(iOS) || os(macOS)
8 | return isUpdating == other.isUpdating &&
9 | location == other.location &&
10 | heading == other.heading &&
11 | title == other.title &&
12 | subtitle == other.subtitle
13 | #else
14 | return isUpdating == other.isUpdating &&
15 | location == other.location &&
16 | title == other.title &&
17 | subtitle == other.subtitle
18 | #endif
19 | }
20 |
21 | public static let mkUserLocationViewFactory = AnnotationViewFactory.factory(for: MKUserLocation.self, MKUserLocationView.self)
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/MapKit+Equatable.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 | extension MKMapRect: Equatable {
4 | public static func == (lhs: MKMapRect, rhs: MKMapRect) -> Bool {
5 | return lhs.origin == rhs.origin && lhs.size == rhs.size
6 | }
7 | }
8 |
9 | extension MKMapPoint: Equatable {
10 | public static func == (lhs: MKMapPoint, rhs: MKMapPoint) -> Bool {
11 | return (lhs.x ==~ rhs.x) && (lhs.y ==~ rhs.y)
12 | }
13 | }
14 |
15 | extension MKMapSize: Equatable {
16 | public static func == (lhs: MKMapSize, rhs: MKMapSize) -> Bool {
17 | return lhs.width ==~ rhs.width
18 | }
19 | }
20 |
21 | extension CLLocationCoordinate2D: Equatable {
22 | public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
23 | return (lhs.latitude ==~ rhs.latitude) && (lhs.longitude ==~ rhs.longitude)
24 | }
25 | }
26 |
27 | extension MKCoordinateRegion: Equatable {
28 | public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool {
29 | lhs.center == rhs.center && lhs.span == rhs.span
30 | }
31 | }
32 |
33 | extension MKCoordinateSpan: Equatable {
34 | public static func == (lhs: MKCoordinateSpan, rhs: MKCoordinateSpan) -> Bool {
35 | lhs.latitudeDelta == rhs.latitudeDelta && rhs.longitudeDelta == rhs.longitudeDelta
36 | }
37 | }
38 |
39 |
40 | extension CLLocationDistance {
41 | static let oneHundredKm = 100_000.0
42 | static let mapEqualityPrecision = 1e-7
43 | }
44 |
45 | infix operator ==~ : AssignmentPrecedence
46 | public func ==~ (left: Double, right: Double) -> Bool
47 | {
48 | return fabs(left.distance(to: right)) <= .mapEqualityPrecision
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/MapViewCoordinator.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 | public class Coordinator: NSObject, MKMapViewDelegate {
4 | var advancedMap: AdvancedMap
5 | var mapView: MKMapView?
6 |
7 | /// I'm using this to avoid updating the map while an animation is in progress. There is
8 | /// probably a better way to defer updates, perhaps the binding is a bad idea and the
9 | /// client should just express what they would like to be visible and we handle the rest?
10 | var isChangingRegion = false
11 |
12 | var isFirstRender = true
13 |
14 | var tapOrClickGesture: XTapOrClickGestureRecognizer?
15 | var longPressGesture: XLongTapOrClickGestureRecognizer?
16 |
17 | init(advancedMap: AdvancedMap) {
18 | self.advancedMap = advancedMap
19 |
20 | super.init()
21 | }
22 |
23 | private func updateVisibleBinding() {
24 | guard let mapView else { return }
25 | switch advancedMap.mapVisibility {
26 | case .none:
27 | // If clients initially provide `nil` for `mapVisibility` this will write a
28 | // `MapVisibility.centerCoordinate` to the binding.
29 | advancedMap.mapVisibility = .centerCoordinate(mapView.centerCoordinate)
30 | case .region:
31 | advancedMap.mapVisibility = .region(mapView.region)
32 | case .centerCoordinate:
33 | advancedMap.mapVisibility = .centerCoordinate(mapView.centerCoordinate)
34 | case .visibleMapRect:
35 | advancedMap.mapVisibility = .visibleMapRect(mapView.visibleMapRect)
36 | case .annotations:
37 | // We don't write back to the binding
38 | break
39 | case .camera:
40 | advancedMap.mapVisibility = .camera(mapView.camera)
41 | }
42 | }
43 |
44 | // MARK: MKMapViewDelegate
45 |
46 | public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
47 | DispatchQueue.main.async { [weak self] in
48 | guard let self = self else { return }
49 | self.updateVisibleBinding()
50 | }
51 | }
52 |
53 | public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
54 | isChangingRegion = true
55 | advancedMap.regionChangingHandler?(true, animated)
56 | }
57 |
58 | public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
59 | isChangingRegion = false
60 | advancedMap.regionChangingHandler?(false, animated)
61 |
62 | if isFirstRender {
63 | DispatchQueue.main.async { [weak self] in
64 | guard let self else { return }
65 | self.updateVisibleBinding()
66 | }
67 | }
68 | isFirstRender = false
69 | }
70 |
71 | #if os(iOS) || os(macOS)
72 | public func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
73 | DispatchQueue.main.async {
74 | self.advancedMap.userTrackingMode = mode
75 | }
76 | }
77 | #endif
78 |
79 | public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
80 | advancedMap.annotationViewFactory?.mapView(mapView, viewFor: annotation)
81 | }
82 |
83 | public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
84 | advancedMap.overlayRendererFactory?.rendererFor(overlay) ?? .init()
85 | }
86 |
87 | #if os(iOS) || os(macOS)
88 | public func mapView(
89 | _ mapView: MKMapView,
90 | annotationView view: MKAnnotationView,
91 | didChange newState: MKAnnotationView.DragState,
92 | fromOldState oldState: MKAnnotationView.DragState
93 | ) {
94 | guard let annotation = view.annotation else { return }
95 | advancedMap.annotationDragHandler?(annotation, annotation.coordinate, oldState, newState)
96 | }
97 | #endif
98 |
99 | // MARK: - Gesture Recognizer
100 |
101 | func addTapOrClickGestureRecognizer(mapView: MKMapView) {
102 | #if os(macOS)
103 | let clickGesture = NSClickGestureRecognizer(
104 | target: self,
105 | action: #selector(Coordinator.didClickOnMap(gesture:))
106 | )
107 | clickGesture.delegate = self
108 | mapView.addGestureRecognizer(clickGesture)
109 | tapOrClickGesture = clickGesture
110 | #else
111 | let tapGesture = UITapGestureRecognizer(
112 | target: self,
113 | action: #selector(Coordinator.didTapOnMap(gesture:))
114 | )
115 | tapGesture.delegate = self
116 | mapView.addGestureRecognizer(tapGesture)
117 | tapOrClickGesture = tapGesture
118 | #endif
119 | }
120 |
121 | func addLongTapOrClickGestureRecognizer(mapView: MKMapView) {
122 | #if os(macOS)
123 | let longPress = NSPressGestureRecognizer(
124 | target: self,
125 | action: #selector(Coordinator.didLongPressOnMap(gesture:))
126 | )
127 | mapView.addGestureRecognizer(longPress)
128 | longPressGesture = longPress
129 | #else
130 | let longTap = UILongPressGestureRecognizer(
131 | target: self,
132 | action: #selector(Coordinator.didLongPressOnMap(gesture:))
133 | )
134 | longTap.delegate = self
135 | mapView.addGestureRecognizer(longTap)
136 | longPressGesture = longTap
137 | #endif
138 | }
139 | }
140 |
141 | #if os(macOS)
142 | extension Coordinator: NSGestureRecognizerDelegate {
143 | @objc func didClickOnMap(gesture: NSClickGestureRecognizer) {
144 | didTapOrClickOnMap(gesture: gesture)
145 | }
146 |
147 | @objc func didLongPressOnMap(gesture: NSPressGestureRecognizer) {
148 | didLongPress(gesture: gesture)
149 | }
150 |
151 | @objc public func gestureRecognizerShouldBegin(_ gestureRecognizer: NSGestureRecognizer) -> Bool {
152 | shouldGestureRecognizerBegin(gesture: gestureRecognizer)
153 | }
154 | }
155 | #else
156 | extension Coordinator: UIGestureRecognizerDelegate {
157 | @objc func didTapOnMap(gesture: UITapGestureRecognizer) {
158 | didTapOrClickOnMap(gesture: gesture)
159 | }
160 |
161 | @objc func didLongPressOnMap(gesture: UILongPressGestureRecognizer) {
162 | didLongPress(gesture: gesture)
163 | }
164 |
165 | @objc public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
166 | shouldGestureRecognizerBegin(gesture: gestureRecognizer)
167 | }
168 |
169 | @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
170 | // Without this a long tap/click gesture will only work on every other attempt.
171 | if gestureRecognizer is UILongPressGestureRecognizer {
172 | return true
173 | }
174 |
175 | return true
176 | }
177 | }
178 | #endif
179 |
180 | extension Coordinator {
181 | func didTapOrClickOnMap(gesture: XTapOrClickGestureRecognizer) {
182 | guard gesture.state == .ended else { return }
183 | guard let mapView = mapView else {
184 | fatalError("Missing mapView")
185 | }
186 | let coordinate = mapView.convert(gesture.location(in: mapView),
187 | toCoordinateFrom: mapView)
188 | logger.info("didTapOrClickOnMap, calling tapOrClickHandler")
189 | advancedMap.tapOrClickHandler?(coordinate)
190 | }
191 |
192 | func didLongPress(gesture: XLongTapOrClickGestureRecognizer) {
193 | print("long gesture state: \(String(describing: gesture.state))")
194 | guard gesture.state == .began else { return }
195 | guard let mapView = mapView else {
196 | fatalError("Missing mapView")
197 | }
198 | let coordinate = mapView.convert(gesture.location(in: mapView),
199 | toCoordinateFrom: mapView)
200 | logger.info("didLongPress, calling longPressHandler")
201 | advancedMap.longPressHandler?(coordinate)
202 | }
203 |
204 | func shouldGestureRecognizerBegin(gesture: XGestureRecognizer) -> Bool {
205 | logger.debug("""
206 | shouldGestureRecognizerBegin,
207 | isOurTap, \(gesture == self.tapOrClickGesture, privacy: .public),
208 | isOurLongPress, \(gesture == self.longPressGesture, privacy: .public)
209 | """)
210 |
211 | guard let mapView = mapView else { return false }
212 | for view in mapView.allDescendantSubViews {
213 | let location = gesture.location(in: view)
214 | let isInBounds = view.bounds.contains(location)
215 | guard isInBounds else { continue }
216 | logger.debug("gesture started on subview: \(view)")
217 | if view is MKAnnotationView, isInBounds {
218 | logger.debug("\(type(of: gesture)) shouldBegin on \(type(of: view)): \(!isInBounds)")
219 | return !isInBounds
220 | }
221 | #if os(macOS)
222 | if (view is MKZoomControl || view is MKPitchControl || view is MKCompassButton), isInBounds {
223 | logger.debug("\(gesture.className) shouldBegin on \(type(of: view)): \(!isInBounds)")
224 | return !isInBounds
225 | }
226 | #endif
227 | }
228 | logger.debug("\(type(of: gesture)) shouldBegin: true")
229 | mapView.deselectAnnotation(mapView.selectedAnnotations.first, animated: true)
230 | return true
231 | }
232 | }
233 |
234 |
235 | extension XLegacyView {
236 | var allDescendantSubViews: [XLegacyView] {
237 | var allSubviews = subviews
238 | allSubviews.forEach { allSubviews.append(contentsOf: $0.allDescendantSubViews) }
239 | return allSubviews
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/OverlayRendererFactory.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 | /// Stolen from: https://github.com/darrarski/SwiftUIMKMapView
4 |
5 | /// Returns renderer object to use when drawing the specified overlay.
6 | public struct OverlayRendererFactory {
7 | /// - Parameter renderer: A closure that returns optional render for provided overlay.
8 | public init(renderer: @escaping (MKOverlay) -> MKOverlayRenderer?) {
9 | self.renderer = renderer
10 | }
11 |
12 | var renderer: (MKOverlay) -> MKOverlayRenderer?
13 |
14 | func rendererFor(_ overlay: MKOverlay) -> MKOverlayRenderer? {
15 | renderer(overlay)
16 | }
17 | }
18 |
19 | extension OverlayRendererFactory {
20 | /// An empty factory that does not return renderer.
21 | public static let empty = Self(renderer: { _ in nil })
22 |
23 | /// Combines multiple factories into a single one.
24 | ///
25 | /// The combined factory returns first non-`nil` renderer returned by the provided factories.
26 | ///
27 | /// - Parameter factories: Factories to combine.
28 | /// - Returns: Single, combined factory.
29 | public static func combine(_ factories: [Self]) -> Self {
30 | .init { overlay in
31 | for factory in factories {
32 | if let renderer = factory.rendererFor(overlay) {
33 | return renderer
34 | }
35 | }
36 | return nil
37 | }
38 | }
39 |
40 | /// Combines multiple factories into a single one.
41 | ///
42 | /// The combined factory returns first non-`nil` renderer returned by the provided factories.
43 | ///
44 | /// - Parameter factories: Factories to combine.
45 | /// - Returns: Single, combined factory.
46 | public static func combine(_ factories: Self...) -> Self {
47 | .combine(factories)
48 | }
49 |
50 | /// Creates a factory that uses provided closure to create renderer for overlay of the provided class.
51 | /// - Parameters:
52 | /// - overlayClass: A class of the overlay.
53 | /// - renderer: A closure that returns renderer for the provided overlay.
54 | /// - Returns: Factory.
55 | public static func factory(
56 | for overlayClass: Overlay.Type,
57 | _ renderer: @escaping (Overlay) -> MKOverlayRenderer
58 | ) -> Self
59 | where Overlay: MKOverlay
60 | {
61 | return .init { overlay in
62 | guard let overlay = overlay as? Overlay else { return nil }
63 | return renderer(overlay)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/SwiftUIView.swift:
--------------------------------------------------------------------------------
1 | import MapKit
2 |
3 |
--------------------------------------------------------------------------------
/Sources/AdvancedMap/XViewRepresentableContext+Extras.swift:
--------------------------------------------------------------------------------
1 |
2 | extension XViewRepresentableContext {
3 | var shouldAnimateChanges: Bool {
4 | return !transaction.disablesAnimations && transaction.animation != nil
5 | }
6 | }
7 |
--------------------------------------------------------------------------------