diff --git a/.github/navigation.png b/.github/navigation.png new file mode 100644 index 00000000..c85f7efe Binary files /dev/null and b/.github/navigation.png differ diff --git a/.gitignore b/.gitignore index 72dde1df..dc1234b8 100644 --- a/.gitignore +++ b/.gitignore @@ -129,7 +129,7 @@ iOSInjectionProject/ Packages xcuserdata *.xcodeproj - +!Example/Example.xcodeproj ### SwiftPM ### diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme index 8a565c94..8e6dd872 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme @@ -1,6 +1,6 @@ . - - BREAKING: Removed `MLNStyle` extensions referencing non-functioning MapBox styles, e.g. `MLNStyle.navigationGuidanceDayStyleURL`. - - Added `Day/NightStyle(styleURL:)` which takes an explicit URL to a hosting tileserver style. - - Added `Day/NightStyle(demoStyle: ())` as an explicit alternative when the user doesn't have a tileserver handy. This uses MapLibre's demo style and is intended for testing and demonstration use only. - - Deprecated `DayStyle()`/`NightStyle()` initializers because they were backed by an implicit tile service. If these default styles *are* still used, they'll now use the MapLibre demo style. - - `NavigationViewController` now expects explicit style URLs with `NavigationViewController(route:dayStyleURL:nightStyleURL:...)` or NavigationViewController(route:dayStyle:nightStyle:...)` and the existing initializer, which allowed "default" styles, is deprecated and uses the MapLibre demo styles. - - Fix: NavigationViewController was not re-routing when the user went off route. - - Merged in - - Fix: NavigationViewController displayed incorrect `speedMultiplier` when using SimulatedLocationManager - - Merged in +* The `speak` method in `RouteVoiceController` can be used without a given `RouteProgress` or the `RouteProgress` can explicitly ignored so that it will not be added to the voice instruction. +* `RouteProgress` is now optional in `willSpeak` method of `VoiceControllerDelegate` if the `RouteProgress` in the `speak` method of the `RouteVoiceController is `nil`. +* Uses the `Locale` given in `RouteOptions` to create the corresponding `AVSpeechSynthesisVoice`. +* Removed setCamera() from updateCourseTracking() +* Added setCamera() to progressDidChange() +* Allow to customize rerouting logic by implementing RouteControllerDelegate#routeControllerGetDirections +* Add option to overwrite camera update via NavigationMapViewCourseTrackingDelegate#updateCamera +* Remove MapboxVoiceController and Mapbox Speech dependency. If you would like to use MapboxSpeech, you can copy the deleted MapboxVoiceController into your project. +* Updated MapLibre Native dependency to ios-v6.0.0 (https://github.com/maplibre/maplibre-native/releases/tag/ios-v6.0.0). Implementers need to change the prefix MGL to MLN for all MapLibre Native classes that are referenced. +* Only snap location to route if the location is within the `RouteControllerUserLocationSnappingDistance` +* Add support for Swift Package Manager while dropping Carthage and Cocoapods. +* Initialization no longer tries to add mapbox://mapbox.mapbox-streets-v7 to all mapstyles. +* Removed implicit default dependencies on MapBox tileservers by requiring explicit styles URLs in more places. + - Merged in . + - BREAKING: Removed `MLNStyle` extensions referencing non-functioning MapBox styles, e.g. `MLNStyle.navigationGuidanceDayStyleURL`. + - Added `Day/NightStyle(styleURL:)` which takes an explicit URL to a hosting tileserver style. + - Added `Day/NightStyle(demoStyle: ())` as an explicit alternative when the user doesn't have a tileserver handy. This uses MapLibre's demo style and is intended for testing and demonstration use only. + - Deprecated `DayStyle()`/`NightStyle()` initializers because they were backed by an implicit tile service. If these default styles *are* still used, they'll now use the MapLibre demo style. + - `NavigationViewController` now expects explicit style URLs with `NavigationViewController(route:dayStyleURL:nightStyleURL:...)` or NavigationViewController(route:dayStyle:nightStyle:...)` and the existing initializer, which allowed "default" styles, is deprecated and uses the MapLibre demo styles. +* Fix: NavigationViewController was not re-routing when the user went off route. + - Merged in +* Fix: NavigationViewController displayed incorrect `speedMultiplier` when using SimulatedLocationManager + - Merged in ## v2.0.0 (May 23, 2023) - - Upgrade minimum iOS version from 11.0 to 12.0. - - Upgraded dependencies to support iOS 12.0 +* Upgrade minimum iOS version from 11.0 to 12.0. +* Upgraded dependencies to support iOS 12.0 ## v1.0.7 (November 1, 2022) -- Rerouting logic changed +* Rerouting logic changed - Routes that are found when requesting a new route that are slower but more than 90% the same geometry will get applied. This is done to account for traffic on the route that could change the ETA dramatically - Added `rerouteReason` parameter in `didRerouteAlong` `RouteControllerDelegate` so the client can react accordingly - Added option to skip check for rerouting in `RouteController` where route should have 10+mins left before fetching a new route, called `shouldCheckForRerouteInLastMinutes` -- Moved & renamed `RouteControllerProactiveReroutingInterval` to be an instance property of `RouteController` so the client can easily set this per route controller -- Added `shouldReturnTestingETAUpdateReroutes` property to `RouteController` as an easy way to test an ETA update client-side. It uses two of the same test-routes between `52.02224357,5.78149084` and `52.03924958,5.55054131` with different ETA's to easily see the ETA change happen in the client's UI +* Moved & renamed `RouteControllerProactiveReroutingInterval` to be an instance property of `RouteController` so the client can easily set this per route controller +* Added `shouldReturnTestingETAUpdateReroutes` property to `RouteController` as an easy way to test an ETA update client-side. It uses two of the same test-routes between `52.02224357,5.78149084` and `52.03924958,5.55054131` with different ETA's to easily see the ETA change happen in the client's UI ## v1.0.6 (October 5, 2022) diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c4727dd1 --- /dev/null +++ b/Example/Example.xcodeproj/project.pbxproj @@ -0,0 +1,379 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4A2BF6156100501F93 /* AppDelegate.swift */; }; + CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4C2BF6156100501F93 /* SceneDelegate.swift */; }; + CD958B542BF6156200501F93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD958B532BF6156200501F93 /* Assets.xcassets */; }; + CD958B572BF6156200501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B562BF6156200501F93 /* Base */; }; + CD958B682BF63B3500501F93 /* MapboxNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CD958B672BF63B3500501F93 /* MapboxNavigation */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + CD958B472BF6156100501F93 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CD958B4A2BF6156100501F93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + CD958B4C2BF6156100501F93 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + CD958B532BF6156200501F93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CD958B562BF6156200501F93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + CD958B582BF6156200501F93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CD958B652BF63A6700501F93 /* maplibre-navigation-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "maplibre-navigation-ios"; path = ..; sourceTree = ""; }; + CD958B692BF651F400501F93 /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CD958B442BF6156100501F93 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CD958B682BF63B3500501F93 /* MapboxNavigation in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CD958B3E2BF6156100501F93 = { + isa = PBXGroup; + children = ( + CD958B652BF63A6700501F93 /* maplibre-navigation-ios */, + CD958B492BF6156100501F93 /* example */, + CD958B482BF6156100501F93 /* Products */, + CD958B662BF63B3500501F93 /* Frameworks */, + ); + sourceTree = ""; + usesTabs = 0; + }; + CD958B482BF6156100501F93 /* Products */ = { + isa = PBXGroup; + children = ( + CD958B472BF6156100501F93 /* Example.app */, + ); + name = Products; + sourceTree = ""; + }; + CD958B492BF6156100501F93 /* example */ = { + isa = PBXGroup; + children = ( + CD958B4A2BF6156100501F93 /* AppDelegate.swift */, + CD958B4C2BF6156100501F93 /* SceneDelegate.swift */, + CD958B692BF651F400501F93 /* Secrets.xcconfig */, + CD958B532BF6156200501F93 /* Assets.xcassets */, + CD958B552BF6156200501F93 /* LaunchScreen.storyboard */, + CD958B582BF6156200501F93 /* Info.plist */, + ); + path = example; + sourceTree = ""; + }; + CD958B662BF63B3500501F93 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CD958B462BF6156100501F93 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = CD958B5B2BF6156200501F93 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + CD958B432BF6156100501F93 /* Sources */, + CD958B442BF6156100501F93 /* Frameworks */, + CD958B452BF6156100501F93 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + packageProductDependencies = ( + CD958B672BF63B3500501F93 /* MapboxNavigation */, + ); + productName = "navigation sample"; + productReference = CD958B472BF6156100501F93 /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CD958B3F2BF6156100501F93 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + CD958B462BF6156100501F93 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = CD958B422BF6156100501F93 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CD958B3E2BF6156100501F93; + packageReferences = ( + ); + productRefGroup = CD958B482BF6156100501F93 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CD958B462BF6156100501F93 /* Example */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CD958B452BF6156100501F93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD958B542BF6156200501F93 /* Assets.xcassets in Resources */, + CD958B572BF6156200501F93 /* Base in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CD958B432BF6156100501F93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */, + CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + CD958B552BF6156200501F93 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + CD958B562BF6156200501F93 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + CD958B592BF6156200501F93 /* 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 = 17.4; + 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; + }; + CD958B5A2BF6156200501F93 /* 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 = 17.4; + 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; + }; + CD958B5C2BF6156200501F93 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CD958B692BF651F400501F93 /* Secrets.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = example/Info.plist; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "for live navigation"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + 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 = com.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CD958B5D2BF6156200501F93 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CD958B692BF651F400501F93 /* Secrets.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = example/Info.plist; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "for live navigation"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + 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 = com.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CD958B422BF6156100501F93 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD958B592BF6156200501F93 /* Debug */, + CD958B5A2BF6156200501F93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CD958B5B2BF6156200501F93 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD958B5C2BF6156200501F93 /* Debug */, + CD958B5D2BF6156200501F93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + CD958B672BF63B3500501F93 /* MapboxNavigation */ = { + isa = XCSwiftPackageProductDependency; + productName = MapboxNavigation; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = CD958B3F2BF6156100501F93 /* Project object */; +} diff --git a/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..5635fc2c --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "mapbox-directions-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/flitsmeister/mapbox-directions-swift", + "state" : { + "revision" : "6c19ecc4e1324887ae3250802b8d13d8d8b3ff2d", + "version" : "0.23.3" + } + }, + { + "identity" : "maplibre-gl-native-distribution", + "kind" : "remoteSourceControl", + "location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git", + "state" : { + "revision" : "6579f48fe126ce2916f7b5d0c84c1869d790c4e4", + "version" : "6.4.1" + } + }, + { + "identity" : "polyline", + "kind" : "remoteSourceControl", + "location" : "https://github.com/raphaelmor/Polyline", + "state" : { + "revision" : "353f80378dcd8f17eefe8550090c6b1ae3c9da23", + "version" : "5.1.0" + } + }, + { + "identity" : "solar", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ceeK/Solar.git", + "state" : { + "revision" : "c2b96f2d5fb7f835b91cefac5e83101f54643901", + "version" : "3.0.1" + } + }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat.git", + "state" : { + "revision" : "05cb325003a673b3d177c711b3bc909cfee07622", + "version" : "0.53.9" + } + }, + { + "identity" : "turf-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/flitsmeister/turf-swift", + "state" : { + "revision" : "b05b4658d1b48eac4127a0d9ebbb5a6f965a8251", + "version" : "0.2.2" + } + } + ], + "version" : 2 +} diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme new file mode 100644 index 00000000..73a42459 --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/example/AppDelegate.swift b/Example/example/AppDelegate.swift new file mode 100644 index 00000000..4362a569 --- /dev/null +++ b/Example/example/AppDelegate.swift @@ -0,0 +1,30 @@ +// +// AppDelegate.swift +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/Example/example/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/example/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Example/example/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/example/Assets.xcassets/Contents.json b/Example/example/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Example/example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/example/Base.lproj/LaunchScreen.storyboard b/Example/example/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Example/example/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/example/Info.plist b/Example/example/Info.plist new file mode 100644 index 00000000..2fead199 --- /dev/null +++ b/Example/example/Info.plist @@ -0,0 +1,29 @@ + + + + + MGLMapboxAccessToken + ${MAPBOX_TOKEN} + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIBackgroundModes + + audio + + + diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift new file mode 100644 index 00000000..39603c5f --- /dev/null +++ b/Example/example/SceneDelegate.swift @@ -0,0 +1,122 @@ +// +// SceneDelegate.swift +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +import MapboxCoreNavigation +import MapboxDirections +import MapboxNavigation +import MapLibre +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + var viewController: NavigationViewController! + var route: Route! + + let startButton = UIButton() + let waypoints = [ + CLLocation(latitude: 52.032407, longitude: 5.580310), + CLLocation(latitude: 52.04, longitude: 5.580310), + CLLocation(latitude: 51.768686, longitude: 4.6827956) + ].map { Waypoint(location: $0) } + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + self.window = UIWindow(windowScene: windowScene) + + // NOTE: You will need your own tile server, MapLibre doesn't provide the server infrastructure + // so this uses a demo style that only shows country borders + // this is not useful to evaluate the navigation, please change accordingly + self.viewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) + self.viewController.mapView.tracksUserCourse = false + self.viewController.mapView.showsUserLocation = true + self.viewController.mapView.centerCoordinate = self.waypoints[0].coordinate + self.viewController.delegate = self + + self.window?.rootViewController = self.viewController + self.window?.makeKeyAndVisible() + + self.viewController.mapView.zoomLevel = 5 + + let positionCameraRandomlyButton = UIButton() + positionCameraRandomlyButton.translatesAutoresizingMaskIntoConstraints = false + positionCameraRandomlyButton.setImage(UIImage(systemName: "globe"), for: .normal) + positionCameraRandomlyButton.addTarget(self, action: #selector(cameraButtonTapped), for: .touchUpInside) + positionCameraRandomlyButton.backgroundColor = .white + positionCameraRandomlyButton.layer.cornerRadius = 8 + self.viewController.view.addSubview(positionCameraRandomlyButton) + NSLayoutConstraint.activate([ + positionCameraRandomlyButton.trailingAnchor.constraint(equalTo: self.viewController.view.layoutMarginsGuide.trailingAnchor), + positionCameraRandomlyButton.centerYAnchor.constraint(equalTo: self.viewController.view.centerYAnchor), + positionCameraRandomlyButton.widthAnchor.constraint(equalTo: positionCameraRandomlyButton.heightAnchor), + positionCameraRandomlyButton.widthAnchor.constraint(equalToConstant: 44) + ]) + + self.startButton.translatesAutoresizingMaskIntoConstraints = false + self.startButton.setImage(UIImage(systemName: "play.fill"), for: .normal) + self.startButton.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside) + self.startButton.backgroundColor = .white + self.startButton.layer.cornerRadius = 8 + self.viewController.view.addSubview(self.startButton) + NSLayoutConstraint.activate([ + self.startButton.trailingAnchor.constraint(equalTo: self.viewController.view.layoutMarginsGuide.trailingAnchor), + positionCameraRandomlyButton.bottomAnchor.constraint(equalTo: self.startButton.topAnchor, constant: -12), + self.startButton.widthAnchor.constraint(equalTo: self.startButton.heightAnchor), + self.startButton.widthAnchor.constraint(equalToConstant: 44) + ]) + } +} + +extension SceneDelegate: NavigationViewControllerDelegate { + func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) { + navigationViewController.endNavigation() + } +} + +// MARK: - Private + +private extension SceneDelegate { + func startNavigation(for waypoints: [Waypoint]) { + let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) + options.shapeFormat = .polyline6 + options.distanceMeasurementSystem = .metric + options.attributeOptions = [] + + Directions.shared.calculate(options) { _, routes, _ in + guard let route = routes?.first else { return } + + self.route = route + + let simulatedLocationManager = SimulatedLocationManager(route: route) + simulatedLocationManager.speedMultiplier = 2 + + self.viewController.startNavigation(with: route, locationManager: simulatedLocationManager) + } + } + + @objc + func cameraButtonTapped() { + guard let waypoint = self.waypoints.randomElement() else { return } + + let distance = CLLocationDistance.random(in: 10 ... 100_000) + self.viewController.mapView.camera = .init(lookingAtCenter: waypoint.coordinate, + acrossDistance: distance, + pitch: 0, + heading: 0) + } + + @objc + func startButtonTapped() { + if self.viewController.route == nil { + self.startNavigation(for: Array(self.waypoints[0 ... 1])) + self.startButton.setImage(UIImage(systemName: "stop.fill"), for: .normal) + } else { + self.viewController.endNavigation() + self.startButton.setImage(UIImage(systemName: "play.fill"), for: .normal) + } + } +} diff --git a/Example/example/Secrets.xcconfig b/Example/example/Secrets.xcconfig new file mode 100644 index 00000000..f997102f --- /dev/null +++ b/Example/example/Secrets.xcconfig @@ -0,0 +1,11 @@ +// +// Secrets.xcconfig +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +MAPBOX_TOKEN= diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 15b0b1ff..f1c37e1a 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -148,14 +148,14 @@ open class RouteController: NSObject, Router { self.locationManager.delegate = self self.resumeNotifications() - checkForUpdates() - checkForLocationUsageDescription() + self.checkForUpdates() + self.checkForLocationUsageDescription() self.tunnelIntersectionManager.delegate = self } deinit { - endNavigation() + self.endNavigation() guard let shouldDisable = delegate?.routeControllerShouldDisableBatteryMonitoring?(self) else { UIDevice.current.isBatteryMonitoringEnabled = false diff --git a/MapboxNavigation/BottomBannerViewLayout.swift b/MapboxNavigation/BottomBannerViewLayout.swift index 4a25fe58..7ca33ce4 100644 --- a/MapboxNavigation/BottomBannerViewLayout.swift +++ b/MapboxNavigation/BottomBannerViewLayout.swift @@ -5,34 +5,34 @@ extension BottomBannerView { let timeRemainingLabel = TimeRemainingLabel() timeRemainingLabel.translatesAutoresizingMaskIntoConstraints = false timeRemainingLabel.font = .systemFont(ofSize: 28, weight: .medium) - addSubview(timeRemainingLabel) + self.addSubview(timeRemainingLabel) self.timeRemainingLabel = timeRemainingLabel let distanceRemainingLabel = DistanceRemainingLabel() distanceRemainingLabel.translatesAutoresizingMaskIntoConstraints = false distanceRemainingLabel.font = .systemFont(ofSize: 18, weight: .medium) - addSubview(distanceRemainingLabel) + self.addSubview(distanceRemainingLabel) self.distanceRemainingLabel = distanceRemainingLabel let arrivalTimeLabel = ArrivalTimeLabel() arrivalTimeLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(arrivalTimeLabel) + self.addSubview(arrivalTimeLabel) self.arrivalTimeLabel = arrivalTimeLabel let cancelButton = CancelButton(type: .custom) cancelButton.translatesAutoresizingMaskIntoConstraints = false cancelButton.setImage(UIImage(named: "close", in: .mapboxNavigation, compatibleWith: nil), for: .normal) - addSubview(cancelButton) + self.addSubview(cancelButton) self.cancelButton = cancelButton let verticalDivider = SeparatorView() verticalDivider.translatesAutoresizingMaskIntoConstraints = false - addSubview(verticalDivider) + self.addSubview(verticalDivider) verticalDividerView = verticalDivider let horizontalDividerView = SeparatorView() horizontalDividerView.translatesAutoresizingMaskIntoConstraints = false - addSubview(horizontalDividerView) + self.addSubview(horizontalDividerView) self.horizontalDividerView = horizontalDividerView self.setupConstraints() diff --git a/MapboxNavigation/CarPlayManager.swift b/MapboxNavigation/CarPlayManager.swift index e600264f..4e315829 100644 --- a/MapboxNavigation/CarPlayManager.swift +++ b/MapboxNavigation/CarPlayManager.swift @@ -574,7 +574,7 @@ extension CarPlayManager: CPMapTemplateDelegate { topDownCamera.pitch = 0 mapView.setCamera(topDownCamera, animated: false) - let padding = NavigationMapView.defaultPadding + mapView.safeArea + let padding = NavigationMapView.defaultPadding + mapView.safeAreaInsets mapView.showcase([route], padding: padding) } @@ -628,7 +628,7 @@ extension CarPlayManager: CPMapTemplateDelegate { return } - mapView.setContentInset(mapView.safeArea, animated: false, completionHandler: nil) // make sure this is always up to date in-case safe area changes during gesture + mapView.setContentInset(mapView.safeAreaInsets, animated: false, completionHandler: nil) // make sure this is always up to date in-case safe area changes during gesture self.updatePan(by: translation, mapTemplate: mapTemplate, animated: false) } @@ -647,7 +647,7 @@ extension CarPlayManager: CPMapTemplateDelegate { } func coordinate(of offset: CGPoint, in mapView: NavigationMapView) -> CLLocationCoordinate2D { - let contentFrame = mapView.bounds.inset(by: mapView.safeArea) + let contentFrame = mapView.bounds.inset(by: mapView.safeAreaInsets) let centerPoint = CGPoint(x: contentFrame.midX, y: contentFrame.midY) let endCameraPoint = CGPoint(x: centerPoint.x - offset.x, y: centerPoint.y - offset.y) @@ -661,7 +661,7 @@ extension CarPlayManager: CPMapTemplateDelegate { // Determine the screen distance to pan by based on the distance from the visual center to the closest side. let mapView = carPlayMapViewController.mapView - let contentFrame = mapView.bounds.inset(by: mapView.safeArea) + let contentFrame = mapView.bounds.inset(by: mapView.safeAreaInsets) let increment = min(mapView.bounds.width, mapView.bounds.height) / 2.0 // Calculate the distance in physical units from the visual center to where it would be after panning downwards. diff --git a/MapboxNavigation/CarPlayMapViewController.swift b/MapboxNavigation/CarPlayMapViewController.swift index 4de78ece..0feff433 100644 --- a/MapboxNavigation/CarPlayMapViewController.swift +++ b/MapboxNavigation/CarPlayMapViewController.swift @@ -98,7 +98,7 @@ class CarPlayMapViewController: UIViewController, MLNMapViewDelegate { } override func viewSafeAreaInsetsDidChange() { - self.mapView.setContentInset(self.mapView.safeArea, animated: false, completionHandler: nil) + self.mapView.setContentInset(self.mapView.safeAreaInsets, animated: false, completionHandler: nil) guard self.isOverviewingRoutes else { super.viewSafeAreaInsetsDidChange() diff --git a/MapboxNavigation/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index b7789fef..e0207014 100644 --- a/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/MapboxNavigation/CarPlayNavigationViewController.swift @@ -246,7 +246,7 @@ public class CarPlayNavigationViewController: UIViewController, MLNMapViewDelega // Estimating the width of Apple's maneuver view let bounds: () -> (CGRect) = { - let widthOfManeuverView = min(self.view.bounds.width - self.view.safeArea.left, self.view.bounds.width - self.view.safeArea.right) + let widthOfManeuverView = min(self.view.bounds.width - self.view.safeAreaInsets.left, self.view.bounds.width - self.view.safeAreaInsets.right) return CGRect(x: 0, y: 0, width: widthOfManeuverView, height: 30) } diff --git a/MapboxNavigation/EndOfRouteViewController.swift b/MapboxNavigation/EndOfRouteViewController.swift index 31b026dc..92ba0a0a 100644 --- a/MapboxNavigation/EndOfRouteViewController.swift +++ b/MapboxNavigation/EndOfRouteViewController.swift @@ -151,7 +151,7 @@ class EndOfRouteViewController: UIViewController { private func height(for height: ContainerHeight) -> CGFloat { let window = UIApplication.shared.keyWindow - let bottomMargin = window!.safeArea.bottom + let bottomMargin = window!.safeAreaInsets.bottom return height.rawValue + bottomMargin } diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index b1ac0bc5..11355902 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -132,7 +132,7 @@ open class NavigationMapView: MLNMapView, UIGestureRecognizerDelegate { return anchorPoint } - let contentFrame = bounds.inset(by: safeArea) + let contentFrame = bounds.inset(by: safeAreaInsets) let courseViewWidth = self.userCourseView?.frame.width ?? 0 let courseViewHeight = self.userCourseView?.frame.height ?? 0 let edgePadding = UIEdgeInsets(top: 50 + courseViewHeight / 2, diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index ca47ecc4..8453c117 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -2,6 +2,10 @@ import MapboxDirections import MapLibre import UIKit +protocol NavigationViewDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, StatusViewDelegate, InstructionsBannerViewDelegate, NavigationMapViewCourseTrackingDelegate, VisualInstructionDelegate { + func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) +} + /** A view that represents the root view of the MapboxNavigation drop-in UI. @@ -46,7 +50,7 @@ open class NavigationView: UIView { } lazy var bannerShowConstraints: [NSLayoutConstraint] = [ - self.instructionsBannerView.topAnchor.constraint(equalTo: self.safeTopAnchor), + self.instructionsBannerView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), self.instructionsBannerContentView.topAnchor.constraint(equalTo: self.topAnchor) ] @@ -55,12 +59,6 @@ open class NavigationView: UIView { self.instructionsBannerContentView.topAnchor.constraint(equalTo: self.instructionsBannerView.topAnchor) ] - lazy var endOfRouteShowConstraint: NSLayoutConstraint? = self.endOfRouteView?.bottomAnchor.constraint(equalTo: self.safeBottomAnchor) - - lazy var endOfRouteHideConstraint: NSLayoutConstraint? = self.endOfRouteView?.topAnchor.constraint(equalTo: self.bottomAnchor) - - lazy var endOfRouteHeightConstraint: NSLayoutConstraint? = self.endOfRouteView?.heightAnchor.constraint(equalToConstant: Constants.endOfRouteHeight) - private enum Images { static let overview = UIImage(named: "overview", in: .mapboxNavigation, compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) static let volumeUp = UIImage(named: "volume_up", in: .mapboxNavigation, compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) @@ -100,7 +98,6 @@ open class NavigationView: UIView { lazy var overviewButton = FloatingButton.rounded(image: Images.overview) lazy var muteButton = FloatingButton.rounded(image: Images.volumeUp, selectedImage: Images.volumeOff) - lazy var reportButton = FloatingButton.rounded(image: Images.feedback) lazy var lanesView: LanesView = .forAutoLayout(hidden: true) lazy var nextBannerView: NextBannerView = .forAutoLayout(hidden: true) @@ -133,19 +130,7 @@ open class NavigationView: UIView { } } - var endOfRouteView: UIView? { - didSet { - if let active: [NSLayoutConstraint] = constraints(affecting: oldValue) { - NSLayoutConstraint.deactivate(active) - } - - oldValue?.removeFromSuperview() - if let eor = endOfRouteView { addSubview(eor) } - self.endOfRouteView?.translatesAutoresizingMaskIntoConstraints = false - } - } - - // MARK: - Initializers + // MARK: - Lifecycle convenience init(delegate: NavigationViewDelegate) { self.init(frame: .zero) @@ -163,18 +148,75 @@ open class NavigationView: UIView { self.commonInit() } + // MARK: - NavigationView + + override open func prepareForInterfaceBuilder() { + super.prepareForInterfaceBuilder() + DayStyle(demoStyle: ()).apply() + [self.mapView, self.instructionsBannerView, self.lanesView, self.bottomBannerView, self.nextBannerView].forEach { $0.prepareForInterfaceBuilder() } + self.wayNameView.text = "Street Label" + } + + func showUI(animated: Bool = true) { + let views: [UIView] = [ + self.instructionsBannerContentView, + self.lanesView, + self.bottomBannerContentView, + self.floatingStackView + ] + + NSLayoutConstraint.activate(self.bannerShowConstraints) + NSLayoutConstraint.deactivate(self.bannerHideConstraints) + + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { + views.forEach { $0.alpha = 1 } + } completion: { _ in + views.forEach { $0.isHidden = false } + self.bottomBannerView.traitCollectionDidChange(self.traitCollection) + } + } + + func hideUI(animated: Bool = true) { + let views: [UIView] = [ + self.instructionsBannerContentView, + self.lanesView, + self.bottomBannerContentView, + self.floatingStackView, + self.resumeButton + ] + + NSLayoutConstraint.deactivate(self.bannerShowConstraints) + NSLayoutConstraint.activate(self.bannerHideConstraints) + + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { + views.forEach { $0.alpha = 0 } + } completion: { _ in + views.forEach { $0.isHidden = true } + self.bottomBannerView.traitCollectionDidChange(self.traitCollection) + } + } +} + +// MARK: - Private + +private extension NavigationView { + @objc + func cancelButtonTapped(_ sender: CancelButton) { + self.delegate?.navigationView(self, didTapCancelButton: self.bottomBannerView.cancelButton) + } + func commonInit() { self.setupViews() - setupConstraints() + self.setupConstraints() } func setupStackViews() { self.setupInformationStackView() - self.floatingStackView.addArrangedSubviews([self.overviewButton, self.muteButton, self.reportButton]) + self.floatingStackView.addArrangedSubviews([self.overviewButton, self.muteButton]) } func setupInformationStackView() { - let informationChildren: [UIView] = [instructionsBannerView, lanesView, nextBannerView, statusView] + let informationChildren: [UIView] = [self.instructionsBannerView, self.lanesView, self.nextBannerView, self.statusView] self.informationStackView.addArrangedSubviews(informationChildren) for informationChild in informationChildren { @@ -185,8 +227,8 @@ open class NavigationView: UIView { func setupContainers() { let containers: [(UIView, UIView)] = [ - (instructionsBannerContentView, instructionsBannerView), - (bottomBannerContentView, bottomBannerView) + (self.instructionsBannerContentView, self.instructionsBannerView), + (self.bottomBannerContentView, self.bottomBannerView) ] containers.forEach { $0.addSubview($1) } } @@ -194,32 +236,21 @@ open class NavigationView: UIView { func setupViews() { self.setupStackViews() self.setupContainers() - + let subviews: [UIView] = [ - mapView, - informationStackView, - floatingStackView, - resumeButton, - wayNameView, - bottomBannerContentView, - instructionsBannerContentView + self.mapView, + self.informationStackView, + self.floatingStackView, + self.resumeButton, + self.wayNameView, + self.bottomBannerContentView, + self.instructionsBannerContentView ] subviews.forEach(addSubview(_:)) } - override open func prepareForInterfaceBuilder() { - super.prepareForInterfaceBuilder() - DayStyle(demoStyle: ()).apply() - [self.mapView, self.instructionsBannerView, self.lanesView, self.bottomBannerView, self.nextBannerView].forEach { $0.prepareForInterfaceBuilder() } - self.wayNameView.text = "Street Label" - } - - @objc func cancelButtonTapped(_ sender: CancelButton) { - self.delegate?.navigationView(self, didTapCancelButton: self.bottomBannerView.cancelButton) - } - - private func updateDelegates() { + func updateDelegates() { self.mapView.delegate = self.delegate self.mapView.navigationMapDelegate = self.delegate self.mapView.courseTrackingDelegate = self.delegate @@ -229,7 +260,3 @@ open class NavigationView: UIView { self.statusView.delegate = self.delegate } } - -protocol NavigationViewDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, StatusViewDelegate, InstructionsBannerViewDelegate, NavigationMapViewCourseTrackingDelegate, VisualInstructionDelegate { - func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) -} diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 216640df..a2a035c1 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -12,13 +12,20 @@ import CarPlay @objc(MBNavigationViewControllerDelegate) public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { /** - Called when the navigation view controller is dismissed, such as when the user ends a trip. - - - parameter navigationViewController: The navigation view controller that was dismissed. - - parameter canceled: True if the user dismissed the navigation view controller by tapping the Cancel button; false if the navigation view controller dismissed by some other means. + Called when user arrived at the destination of the trip. + + - parameter navigationViewController: The navigation view controller that finished navigation. */ - @objc optional func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) - + @objc optional func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) + + /** + Called when the underlaying mapView finished loading the style. + + - parameter navigationViewController: The navigation view controller that finished navigation. + - parameter style: The applied style + */ + @objc optional func navigationViewController(_ navigationViewController: NavigationViewController, didFinishLoading style: MLNStyle) + /** Called when the user arrives at the destination waypoint for a route leg. @@ -136,6 +143,13 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { */ @objc(navigationViewController:didSelectRoute:) optional func navigationViewController(_ navigationViewController: NavigationViewController, didSelect route: Route) + + /** + Called when the user taps to select an annotation on the navigation view controller’s map view. + - parameter navigationViewController: The navigation view controller presenting the route that the user selected. + - parameter annotation: The annotation on the map that the user selected. + */ + @objc optional func navigationViewController(_ navigationViewController: NavigationViewController, didSelect annotation: MLNAnnotation) /** Return an `MLNAnnotationImage` that represents the destination marker. @@ -190,61 +204,81 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - seealso: CarPlayNavigationViewController */ +@objcMembers @objc(MBNavigationViewController) open class NavigationViewController: UIViewController { + private var locationManager: NavigationLocationManager + + var mapViewController: RouteMapViewController + var styleManager: StyleManager! + + var currentStatusBarStyle: UIStatusBarStyle = .default { + didSet { + self.mapViewController.instructionsBannerView.backgroundColor = InstructionsBannerView.appearance().backgroundColor + self.mapViewController.instructionsBannerContentView.backgroundColor = InstructionsBannerContentView.appearance().backgroundColor + } + } + + // MARK: - Properties + /** A `Route` object constructed by [MapboxDirections](https://mapbox.github.io/mapbox-navigation-ios/directions/). In cases where you need to update the route after navigation has started you can set a new `route` here and `NavigationViewController` will update its UI accordingly. */ - @objc public var route: Route! { + public var route: Route? { didSet { - if self.routeController == nil { - self.routeController = RouteController(along: self.route, directions: self.directions, locationManager: self.locationManager) - self.routeController.delegate = self + if let route { + if self.routeController == nil { + let routeController = RouteController(along: route, directions: self.directions, locationManager: self.locationManager) + routeController.delegate = self + self.routeController = routeController + } else { + self.routeController?.routeProgress = RouteProgress(route: route) + } + NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile + self.mapViewController.notifyDidReroute(route: route) } else { - self.routeController.routeProgress = RouteProgress(route: self.route) + self.routeController = nil } - NavigationSettings.shared.distanceUnit = self.route.routeOptions.locale.usesMetric ? .kilometer : .mile - self.mapViewController?.notifyDidReroute(route: self.route) } } /** An instance of `Directions` need for rerouting. See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. */ - @objc public var directions: Directions! + public var directions: Directions /** An optional `MLNMapCamera` you can use to improve the initial transition from a previous viewport and prevent a trigger from an excessive significant location update. */ - @objc public var pendingCamera: MLNMapCamera? + public var pendingCamera: MLNMapCamera? /** An instance of `MLNAnnotation` representing the origin of your route. */ - @objc public var origin: MLNAnnotation? + public var origin: MLNAnnotation? /** The receiver’s delegate. */ - @objc public weak var delegate: NavigationViewControllerDelegate? + public weak var delegate: NavigationViewControllerDelegate? /** Provides access to various speech synthesizer options. See `RouteVoiceController` for more information. */ - @objc public var voiceController: RouteVoiceController! + public var voiceController: RouteVoiceController? /** Provides all routing logic for the user. See `RouteController` for more information. */ - @objc public var routeController: RouteController! { + public var routeController: RouteController? { didSet { - self.mapViewController?.routeController = self.routeController + self.mapViewController.routeController = self.routeController } } @@ -253,8 +287,8 @@ open class NavigationViewController: UIViewController { - note: Do not change this map view’s delegate. */ - @objc public var mapView: NavigationMapView? { - self.mapViewController?.mapView + public var mapView: NavigationMapView { + self.mapViewController.mapView } /** @@ -262,318 +296,187 @@ open class NavigationViewController: UIViewController { By default, this property is set to `true`, causing the user location annotation to be snapped to the route. */ - @objc public var snapsUserLocationAnnotationToRoute = true + public var snapsUserLocationAnnotationToRoute = true /** Toggles sending of UILocalNotification upon upcoming steps when application is in the background. Defaults to `true`. */ - @objc public var sendsNotifications: Bool = true - - /** - Shows a button that allows drivers to report feedback such as accidents, closed roads, poor instructions, etc. Defaults to `true`. - */ - @objc public var showsReportFeedback: Bool = true { - didSet { - self.mapViewController?.reportButton.isHidden = !self.showsReportFeedback - self.showsEndOfRouteFeedback = self.showsReportFeedback - } - } - - /** - Shows End of route Feedback UI when the route controller arrives at the final destination. Defaults to `true.` - */ - @objc public var showsEndOfRouteFeedback: Bool = true { - didSet { - self.mapViewController?.showsEndOfRoute = self.showsEndOfRouteFeedback - } - } + public var sendsNotifications: Bool = true /** If true, the map style and UI will automatically be updated given the time of day. */ - @objc public var automaticallyAdjustsStyleForTimeOfDay = true { + public var automaticallyAdjustsStyleForTimeOfDay = true { didSet { self.styleManager.automaticallyAdjustsStyleForTimeOfDay = self.automaticallyAdjustsStyleForTimeOfDay } } /** - If `true`, `UIApplication.isIdleTimerDisabled` is set to `true` in `viewWillAppear(_:)` and `false` in `viewWillDisappear(_:)`. If your application manages the idle timer itself, set this property to `false`. + If `true`, `UIApplication.isIdleTimerDisabled` is set to `true` while a navigation is running. If your application manages the idle timer itself, set this property to `false`. */ - @objc public var shouldManageApplicationIdleTimer = true + public var shouldManageApplicationIdleTimer = true /** Bool which should be set to true if a CarPlayNavigationView is also being used. */ - @objc public var isUsedInConjunctionWithCarPlayWindow = false { - didSet { - self.mapViewController?.isUsedInConjunctionWithCarPlayWindow = self.isUsedInConjunctionWithCarPlayWindow + public var isUsedInConjunctionWithCarPlayWindow: Bool { + get { + self.mapViewController.isUsedInConjunctionWithCarPlayWindow } - } - - var isConnectedToCarPlay: Bool { - if #available(iOS 12.0, *) { - CarPlayManager.shared.isConnectedToCarPlay - } else { - false + set { + self.mapViewController.isUsedInConjunctionWithCarPlayWindow = newValue } } - var mapViewController: RouteMapViewController? - /** A Boolean value that determines whether the map annotates the locations at which instructions are spoken for debugging purposes. */ - @objc public var annotatesSpokenInstructions = false - - var styleManager: StyleManager! - - private var locationManager: NavigationLocationManager! - - var currentStatusBarStyle: UIStatusBarStyle = .default { - didSet { - self.mapViewController?.instructionsBannerView.backgroundColor = InstructionsBannerView.appearance().backgroundColor - self.mapViewController?.instructionsBannerContentView.backgroundColor = InstructionsBannerContentView.appearance().backgroundColor - } - } + public var annotatesSpokenInstructions = false - override open var preferredStatusBarStyle: UIStatusBarStyle { - self.currentStatusBarStyle - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. - /// - /// ``` - /// let dayStyle = DayStyle(mapStyleURL: styleURL) - /// let vc = NavigationViewController(route: route, styles: [dayStyle]) - /// self.presentViewController(vc, animated: true) - /// ``` + // MARK: - Lifecycle + + /// Initializes a `NavigationViewController` that displays a map with a given style. /// - Parameters: - /// - route: The route to follow. - /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. - /// - styles: The `[dayStyle]` or `[dayStyle, nightStyle]` styles used to render the map. If nil, the default styles will be used. - /// - routeController: Used to monitor the route and notify of changes to the route. If nil, a default will be used. - /// - locationManager: Tracks the users location along the route. If nil, a default will be used. - /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. - /// - /// See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. - @available(*, deprecated, message: "Use `init(for:dayStyle:...) or init(for:dayStyleURL:...)` instead.") - @objc(initWithRoute:directions:styles:routeController:locationManager:voiceController:) - public convenience init(for route: Route, - directions: Directions = Directions.shared, - styles: [Style]? = [DayStyle(), NightStyle()], - routeController: RouteController? = nil, - locationManager: NavigationLocationManager? = nil, - voiceController: RouteVoiceController? = nil) { - let styles = styles ?? [] - assert(styles.count <= 2, "Having more than two styles is undefined.") - let dayStyle = styles.first ?? DayStyle(demoStyle: ()) - let nightStyle = styles.count > 1 ? styles[1] : NightStyle(mapStyleURL: dayStyle.mapStyleURL) - - self.init(for: route, dayStyle: dayStyle, nightStyle: nightStyle, directions: directions, routeController: routeController, locationManager: locationManager, voiceController: voiceController) - } - - /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. - /// - /// - Parameters: - /// - route: The route to follow. /// - dayStyleURL: URL for the style rules used to render the map during daylight hours. /// - nightStyleURL: URL for the style rules used to render the map during nighttime hours. If nil, `dayStyleURL` will be used at night as well. /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. - /// - routeController: Used to monitor the route and notify of changes to the route. If nil, a default will be used. - /// - locationManager: Tracks the users location along the route. If nil, a default will be used. /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. - /// - /// See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. - @objc(initWithRoute:dayStyleURL:nightStyleURL:directions:routeController:locationManager:voiceController:) - public convenience init(for route: Route, - dayStyleURL: URL, + @objc(initWithDayStyleURL:nightStyleURL:directions:voiceController:) + public convenience init(dayStyleURL: URL, nightStyleURL: URL? = nil, - directions: Directions = Directions.shared, - routeController: RouteController? = nil, - locationManager: NavigationLocationManager? = nil, - voiceController: RouteVoiceController? = nil) { + directions: Directions = .shared, + voiceController: RouteVoiceController = RouteVoiceController()) { let dayStyle = DayStyle(mapStyleURL: dayStyleURL) let nightStyle = NightStyle(mapStyleURL: nightStyleURL ?? dayStyleURL) - self.init(for: route, dayStyle: dayStyle, nightStyle: nightStyle, directions: directions, routeController: routeController, locationManager: locationManager, voiceController: voiceController) + self.init(dayStyle: dayStyle, nightStyle: nightStyle, directions: directions, voiceController: voiceController) } - /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. - /// + /// Initializes a `NavigationViewController` that displays a map with a given style. /// - Parameters: - /// - route: The route to follow. /// - dayStyle: Style used to render the map during daylight hours. /// - nightStyle: Style used to render the map during nighttime hours. If nil, `dayStyle` will be used at night as well. /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. - /// - routeController: Used to monitor the route and notify of changes to the route. If nil, a default will be used. - /// - locationManager: Tracks the users location along the route. If nil, a default will be used. /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. - /// - /// See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. - @objc(initWithRoute:dayStyle:nightStyle:directions:routeController:locationManager:voiceController:) - public required init(for route: Route, - dayStyle: Style, + @objc(initWithDayStyle:nightStyle:directions:voiceController:) + public required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, - routeController: RouteController? = nil, - locationManager: NavigationLocationManager? = nil, - voiceController: RouteVoiceController? = nil) { + voiceController: RouteVoiceController = RouteVoiceController()) { let nightStyle = { if let nightStyle { return nightStyle } - + let dayCopy: Style = dayStyle.copy() as! Style dayCopy.styleType = .night return dayCopy }() - assert(dayStyle.styleType == .day) assert(nightStyle.styleType == .night) - - super.init(nibName: nil, bundle: nil) - self.locationManager = locationManager ?? NavigationLocationManager() - let routeController = routeController ?? RouteController(along: route, directions: directions, locationManager: self.locationManager) - self.routeController = routeController - self.routeController.usesDefaultUserInterface = true - self.routeController.delegate = self - self.routeController.tunnelIntersectionManager.delegate = self - - self.voiceController = voiceController ?? RouteVoiceController() - - self.directions = directions - self.route = route - NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile - routeController.resume() - let mapViewController = RouteMapViewController(routeController: self.routeController, delegate: self) - self.mapViewController = mapViewController - mapViewController.destination = route.legs.last?.destination - mapViewController.willMove(toParent: self) - addChild(mapViewController) - mapViewController.didMove(toParent: self) - let mapSubview: UIView = mapViewController.view + self.directions = directions + self.voiceController = voiceController + self.mapViewController = RouteMapViewController(routeController: self.routeController) + self.locationManager = NavigationLocationManager() + + super.init(nibName: nil, bundle: nil) + + self.mapViewController.delegate = self + self.mapViewController.willMove(toParent: self) + self.addChild(self.mapViewController) + self.mapViewController.didMove(toParent: self) + let mapSubview: UIView = self.mapViewController.view mapSubview.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(mapSubview) - + self.view.addSubview(mapSubview) mapSubview.pinInSuperview() - mapViewController.reportButton.isHidden = !self.showsReportFeedback self.styleManager = StyleManager(self) self.styleManager.styles = [dayStyle, nightStyle] - - if !(route.routeOptions is NavigationRouteOptions) { - print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") - } + + self.mapViewController.navigationView.hideUI(animated: false) + self.mapView.tracksUserCourse = false } - + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + deinit { - suspendNotifications() + self.suspendNotifications() } + // MARK: - UIViewController + + override open var preferredStatusBarStyle: UIStatusBarStyle { + self.currentStatusBarStyle + } + override open func viewDidLoad() { super.viewDidLoad() - // Initialize voice controller if it hasn't been overridden. - // This is optional and lazy so it can be mutated by the developer after init. - _ = self.voiceController + self.resumeNotifications() - view.clipsToBounds = true + self.view.clipsToBounds = true } - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + // MARK: - NavigationViewController + + public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { + if let locationManager { + self.locationManager = locationManager + } + if let routeController { + self.routeController = routeController + } + self.route = route + + self.mapViewController.navigationView.showUI(animated: true) + self.mapViewController.destination = route.legs.last?.destination + self.routeController?.usesDefaultUserInterface = true + self.routeController?.delegate = self + self.routeController?.tunnelIntersectionManager.delegate = self + self.routeController?.resume() + self.mapViewController.prepareForNavigation() + + if !(route.routeOptions is NavigationRouteOptions) { + print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") + } + if self.shouldManageApplicationIdleTimer { UIApplication.shared.isIdleTimerDisabled = true } - - if let simulatedLocationManager = self.routeController.locationManager as? SimulatedLocationManager { + + if let simulatedLocationManager = self.routeController?.locationManager as? SimulatedLocationManager { let localized = String.Localized.simulationStatus(speed: Int(simulatedLocationManager.speedMultiplier)) - self.mapViewController?.statusView.show(localized, showSpinner: false, interactive: true) + self.mapViewController.statusView.show(localized, showSpinner: false, interactive: true) } } - - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) + + public func endNavigation(animated: Bool = true) { + self.routeController?.endNavigation() + self.mapView.removeRoutes() + self.mapView.removeWaypoints() + self.mapView.removeArrow() + self.voiceController = nil + self.route = nil + + self.mapViewController.navigationView.hideUI(animated: animated) + self.mapView.tracksUserCourse = false + self.mapView.userLocationForCourseTracking = nil + self.mapView.showsUserLocation = true + + let camera = self.mapView.camera + camera.pitch = 0 + self.mapView.setCamera(camera, animated: false) + if self.shouldManageApplicationIdleTimer { UIApplication.shared.isIdleTimerDisabled = false } - - self.routeController.suspendLocationUpdates() } - - // MARK: Route controller notifications - - func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(self.progressDidChange(notification:)), name: .routeControllerProgressDidChange, object: self.routeController) - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) - } - - func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: .routeControllerProgressDidChange, object: self.routeController) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) - } - - @objc func progressDidChange(notification: NSNotification) { - let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress - let location = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as! CLLocation - let secondsRemaining = routeProgress.currentLegProgress.currentStepProgress.durationRemaining - - self.mapViewController?.notifyDidChange(routeProgress: routeProgress, location: location, secondsRemaining: secondsRemaining) - - // If the user has arrived, don't snap the user puck. - // In the case the user drives beyond the waypoint, - // we should accurately depict this. - let shouldPreventReroutesWhenArrivingAtWaypoint = self.routeController.delegate?.routeController?(self.routeController, shouldPreventReroutesWhenArrivingAt: self.routeController.routeProgress.currentLeg.destination) ?? true - let userHasArrivedAndShouldPreventRerouting = shouldPreventReroutesWhenArrivingAtWaypoint && !self.routeController.routeProgress.currentLegProgress.userHasArrivedAtWaypoint - - if self.snapsUserLocationAnnotationToRoute, - userHasArrivedAndShouldPreventRerouting { - self.mapViewController?.mapView.updateCourseTracking(location: location, animated: true) - } - } - - @objc func didPassInstructionPoint(notification: NSNotification) { - let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress - - self.mapViewController?.updateCameraAltitude(for: routeProgress) - - self.clearStaleNotifications() - - if routeProgress.currentLegProgress.currentStepProgress.durationRemaining <= RouteControllerHighAlertInterval { - self.scheduleLocalNotification(about: routeProgress.currentLegProgress.currentStep, legIndex: routeProgress.legIndex, numberOfLegs: routeProgress.route.legs.count) - } - } - - func scheduleLocalNotification(about step: RouteStep, legIndex: Int?, numberOfLegs: Int?) { - guard self.sendsNotifications else { return } - guard UIApplication.shared.applicationState == .background else { return } - guard let text = step.instructionsSpokenAlongStep?.last?.text else { return } - - let notification = UILocalNotification() - notification.alertBody = text - notification.fireDate = Date() - - self.clearStaleNotifications() - - UIApplication.shared.cancelAllLocalNotifications() - UIApplication.shared.scheduleLocalNotification(notification) - } - - func clearStaleNotifications() { - guard self.sendsNotifications else { return } - // Remove all outstanding notifications from notification center. - // This will only work if it's set to 1 and then back to 0. - // This way, there is always just one notification. - UIApplication.shared.applicationIconBadgeNumber = 1 - UIApplication.shared.applicationIconBadgeNumber = 0 - } - + #if canImport(CarPlay) /** Presents a `NavigationViewController` on the top most view controller in the window and opens up the `StepsViewController`. @@ -592,8 +495,9 @@ open class NavigationViewController: UIViewController { let locationManager = routeController.locationManager.copy() as! NavigationLocationManager let directions = routeController.directions let route = routeController.routeProgress.route - let navigationViewController = NavigationViewController(for: route, dayStyle: DayStyle(demoStyle: ()), directions: directions, routeController: routeController, locationManager: locationManager) - + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ()), directions: directions) + navigationViewController.startNavigation(with: route, routeController: routeController, locationManager: locationManager) + window.rootViewController?.topMostViewController()?.present(navigationViewController, animated: true, completion: { navigationViewController.isUsedInConjunctionWithCarPlayWindow = true }) @@ -616,6 +520,14 @@ open class NavigationViewController: UIViewController { // MARK: - RouteMapViewControllerDelegate extension NavigationViewController: RouteMapViewControllerDelegate { + public func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { + self.delegate?.navigationViewController?(self, didFinishLoading: style) + } + + public func mapView(_ mapView: MLNMapView, didSelect annotation: any MLNAnnotation) { + self.delegate?.navigationViewController?(self, didSelect: annotation) + } + public func navigationMapView(_ mapView: NavigationMapView, routeCasingStyleLayerWithIdentifier identifier: String, source: MLNSource) -> MLNStyleLayer? { self.delegate?.navigationViewController?(self, routeCasingStyleLayerWithIdentifier: identifier, source: source) } @@ -628,11 +540,11 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.delegate?.navigationViewController?(self, didSelect: route) } - @objc public func navigationMapView(_ mapView: NavigationMapView, shapeFor routes: [Route]) -> MLNShape? { + public func navigationMapView(_ mapView: NavigationMapView, shapeFor routes: [Route]) -> MLNShape? { self.delegate?.navigationViewController?(self, shapeFor: routes) } - @objc public func navigationMapView(_ mapView: NavigationMapView, simplifiedShapeFor route: Route) -> MLNShape? { + public func navigationMapView(_ mapView: NavigationMapView, simplifiedShapeFor route: Route) -> MLNShape? { self.delegate?.navigationViewController?(self, simplifiedShapeFor: route) } @@ -644,24 +556,21 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.delegate?.navigationViewController?(self, waypointSymbolStyleLayerWithIdentifier: identifier, source: source) } - @objc public func navigationMapView(_ mapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) -> MLNShape? { + public func navigationMapView(_ mapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) -> MLNShape? { self.delegate?.navigationViewController?(self, shapeFor: waypoints, legIndex: legIndex) } - @objc public func navigationMapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { + public func navigationMapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { self.delegate?.navigationViewController?(self, imageFor: annotation) } - @objc public func navigationMapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? { + public func navigationMapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? { self.delegate?.navigationViewController?(self, viewFor: annotation) } - func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { - if self.delegate?.navigationViewControllerDidDismiss?(self, byCanceling: canceled) != nil { - // The receiver should handle dismissal of the NavigationViewController - } else { - dismiss(animated: true, completion: nil) - } + func mapViewControllerDidFinish(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { + self.endNavigation() + self.delegate?.navigationViewControllerDidFinishRouting?(self) } public func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { @@ -672,14 +581,14 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.annotatesSpokenInstructions } - @objc func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? { + func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? { guard let roadName = delegate?.navigationViewController?(self, roadNameAt: location) else { return nil } return roadName } - @objc public func label(_ label: InstructionLabel, willPresent instruction: VisualInstruction, as presented: NSAttributedString) -> NSAttributedString? { + public func label(_ label: InstructionLabel, willPresent instruction: VisualInstruction, as presented: NSAttributedString) -> NSAttributedString? { self.delegate?.label?(label, willPresent: instruction, as: presented) } } @@ -687,28 +596,29 @@ extension NavigationViewController: RouteMapViewControllerDelegate { // MARK: - RouteControllerDelegate extension NavigationViewController: RouteControllerDelegate { - @objc public func routeController(_ routeController: RouteController, shouldRerouteFrom location: CLLocation) -> Bool { + public func routeController(_ routeController: RouteController, shouldRerouteFrom location: CLLocation) -> Bool { self.delegate?.navigationViewController?(self, shouldRerouteFrom: location) ?? true } - @objc public func routeController(_ routeController: RouteController, willRerouteFrom location: CLLocation) { + public func routeController(_ routeController: RouteController, willRerouteFrom location: CLLocation) { self.delegate?.navigationViewController?(self, willRerouteFrom: location) } - @objc public func routeController(_ routeController: RouteController, didRerouteAlong route: Route, reason: RouteController.RerouteReason) { - self.mapViewController?.notifyDidReroute(route: route) + @objc + public func routeController(_ routeController: RouteController, didRerouteAlong route: Route, reason: RouteController.RerouteReason) { + self.mapViewController.notifyDidReroute(route: route) self.delegate?.navigationViewController?(self, didRerouteAlong: route) } - @objc public func routeController(_ routeController: RouteController, didFailToRerouteWith error: Error) { + public func routeController(_ routeController: RouteController, didFailToRerouteWith error: Error) { self.delegate?.navigationViewController?(self, didFailToRerouteWith: error) } - @objc public func routeController(_ routeController: RouteController, shouldDiscard location: CLLocation) -> Bool { + public func routeController(_ routeController: RouteController, shouldDiscard location: CLLocation) -> Bool { self.delegate?.navigationViewController?(self, shouldDiscard: location) ?? true } - @objc public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { + public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { // If the user has arrived, don't snap the user puck. // In the case the user drives beyond the waypoint, // we should accurately depict this. @@ -719,40 +629,45 @@ extension NavigationViewController: RouteControllerDelegate { let snappedLocation = routeController.location ?? locations.last, let rawLocation = locations.last, userHasArrivedAndShouldPreventRerouting { - self.mapViewController?.labelCurrentRoad(at: rawLocation, for: snappedLocation) + self.mapViewController.labelCurrentRoad(at: rawLocation, for: snappedLocation) } else if let rawlocation = locations.last { - self.mapViewController?.labelCurrentRoad(at: rawlocation) + self.mapViewController.labelCurrentRoad(at: rawlocation) } } - @objc public func routeController(_ routeController: RouteController, didArriveAt waypoint: Waypoint) -> Bool { + public func routeController(_ routeController: RouteController, didArriveAt waypoint: Waypoint) -> Bool { let advancesToNextLeg = self.delegate?.navigationViewController?(self, didArriveAt: waypoint) ?? true if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected - routeController.routeProgress.isFinalLeg, advancesToNextLeg, self.showsEndOfRouteFeedback { - self.mapViewController?.showEndOfRoute { _ in } + routeController.routeProgress.isFinalLeg, advancesToNextLeg { + self.mapViewController.transitionToEndNavigation(with: 1) + self.delegate?.navigationViewControllerDidFinishRouting?(self) } return advancesToNextLeg } } +// MARK: - TunnelIntersectionManagerDelegate + extension NavigationViewController: TunnelIntersectionManagerDelegate { public func tunnelIntersectionManager(_ manager: TunnelIntersectionManager, willEnableAnimationAt location: CLLocation) { - self.routeController.tunnelIntersectionManager(manager, willEnableAnimationAt: location) + self.routeController?.tunnelIntersectionManager(manager, willEnableAnimationAt: location) self.styleManager.applyStyle(type: .night) } public func tunnelIntersectionManager(_ manager: TunnelIntersectionManager, willDisableAnimationAt location: CLLocation) { - self.routeController.tunnelIntersectionManager(manager, willDisableAnimationAt: location) + self.routeController?.tunnelIntersectionManager(manager, willDisableAnimationAt: location) self.styleManager.timeOfDayChanged() } } +// MARK: - StyleManagerDelegate + extension NavigationViewController: StyleManagerDelegate { public func locationFor(styleManager: StyleManager) -> CLLocation? { - if let location = routeController.location { + if let location = self.routeController?.location { location - } else if let firstCoord = routeController.routeProgress.route.coordinates?.first { + } else if let firstCoord = self.routeController?.routeProgress.route.coordinates?.first { CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) } else { nil @@ -760,9 +675,9 @@ extension NavigationViewController: StyleManagerDelegate { } public func styleManager(_ styleManager: StyleManager, didApply style: Style) { - if self.mapView?.styleURL != style.mapStyleURL { - self.mapView?.style?.transition = MLNTransition(duration: 0.5, delay: 0) - self.mapView?.styleURL = style.mapStyleURL + if self.mapView.styleURL != style.mapStyleURL { + self.mapView.style?.transition = MLNTransition(duration: 0.5, delay: 0) + self.mapView.styleURL = style.mapStyleURL } self.currentStatusBarStyle = style.statusBarStyle ?? .default @@ -770,6 +685,80 @@ extension NavigationViewController: StyleManagerDelegate { } public func styleManagerDidRefreshAppearance(_ styleManager: StyleManager) { - self.mapView?.reloadStyle(self) + self.mapView.reloadStyle(self) + } +} + +// MARK: - Private + +private extension NavigationViewController { + var isConnectedToCarPlay: Bool { + CarPlayManager.shared.isConnectedToCarPlay + } + + func resumeNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(self.progressDidChange(notification:)), name: .routeControllerProgressDidChange, object: self.routeController) + NotificationCenter.default.addObserver(self, selector: #selector(self.didPassInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) + } + + func suspendNotifications() { + NotificationCenter.default.removeObserver(self, name: .routeControllerProgressDidChange, object: self.routeController) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) + } + + @objc func progressDidChange(notification: NSNotification) { + let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress + let location = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as! CLLocation + let secondsRemaining = routeProgress.currentLegProgress.currentStepProgress.durationRemaining + + self.mapViewController.notifyDidChange(routeProgress: routeProgress, location: location, secondsRemaining: secondsRemaining) + guard let routeController else { return } + + // If the user has arrived, don't snap the user puck. + // In the case the user drives beyond the waypoint, + // we should accurately depict this. + let shouldPreventReroutesWhenArrivingAtWaypoint = routeController.delegate?.routeController?(routeController, shouldPreventReroutesWhenArrivingAt: routeController.routeProgress.currentLeg.destination) ?? true + let userHasArrivedAndShouldPreventRerouting = shouldPreventReroutesWhenArrivingAtWaypoint && !routeController.routeProgress.currentLegProgress.userHasArrivedAtWaypoint + + if self.snapsUserLocationAnnotationToRoute, + userHasArrivedAndShouldPreventRerouting { + self.mapView.updateCourseTracking(location: location, animated: true) + } + } + + @objc func didPassInstructionPoint(notification: NSNotification) { + let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress + + self.mapViewController.updateCameraAltitude(for: routeProgress) + + self.clearStaleNotifications() + + if routeProgress.currentLegProgress.currentStepProgress.durationRemaining <= RouteControllerHighAlertInterval { + self.scheduleLocalNotification(about: routeProgress.currentLegProgress.currentStep, legIndex: routeProgress.legIndex, numberOfLegs: routeProgress.route.legs.count) + } + } + + func scheduleLocalNotification(about step: RouteStep, legIndex: Int?, numberOfLegs: Int?) { + guard self.sendsNotifications else { return } + guard UIApplication.shared.applicationState == .background else { return } + guard let text = step.instructionsSpokenAlongStep?.last?.text else { return } + + let notification = UILocalNotification() + notification.alertBody = text + notification.fireDate = Date() + + self.clearStaleNotifications() + + UIApplication.shared.cancelAllLocalNotifications() + UIApplication.shared.scheduleLocalNotification(notification) + } + + func clearStaleNotifications() { + guard self.sendsNotifications else { return } + // Remove all outstanding notifications from notification center. + // This will only work if it's set to 1 and then back to 0. + // This way, there is always just one notification. + UIApplication.shared.applicationIconBadgeNumber = 1 + UIApplication.shared.applicationIconBadgeNumber = 0 } } diff --git a/MapboxNavigation/NavigationViewLayout.swift b/MapboxNavigation/NavigationViewLayout.swift index e180fcf5..456c4cad 100644 --- a/MapboxNavigation/NavigationViewLayout.swift +++ b/MapboxNavigation/NavigationViewLayout.swift @@ -2,50 +2,42 @@ import UIKit extension NavigationView { func setupConstraints() { - mapView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mapView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mapView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - mapView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - instructionsBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - instructionsBannerContentView.bottomAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor).isActive = true - instructionsBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - instructionsBannerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - instructionsBannerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - instructionsBannerView.heightAnchor.constraint(equalToConstant: 96).isActive = true - - NSLayoutConstraint.activate(bannerShowConstraints) - - informationStackView.topAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor).isActive = true - informationStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - informationStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - floatingStackView.topAnchor.constraint(equalTo: informationStackView.bottomAnchor, constant: 10).isActive = true - floatingStackView.trailingAnchor.constraint(equalTo: safeTrailingAnchor, constant: -10).isActive = true - - resumeButton.leadingAnchor.constraint(equalTo: safeLeadingAnchor, constant: 10).isActive = true - resumeButton.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10).isActive = true - - bottomBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - bottomBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - bottomBannerContentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - bottomBannerContentView.topAnchor.constraint(equalTo: bottomBannerView.topAnchor).isActive = true - - bottomBannerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - bottomBannerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - bottomBannerView.bottomAnchor.constraint(equalTo: safeBottomAnchor).isActive = true - - wayNameView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - wayNameView.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10).isActive = true - } - - func constrainEndOfRoute() { - endOfRouteHideConstraint?.isActive = true - - endOfRouteView?.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - endOfRouteView?.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - endOfRouteHeightConstraint?.isActive = true + NSLayoutConstraint.activate([ + self.mapView.topAnchor.constraint(equalTo: self.topAnchor), + self.mapView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.mapView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + self.mapView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + + self.instructionsBannerContentView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.instructionsBannerContentView.bottomAnchor.constraint(equalTo: self.instructionsBannerView.bottomAnchor), + self.instructionsBannerContentView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + + self.instructionsBannerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.instructionsBannerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.instructionsBannerView.heightAnchor.constraint(equalToConstant: 96), + + self.informationStackView.topAnchor.constraint(equalTo: self.instructionsBannerView.bottomAnchor), + self.informationStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.informationStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + + self.floatingStackView.topAnchor.constraint(equalTo: self.informationStackView.bottomAnchor, constant: 10), + self.floatingStackView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: -10), + + self.resumeButton.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 10), + self.resumeButton.bottomAnchor.constraint(equalTo: self.bottomBannerView.topAnchor, constant: -10), + + self.bottomBannerContentView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.bottomBannerContentView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.bottomBannerContentView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + self.bottomBannerContentView.topAnchor.constraint(equalTo: self.bottomBannerView.topAnchor), + + self.bottomBannerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.bottomBannerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.bottomBannerView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor), + + self.wayNameView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + self.wayNameView.bottomAnchor.constraint(equalTo: self.bottomBannerView.topAnchor, constant: -10) + ]) + NSLayoutConstraint.activate(self.bannerShowConstraints) } } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 47794c9a..42888beb 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -8,35 +8,58 @@ import UIKit class ArrowFillPolyline: MLNPolylineFeature {} class ArrowStrokePolyline: ArrowFillPolyline {} +@objc protocol RouteMapViewControllerDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, VisualInstructionDelegate { + func mapViewControllerDidFinish(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) + func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool + + /** + Called to allow the delegate to customize the contents of the road name label that is displayed towards the bottom of the map view. + + This method is called on each location update. By default, the label displays the name of the road the user is currently traveling on. + + - parameter mapViewController: The route map view controller that will display the road name. + - parameter location: The user’s current location. + - return: The road name to display in the label, or the empty string to hide the label, or nil to query the map’s vector tiles for the road name. + */ + @objc func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? +} + class RouteMapViewController: UIViewController { + typealias LabelRoadNameCompletionHandler = (_ defaultRaodNameAssigned: Bool) -> Void + + private enum Actions { + static let overview: Selector = #selector(RouteMapViewController.toggleOverview(_:)) + static let mute: Selector = #selector(RouteMapViewController.toggleMute(_:)) + static let recenter: Selector = #selector(RouteMapViewController.recenter(_:)) + } + + private lazy var geocoder: CLGeocoder = .init() + + // MARK: - Properties + + let distanceFormatter = DistanceFormatter(approximate: true) var navigationView: NavigationView { view as! NavigationView } var mapView: NavigationMapView { self.navigationView.mapView } var statusView: StatusView { self.navigationView.statusView } - var reportButton: FloatingButton { self.navigationView.reportButton } var lanesView: LanesView { self.navigationView.lanesView } var nextBannerView: NextBannerView { self.navigationView.nextBannerView } var instructionsBannerView: InstructionsBannerView { self.navigationView.instructionsBannerView } var instructionsBannerContentView: InstructionsBannerContentView { self.navigationView.instructionsBannerContentView } - - lazy var endOfRouteViewController: EndOfRouteViewController = { - let storyboard = UIStoryboard(name: "Navigation", bundle: .mapboxNavigation) - let viewController = storyboard.instantiateViewController(withIdentifier: "EndOfRouteViewController") as! EndOfRouteViewController - return viewController - }() - - private enum Actions { - static let overview: Selector = #selector(RouteMapViewController.toggleOverview(_:)) - static let mute: Selector = #selector(RouteMapViewController.toggleMute(_:)) - static let recenter: Selector = #selector(RouteMapViewController.recenter(_:)) - } - - var route: Route { self.routeController.routeProgress.route } + var route: Route? { self.routeController?.routeProgress.route } var updateETATimer: Timer? var previewInstructionsView: StepInstructionsView? var lastTimeUserRerouted: Date? var stepsViewController: StepsViewController? - private lazy var geocoder: CLGeocoder = .init() var destination: Waypoint? + var showsEndOfRoute: Bool = true + var arrowCurrentStep: RouteStep? + + var contentInsets: UIEdgeInsets { + let top = self.navigationView.instructionsBannerContentView.bounds.height + let bottom = self.navigationView.bottomBannerView.bounds.height + return UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0) + } + var isUsedInConjunctionWithCarPlayWindow = false { didSet { if self.isUsedInConjunctionWithCarPlayWindow { @@ -47,8 +70,6 @@ class RouteMapViewController: UIViewController { } } - var showsEndOfRoute: Bool = true - var pendingCamera: MLNMapCamera? { guard let parent = parent as? NavigationViewController else { return nil @@ -63,17 +84,15 @@ class RouteMapViewController: UIViewController { return camera } - weak var delegate: RouteMapViewControllerDelegate? - var routeController: Router! { + var routeController: Router? { didSet { - self.navigationView.statusView.canChangeValue = self.routeController.locationManager is SimulatedLocationManager - guard let destination = route.legs.last?.destination else { return } + self.navigationView.statusView.canChangeValue = self.routeController?.locationManager is SimulatedLocationManager + guard let destination = self.route?.legs.last?.destination else { return } + self.populateName(for: destination, populated: { self.destination = $0 }) } } - - let distanceFormatter = DistanceFormatter(approximate: true) - var arrowCurrentStep: RouteStep? + var isInOverviewMode = false { didSet { if self.isInOverviewMode { @@ -91,39 +110,56 @@ class RouteMapViewController: UIViewController { var currentLegIndexMapped = 0 var currentStepIndexMapped = 0 - + var labelRoadNameCompletionHandler: LabelRoadNameCompletionHandler? + /** A Boolean value that determines whether the map annotates the locations at which instructions are spoken for debugging purposes. */ var annotatesSpokenInstructions = false var overheadInsets: UIEdgeInsets { - UIEdgeInsets(top: self.navigationView.instructionsBannerView.bounds.height, left: 20, bottom: self.navigationView.bottomBannerView.bounds.height, right: 20) + UIEdgeInsets(top: self.navigationView.instructionsBannerView.bounds.height, + left: 20, + bottom: self.navigationView.bottomBannerView.bounds.height, + right: 20) } + + lazy var endOfRouteViewController: EndOfRouteViewController = { + let storyboard = UIStoryboard(name: "Navigation", bundle: .mapboxNavigation) + let viewController = storyboard.instantiateViewController(withIdentifier: "EndOfRouteViewController") as! EndOfRouteViewController + return viewController + }() - typealias LabelRoadNameCompletionHandler = (_ defaultRaodNameAssigned: Bool) -> Void - - var labelRoadNameCompletionHandler: LabelRoadNameCompletionHandler? - - convenience init(routeController: RouteController, delegate: RouteMapViewControllerDelegate? = nil) { + weak var delegate: RouteMapViewControllerDelegate? + + // MARK: - Lifecycle + + convenience init(routeController: RouteController?, delegate: RouteMapViewControllerDelegate? = nil) { self.init() self.routeController = routeController self.delegate = delegate - automaticallyAdjustsScrollViewInsets = false + self.automaticallyAdjustsScrollViewInsets = false } + deinit { + self.suspendNotifications() + self.removeTimer() + } + + // MARK: - UIViewController + override func loadView() { - view = NavigationView(delegate: self) - view.frame = parent?.view.bounds ?? UIScreen.main.bounds + self.view = NavigationView(delegate: self) + self.view.frame = self.parent?.view.bounds ?? UIScreen.main.bounds } override func viewDidLoad() { super.viewDidLoad() self.mapView.contentInset = self.contentInsets - view.layoutIfNeeded() + self.view.layoutIfNeeded() - self.mapView.tracksUserCourse = true + self.mapView.tracksUserCourse = self.route != nil self.distanceFormatter.numberFormatter.locale = .nationalizedCurrent @@ -134,41 +170,14 @@ class RouteMapViewController: UIViewController { self.notifyUserAboutLowVolume() } - deinit { - suspendNotifications() - removeTimer() - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - resetETATimer() - - self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted - self.mapView.compassView.isHidden = true - - self.mapView.tracksUserCourse = true - - if let camera = pendingCamera { - self.mapView.camera = camera - } else if let location = routeController.location, location.course > 0 { - self.mapView.updateCourseTracking(location: location, animated: false) - } else if let coordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { - let secondCoordinate = coordinates[1] - let course = firstCoordinate.direction(to: secondCoordinate) - let newLocation = CLLocation(coordinate: routeController.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) - self.mapView.updateCourseTracking(location: newLocation, animated: false) - } else { - self.mapView.setCamera(self.tiltedCamera, animated: false) - } + self.prepareForNavigation() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.annotatesSpokenInstructions = self.delegate?.mapViewControllerShouldAnnotateSpokenInstructions(self) ?? false - showRouteIfNeeded() - self.currentLegIndexMapped = self.routeController.routeProgress.legIndex - self.currentStepIndexMapped = self.routeController.routeProgress.currentLegProgress.stepIndex + self.prepareForMap() } override func viewWillDisappear(_ animated: Bool) { @@ -176,68 +185,6 @@ class RouteMapViewController: UIViewController { self.removeTimer() } - func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(self.willReroute(notification:)), name: .routeControllerWillReroute, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didReroute(notification:)), name: .routeControllerDidReroute, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.rerouteDidFail(notification:)), name: .routeControllerDidFailToReroute, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.removeTimer), name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.updateInstructionsBanner(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: self.routeController) - subscribeToKeyboardNotifications() - } - - func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: .routeControllerWillReroute, object: nil) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidReroute, object: nil) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidFailToReroute, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassVisualInstructionPoint, object: nil) - unsubscribeFromKeyboardNotifications() - } - - @objc func recenter(_ sender: AnyObject) { - self.mapView.tracksUserCourse = true - self.mapView.enableFrameByFrameCourseViewTracking(for: 3) - self.isInOverviewMode = false - self.updateCameraAltitude(for: self.routeController.routeProgress) - - self.mapView.addArrow(route: self.routeController.routeProgress.route, - legIndex: self.routeController.routeProgress.legIndex, - stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) - - self.removePreviewInstructions() - } - - @objc func removeTimer() { - self.updateETATimer?.invalidate() - self.updateETATimer = nil - } - - func removePreviewInstructions() { - if let view = previewInstructionsView { - view.removeFromSuperview() - self.navigationView.instructionsBannerContentView.backgroundColor = InstructionsBannerView.appearance().backgroundColor - self.navigationView.instructionsBannerView.delegate = self - self.previewInstructionsView = nil - } - } - - @objc func toggleOverview(_ sender: Any) { - self.mapView.enableFrameByFrameCourseViewTracking(for: 3) - if let coordinates = routeController.routeProgress.route.coordinates, let userLocation = routeController.locationManager.location?.coordinate { - self.mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: self.overheadInsets) - } - self.isInOverviewMode = true - } - - @objc func toggleMute(_ sender: UIButton) { - sender.isSelected = !sender.isSelected - - let muted = sender.isSelected - NavigationSettings.shared.voiceMuted = muted - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) self.mapView.enableFrameByFrameCourseViewTracking(for: 3) @@ -248,19 +195,70 @@ class RouteMapViewController: UIViewController { self.mapView.setContentInset(self.contentInsets, animated: true, completionHandler: nil) self.mapView.setNeedsUpdateConstraints() } + + // MARK: - UIContentContainer + + override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { + UIView.animate(withDuration: 0.3, animations: self.view.layoutIfNeeded) + } + + // MARK: - RouteMapViewController + + func prepareForNavigation() { + guard let routeController else { return } + + self.resetETATimer() + self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted + self.mapView.compassView.isHidden = true + self.mapView.tracksUserCourse = true + if let camera = self.pendingCamera { + self.mapView.camera = camera + return + } + + if let location = routeController.location, + location.course > 0 { + self.mapView.updateCourseTracking(location: location, animated: false) + } else if let coordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates, + let firstCoordinate = coordinates.first, + coordinates.count > 1 { + let secondCoordinate = coordinates[1] + let course = firstCoordinate.direction(to: secondCoordinate) + let newLocation = CLLocation(coordinate: routeController.location?.coordinate ?? firstCoordinate, + altitude: 0, + horizontalAccuracy: 0, + verticalAccuracy: 0, + course: course, + speed: 0, + timestamp: Date()) + self.mapView.updateCourseTracking(location: newLocation, animated: false) + } + } + + func prepareForMap() { + self.annotatesSpokenInstructions = self.delegate?.mapViewControllerShouldAnnotateSpokenInstructions(self) ?? false + self.showRouteIfNeeded() + self.currentLegIndexMapped = self.routeController?.routeProgress.legIndex ?? 0 + self.currentStepIndexMapped = self.routeController?.routeProgress.currentLegProgress.stepIndex ?? 0 + } + func notifyDidReroute(route: Route) { - updateETA() + guard let routeController else { + assertionFailure("routeController needs to be set before calling this method") + return + } + + self.updateETA() self.currentStepIndexMapped = 0 + self.instructionsBannerView.updateDistance(for: routeController.routeProgress.currentLegProgress.currentStepProgress) - self.instructionsBannerView.updateDistance(for: self.routeController.routeProgress.currentLegProgress.currentStepProgress) - - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex, stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) - self.mapView.showRoutes([self.routeController.routeProgress.route], legIndex: self.routeController.routeProgress.legIndex) - self.mapView.showWaypoints(self.routeController.routeProgress.route) + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) + self.mapView.showRoutes([routeController.routeProgress.route], legIndex: routeController.routeProgress.legIndex) + self.mapView.showWaypoints(routeController.routeProgress.route) if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: self.routeController.routeProgress.route) + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } if self.isInOverviewMode { @@ -279,23 +277,8 @@ class RouteMapViewController: UIViewController { } } - @objc func applicationWillEnterForeground(notification: NSNotification) { - self.mapView.updateCourseTracking(location: self.routeController.location, animated: false) - resetETATimer() - } - - @objc func willReroute(notification: NSNotification) { - let title = NSLocalizedString("REROUTING", bundle: .mapboxNavigation, value: "Rerouting…", comment: "Indicates that rerouting is in progress") - self.lanesView.hide() - self.statusView.show(title, showSpinner: true) - } - - @objc func rerouteDidFail(notification: NSNotification) { - self.statusView.hide() - } - func notifyUserAboutLowVolume() { - guard !(self.routeController.locationManager is SimulatedLocationManager) else { return } + guard !(self.routeController?.locationManager is SimulatedLocationManager) else { return } guard !NavigationSettings.shared.voiceMuted else { return } guard AVAudioSession.sharedInstance().outputVolume <= NavigationViewMinimumVolumeForWarning else { return } @@ -304,32 +287,16 @@ class RouteMapViewController: UIViewController { self.statusView.hide(delay: 3, animated: true) } - @objc func didReroute(notification: NSNotification) { - guard isViewLoaded else { return } - - if let locationManager = routeController.locationManager as? SimulatedLocationManager { - let localized = String.Localized.simulationStatus(speed: Int(locationManager.speedMultiplier)) - self.showStatus(title: localized, for: .infinity, interactive: true) - } else { - self.statusView.hide(delay: 2, animated: true) - } - - if notification.userInfo![RouteControllerNotificationUserInfoKey.isProactiveKey] as! Bool { - let title = NSLocalizedString("FASTER_ROUTE_FOUND", bundle: .mapboxNavigation, value: "Faster Route Found", comment: "Indicates a faster route was found") - self.showStatus(title: title, withSpinner: true, for: 3) - } - } - - @objc func updateInstructionsBanner(notification: NSNotification) { - guard let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress else { return } - self.instructionsBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) - self.lanesView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) - self.nextBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) - } - func updateMapOverlays(for routeProgress: RouteProgress) { + guard let routeController else { + assertionFailure("routeController needs to be set during navigation") + return + } + if routeProgress.currentLegProgress.followOnStep != nil { - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex, stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) + self.mapView.addArrow(route: routeController.routeProgress.route, + legIndex: routeController.routeProgress.legIndex, + stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) } else { self.mapView.removeArrow() } @@ -358,28 +325,14 @@ class RouteMapViewController: UIViewController { } } - private func showStatus(title: String, withSpinner spin: Bool = false, for time: TimeInterval, animated: Bool = true, interactive: Bool = false) { - self.statusView.show(title, showSpinner: spin, interactive: interactive) - guard time < .infinity else { return } - self.statusView.hide(delay: time, animated: animated) - } - - private func setCamera(altitude: Double) { - guard self.mapView.altitude != altitude else { return } - self.mapView.altitude = altitude - } - - func mapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { - navigationMapView(mapView, imageFor: annotation) - } - - func mapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? { - navigationMapView(mapView, viewFor: annotation) - } - func notifyDidChange(routeProgress: RouteProgress, location: CLLocation, secondsRemaining: TimeInterval) { - resetETATimer() - updateETA() + guard let routeController else { + assertionFailure("routeController needs to be set during navigation") + return + } + + self.resetETATimer() + self.updateETA() self.instructionsBannerView.updateDistance(for: routeProgress.currentLegProgress.currentStepProgress) @@ -396,123 +349,147 @@ class RouteMapViewController: UIViewController { } if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: self.routeController.routeProgress.route) + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } } - var contentInsets: UIEdgeInsets { - let top = self.navigationView.instructionsBannerContentView.bounds.height - let bottom = self.navigationView.bottomBannerView.bounds.height - return UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0) - } - - // MARK: End Of Route - - func embedEndOfRoute() { - let endOfRoute = self.endOfRouteViewController - addChild(endOfRoute) - self.navigationView.endOfRouteView = endOfRoute.view - self.navigationView.constrainEndOfRoute() - endOfRoute.didMove(toParent: self) + /** + Updates the current road name label to reflect the road on which the user is currently traveling. - endOfRoute.dismissHandler = { [weak self] _, _ in - self?.routeController.endNavigation() - self?.delegate?.mapViewControllerDidDismiss(self!, byCanceling: false) + - parameter location: The user’s current location. + */ + func labelCurrentRoad(at rawLocation: CLLocation, for snappedLoction: CLLocation? = nil) { + guard self.navigationView.resumeButton.isHidden else { + return } - } - - func unembedEndOfRoute() { - let endOfRoute = self.endOfRouteViewController - endOfRoute.willMove(toParent: nil) - endOfRoute.removeFromParent() - } - func showEndOfRoute(duration: TimeInterval = 1.0, completion: ((Bool) -> Void)? = nil) { - self.embedEndOfRoute() - self.endOfRouteViewController.destination = self.destination - self.navigationView.endOfRouteView?.isHidden = false - - view.layoutIfNeeded() // flush layout queue - NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) - NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) - self.navigationView.endOfRouteHideConstraint?.isActive = false - self.navigationView.endOfRouteShowConstraint?.isActive = true - - self.mapView.enableFrameByFrameCourseViewTracking(for: duration) - self.mapView.setNeedsUpdateConstraints() - - let animate = { - self.view.layoutIfNeeded() - self.navigationView.floatingStackView.alpha = 0.0 + let roadName = self.delegate?.mapViewController(self, roadNameAt: rawLocation) + guard roadName == nil else { + if let roadName { + self.navigationView.wayNameView.text = roadName + self.navigationView.wayNameView.isHidden = roadName.isEmpty + } + return } - let noAnimation = { animate(); completion?(true) } - - guard duration > 0.0 else { return noAnimation() } - - self.navigationView.mapView.tracksUserCourse = false - UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) - - guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } - let insets = UIEdgeInsets(top: navigationView.instructionsBannerView.bounds.height, left: 20, bottom: height + 20, right: 20) + let location = snappedLoction ?? rawLocation - if let coordinates = routeController.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { - let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates - let line = MLNPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) + self.labelCurrentRoadFeature(at: location) - let camera = self.navigationView.mapView.cameraThatFitsShape(line, direction: self.navigationView.mapView.camera.heading, edgePadding: insets) - camera.pitch = 0 - camera.altitude = self.navigationView.mapView.camera.altitude - self.navigationView.mapView.setCamera(camera, animated: true) + if let labelRoadNameCompletionHandler { + labelRoadNameCompletionHandler(true) } } - func hideEndOfRoute(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) { - view.layoutIfNeeded() // flush layout queue - self.navigationView.endOfRouteHideConstraint?.isActive = true - self.navigationView.endOfRouteShowConstraint?.isActive = false - view.clipsToBounds = true + func labelCurrentRoadFeature(at location: CLLocation) { + guard let stepCoordinates = self.routeController?.routeProgress.currentLegProgress.currentStep.coordinates else { + return + } + + let closestCoordinate = location.coordinate + let roadLabelLayerIdentifier = "roadLabelLayer" + + let userPuck = self.mapView.convert(closestCoordinate, toPointTo: self.mapView) + let features = self.mapView.visibleFeatures(at: userPuck, styleLayerIdentifiers: Set([roadLabelLayerIdentifier])) + var smallestLabelDistance = Double.infinity + var currentName: String? + var currentShieldName: NSAttributedString? + + for feature in features { + var allLines: [MLNPolyline] = [] + + if let line = feature as? MLNPolylineFeature { + allLines.append(line) + } else if let lines = feature as? MLNMultiPolylineFeature { + allLines = lines.polylines + } + + for line in allLines { + let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount))) + let featurePolyline = Polyline(featureCoordinates) + let slicedLine = Polyline(stepCoordinates).sliced(from: closestCoordinate) + + let lookAheadDistance: CLLocationDistance = 10 + guard let pointAheadFeature = featurePolyline.sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } + guard let pointAheadUser = slicedLine.coordinateFromStart(distance: lookAheadDistance) else { continue } + guard let reversedPoint = Polyline(featureCoordinates.reversed()).sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } + + let distanceBetweenPointsAhead = pointAheadFeature.distance(to: pointAheadUser) + let distanceBetweenReversedPoint = reversedPoint.distance(to: pointAheadUser) + let minDistanceBetweenPoints = min(distanceBetweenPointsAhead, distanceBetweenReversedPoint) + + if minDistanceBetweenPoints < smallestLabelDistance { + smallestLabelDistance = minDistanceBetweenPoints + + if let line = feature as? MLNPolylineFeature { + let roadNameRecord = self.roadFeature(for: line) + currentShieldName = roadNameRecord.shieldName + currentName = roadNameRecord.roadName + } else if let line = feature as? MLNMultiPolylineFeature { + let roadNameRecord = self.roadFeature(for: line) + currentShieldName = roadNameRecord.shieldName + currentName = roadNameRecord.roadName + } + } + } + } + + let hasWayName = currentName != nil || currentShieldName != nil + if smallestLabelDistance < 5, hasWayName { + if let currentShieldName { + self.navigationView.wayNameView.attributedText = currentShieldName + } else if let currentName { + self.navigationView.wayNameView.text = currentName + } + self.navigationView.wayNameView.isHidden = false + } else { + self.navigationView.wayNameView.isHidden = true + } + } + + // MARK: End Of Route + + func transitionToEndNavigation(with duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { + self.view.layoutIfNeeded() // flush layout queue + NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) + NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) self.mapView.enableFrameByFrameCourseViewTracking(for: duration) self.mapView.setNeedsUpdateConstraints() let animate = { self.view.layoutIfNeeded() - self.navigationView.floatingStackView.alpha = 1.0 - } - - let complete: (Bool) -> Void = { - self.navigationView.endOfRouteView?.isHidden = true - self.unembedEndOfRoute() - completion?($0) + self.navigationView.floatingStackView.alpha = 0.0 } - let noAnimation = { - animate() - complete(true) - } + let noAnimation = { animate(); completion?(true) } guard duration > 0.0 else { return noAnimation() } - UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: complete) - } - fileprivate func populateName(for waypoint: Waypoint, populated: @escaping (Waypoint) -> Void) { - guard waypoint.name == nil else { return populated(waypoint) } - CLGeocoder().reverseGeocodeLocation(waypoint.location) { places, error in - guard let place = places?.first, let placeName = place.name, error == nil else { return } - let named = Waypoint(coordinate: waypoint.coordinate, name: placeName) - return populated(named) - } + self.navigationView.mapView.tracksUserCourse = false + UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) } -} -// MARK: - UIContentContainer + func hideEndOfRoute(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) { + self.view.layoutIfNeeded() // flush layout queue + self.view.clipsToBounds = true -extension RouteMapViewController { - override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - self.navigationView.endOfRouteHeightConstraint?.constant = container.preferredContentSize.height + self.mapView.enableFrameByFrameCourseViewTracking(for: duration) + self.mapView.setNeedsUpdateConstraints() + + let animate = { + self.view.layoutIfNeeded() + self.navigationView.floatingStackView.alpha = 1.0 + } + + let noAnimation = { + animate() + completion?(true) + } - UIView.animate(withDuration: 0.3, animations: view.layoutIfNeeded) + guard duration > 0.0 else { return noAnimation() } + + UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) } } @@ -522,7 +499,7 @@ extension RouteMapViewController: NavigationViewDelegate { // MARK: NavigationViewDelegate func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) { - self.delegate?.mapViewControllerDidDismiss(self, byCanceling: true) + self.delegate?.mapViewControllerDidFinish(self, byCanceling: true) } // MARK: MLNMapViewDelegate @@ -549,6 +526,8 @@ extension RouteMapViewController: NavigationViewDelegate { self.delegate?.mapViewDidFinishLoadingMap?(mapView) } + // MARK: - VisualInstructionDelegate + func label(_ label: InstructionLabel, willPresent instruction: VisualInstruction, as presented: NSAttributedString) -> NSAttributedString? { self.delegate?.label?(label, willPresent: instruction, as: presented) } @@ -556,12 +535,20 @@ extension RouteMapViewController: NavigationViewDelegate { // MARK: NavigationMapViewCourseTrackingDelegate func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) { + // Important, this look redundant but it needed. + // In NavigationView.showUI(animated:) we animate using alpha and then set isHidden + // To keep this in sync we also need to adjust alpha here as well. self.navigationView.resumeButton.isHidden = true + self.navigationView.resumeButton.alpha = 0 mapView.logoView.isHidden = false } func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) { + // Important, this look redundant but it needed. + // In NavigationView.hideUI(animated:) we animate using alpha and then set isHidden + // To keep this in sync we also need to adjust alpha here as well. self.navigationView.resumeButton.isHidden = false + self.navigationView.resumeButton.alpha = 1 self.navigationView.wayNameView.isHidden = true mapView.logoView.isHidden = true } @@ -575,31 +562,6 @@ extension RouteMapViewController: NavigationViewDelegate { func didTapInstructionsBanner(_ sender: BaseInstructionsBannerView) { self.displayPreviewInstructions() } - - private func displayPreviewInstructions() { - self.removePreviewInstructions() - - if let controller = stepsViewController { - self.stepsViewController = nil - controller.dismiss() - } else { - let controller = StepsViewController(routeProgress: routeController.routeProgress) - controller.delegate = self - addChild(controller) - view.insertSubview(controller.view, belowSubview: self.navigationView.instructionsBannerContentView) - - controller.view.topAnchor.constraint(equalTo: self.navigationView.instructionsBannerContentView.bottomAnchor).isActive = true - controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - - controller.didMove(toParent: self) - controller.dropDownAnimation() - - self.stepsViewController = controller - return - } - } // MARK: NavigationMapViewDelegate @@ -644,110 +606,237 @@ extension RouteMapViewController: NavigationViewDelegate { } func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { - // If the end of route component is showing, then put the anchor point slightly above the middle of the map - if self.navigationView.endOfRouteView != nil, let show = navigationView.endOfRouteShowConstraint, show.isActive { - return CGPoint(x: mapView.bounds.midX, y: mapView.bounds.height * 0.4) - } - - // otherwise, ask the delegate or return .zero - return self.delegate?.navigationMapViewUserAnchorPoint?(mapView) ?? .zero + self.delegate?.navigationMapViewUserAnchorPoint?(mapView) ?? .zero } +} - /** - Updates the current road name label to reflect the road on which the user is currently traveling. +// MARK: StepsViewControllerDelegate - - parameter location: The user’s current location. - */ - func labelCurrentRoad(at rawLocation: CLLocation, for snappedLoction: CLLocation? = nil) { - guard self.navigationView.resumeButton.isHidden else { +extension RouteMapViewController: StepsViewControllerDelegate { + func stepsViewController(_ viewController: StepsViewController, didSelect legIndex: Int, stepIndex: Int, cell: StepTableViewCell) { + guard let routeController else { + assertionFailure("routeController is needed during navigation") return } + + let legProgress = RouteLegProgress(leg: routeController.routeProgress.route.legs[legIndex], stepIndex: stepIndex) + let step = legProgress.currentStep + guard let upcomingStep = legProgress.upComingStep else { return } - let roadName = self.delegate?.mapViewController(self, roadNameAt: rawLocation) - guard roadName == nil else { - if let roadName { - self.navigationView.wayNameView.text = roadName - self.navigationView.wayNameView.isHidden = roadName.isEmpty - } - return + viewController.dismiss { + self.addPreviewInstructions(step: step, maneuverStep: upcomingStep, distance: cell.instructionsView.distance) + self.stepsViewController = nil } - let location = snappedLoction ?? rawLocation + self.mapView.enableFrameByFrameCourseViewTracking(for: 1) + self.mapView.tracksUserCourse = false + self.mapView.setCenter(upcomingStep.maneuverLocation, zoomLevel: self.mapView.zoomLevel, direction: upcomingStep.initialHeading!, animated: true, completionHandler: nil) - self.labelCurrentRoadFeature(at: location) + guard isViewLoaded, view.window != nil else { return } + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: legIndex, stepIndex: stepIndex + 1) + } - if let labelRoadNameCompletionHandler { - labelRoadNameCompletionHandler(true) + func addPreviewInstructions(step: RouteStep, maneuverStep: RouteStep, distance: CLLocationDistance?) { + self.removePreviewInstructions() + + guard let instructions = step.instructionsDisplayedAlongStep?.last else { return } + + let instructionsView = StepInstructionsView(frame: navigationView.instructionsBannerView.frame) + instructionsView.backgroundColor = StepInstructionsView.appearance().backgroundColor + instructionsView.delegate = self + instructionsView.distance = distance + + self.navigationView.instructionsBannerContentView.backgroundColor = instructionsView.backgroundColor + + view.addSubview(instructionsView) + instructionsView.update(for: instructions) + self.previewInstructionsView = instructionsView + } + + func didDismissStepsViewController(_ viewController: StepsViewController) { + viewController.dismiss { + self.stepsViewController = nil + self.navigationView.instructionsBannerView.stepListIndicatorView.isHidden = false } } - func labelCurrentRoadFeature(at location: CLLocation) { - guard let stepCoordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates else { - return + func statusView(_ statusView: StatusView, valueChangedTo value: Double) { + let displayValue = 1 + min(Int(9 * value), 8) + let title = String.Localized.simulationStatus(speed: displayValue) + self.showStatus(title: title, for: .infinity, interactive: true) + + if let locationManager = self.routeController?.locationManager as? SimulatedLocationManager { + locationManager.speedMultiplier = Double(displayValue) } + } +} - let closestCoordinate = location.coordinate - let roadLabelLayerIdentifier = "roadLabelLayer" +// MARK: - Keyboard Handling - let userPuck = self.mapView.convert(closestCoordinate, toPointTo: self.mapView) - let features = self.mapView.visibleFeatures(at: userPuck, styleLayerIdentifiers: Set([roadLabelLayerIdentifier])) - var smallestLabelDistance = Double.infinity - var currentName: String? - var currentShieldName: NSAttributedString? +private extension RouteMapViewController { + @objc + func recenter(_ sender: AnyObject) { + self.mapView.tracksUserCourse = true + self.mapView.enableFrameByFrameCourseViewTracking(for: 3) + self.isInOverviewMode = false + guard let routeController else { return } + + self.updateCameraAltitude(for: routeController.routeProgress) - for feature in features { - var allLines: [MLNPolyline] = [] + self.mapView.addArrow(route: routeController.routeProgress.route, + legIndex: routeController.routeProgress.legIndex, + stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) - if let line = feature as? MLNPolylineFeature { - allLines.append(line) - } else if let lines = feature as? MLNMultiPolylineFeature { - allLines = lines.polylines - } + self.removePreviewInstructions() + } - for line in allLines { - let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount))) - let featurePolyline = Polyline(featureCoordinates) - let slicedLine = Polyline(stepCoordinates).sliced(from: closestCoordinate) + @objc + func removeTimer() { + self.updateETATimer?.invalidate() + self.updateETATimer = nil + } + + @objc + func toggleOverview(_ sender: Any) { + self.mapView.enableFrameByFrameCourseViewTracking(for: 3) + if let coordinates = self.routeController?.routeProgress.route.coordinates, + let userLocation = self.routeController?.locationManager.location?.coordinate { + self.mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: self.overheadInsets) + } + self.isInOverviewMode = true + } - let lookAheadDistance: CLLocationDistance = 10 - guard let pointAheadFeature = featurePolyline.sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } - guard let pointAheadUser = slicedLine.coordinateFromStart(distance: lookAheadDistance) else { continue } - guard let reversedPoint = Polyline(featureCoordinates.reversed()).sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } + @objc + func toggleMute(_ sender: UIButton) { + sender.isSelected = !sender.isSelected - let distanceBetweenPointsAhead = pointAheadFeature.distance(to: pointAheadUser) - let distanceBetweenReversedPoint = reversedPoint.distance(to: pointAheadUser) - let minDistanceBetweenPoints = min(distanceBetweenPointsAhead, distanceBetweenReversedPoint) + let muted = sender.isSelected + NavigationSettings.shared.voiceMuted = muted + } + + @objc + func applicationWillEnterForeground(notification: NSNotification) { + self.mapView.updateCourseTracking(location: self.routeController?.location, animated: false) + self.resetETATimer() + } - if minDistanceBetweenPoints < smallestLabelDistance { - smallestLabelDistance = minDistanceBetweenPoints + @objc + func willReroute(notification: NSNotification) { + let title = NSLocalizedString("REROUTING", bundle: .mapboxNavigation, value: "Rerouting…", comment: "Indicates that rerouting is in progress") + self.lanesView.hide() + self.statusView.show(title, showSpinner: true) + } - if let line = feature as? MLNPolylineFeature { - let roadNameRecord = self.roadFeature(for: line) - currentShieldName = roadNameRecord.shieldName - currentName = roadNameRecord.roadName - } else if let line = feature as? MLNMultiPolylineFeature { - let roadNameRecord = self.roadFeature(for: line) - currentShieldName = roadNameRecord.shieldName - currentName = roadNameRecord.roadName - } - } - } - } + @objc + func rerouteDidFail(notification: NSNotification) { + self.statusView.hide() + } + + @objc + func didReroute(notification: NSNotification) { + guard isViewLoaded else { return } - let hasWayName = currentName != nil || currentShieldName != nil - if smallestLabelDistance < 5, hasWayName { - if let currentShieldName { - self.navigationView.wayNameView.attributedText = currentShieldName - } else if let currentName { - self.navigationView.wayNameView.text = currentName - } - self.navigationView.wayNameView.isHidden = false + if let locationManager = self.routeController?.locationManager as? SimulatedLocationManager { + let localized = String.Localized.simulationStatus(speed: Int(locationManager.speedMultiplier)) + self.showStatus(title: localized, for: .infinity, interactive: true) } else { - self.navigationView.wayNameView.isHidden = true + self.statusView.hide(delay: 2, animated: true) + } + + if notification.userInfo![RouteControllerNotificationUserInfoKey.isProactiveKey] as! Bool { + let title = NSLocalizedString("FASTER_ROUTE_FOUND", bundle: .mapboxNavigation, value: "Faster Route Found", comment: "Indicates a faster route was found") + self.showStatus(title: title, withSpinner: true, for: 3) } } - private func roadFeature(for line: MLNPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { + @objc + func updateInstructionsBanner(notification: NSNotification) { + guard let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress else { return } + self.instructionsBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) + self.lanesView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) + self.nextBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) + } + + @objc + func updateETA() { + guard isViewLoaded, let routeController else { return } + + self.navigationView.bottomBannerView.updateETA(routeProgress: routeController.routeProgress) + } + + func resumeNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(self.willReroute(notification:)), name: .routeControllerWillReroute, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.didReroute(notification:)), name: .routeControllerDidReroute, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.rerouteDidFail(notification:)), name: .routeControllerDidFailToReroute, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.removeTimer), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.updateInstructionsBanner(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: self.routeController) + } + + func suspendNotifications() { + NotificationCenter.default.removeObserver(self, name: .routeControllerWillReroute, object: nil) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidReroute, object: nil) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidFailToReroute, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassVisualInstructionPoint, object: nil) + } + + func showStatus(title: String, withSpinner spin: Bool = false, for time: TimeInterval, animated: Bool = true, interactive: Bool = false) { + self.statusView.show(title, showSpinner: spin, interactive: interactive) + guard time < .infinity else { return } + self.statusView.hide(delay: time, animated: animated) + } + + func setCamera(altitude: Double) { + guard self.mapView.altitude != altitude else { return } + self.mapView.altitude = altitude + } + + func populateName(for waypoint: Waypoint, populated: @escaping (Waypoint) -> Void) { + guard waypoint.name == nil else { return populated(waypoint) } + CLGeocoder().reverseGeocodeLocation(waypoint.location) { places, error in + guard let place = places?.first, let placeName = place.name, error == nil else { return } + let named = Waypoint(coordinate: waypoint.coordinate, name: placeName) + return populated(named) + } + } + + func removePreviewInstructions() { + if let view = previewInstructionsView { + view.removeFromSuperview() + self.navigationView.instructionsBannerContentView.backgroundColor = InstructionsBannerView.appearance().backgroundColor + self.navigationView.instructionsBannerView.delegate = self + self.previewInstructionsView = nil + } + } + + func displayPreviewInstructions() { + self.removePreviewInstructions() + + if let controller = stepsViewController { + self.stepsViewController = nil + controller.dismiss() + } else if let routeController { + let controller = StepsViewController(routeProgress: routeController.routeProgress) + controller.delegate = self + addChild(controller) + view.insertSubview(controller.view, belowSubview: self.navigationView.instructionsBannerContentView) + + controller.view.topAnchor.constraint(equalTo: self.navigationView.instructionsBannerContentView.bottomAnchor).isActive = true + controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + controller.didMove(toParent: self) + controller.dropDownAnimation() + + self.stepsViewController = controller + return + } + } + + func roadFeature(for line: MLNPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { let roadNameRecord = self.roadFeatureHelper(ref: line.attribute(forKey: "ref"), shield: line.attribute(forKey: "shield"), reflen: line.attribute(forKey: "reflen"), @@ -756,7 +845,7 @@ extension RouteMapViewController: NavigationViewDelegate { return (roadName: roadNameRecord.roadName, shieldName: roadNameRecord.shieldName) } - private func roadFeature(for line: MLNMultiPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { + func roadFeature(for line: MLNMultiPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { let roadNameRecord = self.roadFeatureHelper(ref: line.attribute(forKey: "ref"), shield: line.attribute(forKey: "shield"), reflen: line.attribute(forKey: "reflen"), @@ -765,7 +854,7 @@ extension RouteMapViewController: NavigationViewDelegate { return (roadName: roadNameRecord.roadName, shieldName: roadNameRecord.shieldName) } - private func roadFeatureHelper(ref: Any?, shield: Any?, reflen: Any?, name: Any?) -> (roadName: String?, shieldName: NSAttributedString?) { + func roadFeatureHelper(ref: Any?, shield: Any?, reflen: Any?, name: Any?) -> (roadName: String?, shieldName: NSAttributedString?) { var currentShieldName: NSAttributedString?, currentRoadName: String? if let text = ref as? String, let shieldID = shield as? String, let reflenDigit = reflen as? Int { @@ -785,7 +874,7 @@ extension RouteMapViewController: NavigationViewDelegate { return (roadName: currentRoadName, shieldName: currentShieldName) } - private func roadShieldName(for text: String?, shield: String?, reflen: Int?) -> NSAttributedString? { + func roadShieldName(for text: String?, shield: String?, reflen: Int?) -> NSAttributedString? { guard let text, let shield, let reflen else { return nil } let currentShield = HighwayShield.RoadType(rawValue: shield) @@ -800,135 +889,29 @@ extension RouteMapViewController: NavigationViewDelegate { return NSAttributedString(attachment: attachment) } - @objc func updateETA() { - guard isViewLoaded, self.routeController != nil else { return } - self.navigationView.bottomBannerView.updateETA(routeProgress: self.routeController.routeProgress) - } - func resetETATimer() { self.removeTimer() self.updateETATimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(self.updateETA), userInfo: nil, repeats: true) } func showRouteIfNeeded() { - guard isViewLoaded, view.window != nil else { return } + guard self.isViewLoaded, self.view.window != nil else { return } guard !self.mapView.showsRoute else { return } - self.mapView.showRoutes([self.routeController.routeProgress.route], legIndex: self.routeController.routeProgress.legIndex) - self.mapView.showWaypoints(self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex) + guard let routeController else { return } + + self.mapView.showRoutes([routeController.routeProgress.route], legIndex: routeController.routeProgress.legIndex) + self.mapView.showWaypoints(routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex) - if self.routeController.routeProgress.currentLegProgress.stepIndex + 1 <= self.routeController.routeProgress.currentLegProgress.leg.steps.count { - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex, stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) + if routeController.routeProgress.currentLegProgress.stepIndex + 1 <= routeController.routeProgress.currentLegProgress.leg.steps.count { + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) } if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: self.routeController.routeProgress.route) + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } } } -// MARK: StepsViewControllerDelegate - -extension RouteMapViewController: StepsViewControllerDelegate { - func stepsViewController(_ viewController: StepsViewController, didSelect legIndex: Int, stepIndex: Int, cell: StepTableViewCell) { - let legProgress = RouteLegProgress(leg: routeController.routeProgress.route.legs[legIndex], stepIndex: stepIndex) - let step = legProgress.currentStep - guard let upcomingStep = legProgress.upComingStep else { return } - - viewController.dismiss { - self.addPreviewInstructions(step: step, maneuverStep: upcomingStep, distance: cell.instructionsView.distance) - self.stepsViewController = nil - } - - self.mapView.enableFrameByFrameCourseViewTracking(for: 1) - self.mapView.tracksUserCourse = false - self.mapView.setCenter(upcomingStep.maneuverLocation, zoomLevel: self.mapView.zoomLevel, direction: upcomingStep.initialHeading!, animated: true, completionHandler: nil) - - guard isViewLoaded, view.window != nil else { return } - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: legIndex, stepIndex: stepIndex + 1) - } - - func addPreviewInstructions(step: RouteStep, maneuverStep: RouteStep, distance: CLLocationDistance?) { - self.removePreviewInstructions() - - guard let instructions = step.instructionsDisplayedAlongStep?.last else { return } - - let instructionsView = StepInstructionsView(frame: navigationView.instructionsBannerView.frame) - instructionsView.backgroundColor = StepInstructionsView.appearance().backgroundColor - instructionsView.delegate = self - instructionsView.distance = distance - - self.navigationView.instructionsBannerContentView.backgroundColor = instructionsView.backgroundColor - - view.addSubview(instructionsView) - instructionsView.update(for: instructions) - self.previewInstructionsView = instructionsView - } - - func didDismissStepsViewController(_ viewController: StepsViewController) { - viewController.dismiss { - self.stepsViewController = nil - self.navigationView.instructionsBannerView.stepListIndicatorView.isHidden = false - } - } - - func statusView(_ statusView: StatusView, valueChangedTo value: Double) { - let displayValue = 1 + min(Int(9 * value), 8) - let title = String.Localized.simulationStatus(speed: displayValue) - self.showStatus(title: title, for: .infinity, interactive: true) - - if let locationManager = routeController.locationManager as? SimulatedLocationManager { - locationManager.speedMultiplier = Double(displayValue) - } - } -} - -// MARK: - Keyboard Handling - -private extension RouteMapViewController { - func subscribeToKeyboardNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - func unsubscribeFromKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @objc func keyboardWillShow(notification: NSNotification) { - guard self.navigationView.endOfRouteView != nil else { return } - guard let userInfo = notification.userInfo else { return } - guard let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int else { return } - guard let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return } - guard let keyBoardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } - - let curve = UIView.AnimationCurve(rawValue: curveValue) ?? UIView.AnimationCurve.easeIn - let options = (duration: duration, curve: curve) - let keyboardHeight = keyBoardRect.size.height - - if #available(iOS 11.0, *) { - navigationView.endOfRouteShowConstraint?.constant = -1 * (keyboardHeight - view.safeAreaInsets.bottom) // subtract the safe area, which is part of the keyboard's frame - } else { - self.navigationView.endOfRouteShowConstraint?.constant = -1 * keyboardHeight - } - - let opts = UIView.AnimationOptions(curve: options.curve) - UIView.animate(withDuration: options.duration, delay: 0, options: opts, animations: view.layoutIfNeeded, completion: nil) - } - - @objc func keyboardWillHide(notification: NSNotification) { - guard self.navigationView.endOfRouteView != nil else { return } - guard let userInfo = notification.userInfo else { return } - let curve = UIView.AnimationCurve(rawValue: userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int) - let options = (duration: userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double, - curve: UIView.AnimationOptions(curve: curve!)) - - self.navigationView.endOfRouteShowConstraint?.constant = 0 - - UIView.animate(withDuration: options.duration, delay: 0, options: options.curve, animations: view.layoutIfNeeded, completion: nil) - } -} - private extension UIView.AnimationOptions { init(curve: UIView.AnimationCurve) { switch curve { @@ -945,19 +928,3 @@ private extension UIView.AnimationOptions { } } } - -@objc protocol RouteMapViewControllerDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, VisualInstructionDelegate { - func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) - func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool - - /** - Called to allow the delegate to customize the contents of the road name label that is displayed towards the bottom of the map view. - - This method is called on each location update. By default, the label displays the name of the road the user is currently traveling on. - - - parameter mapViewController: The route map view controller that will display the road name. - - parameter location: The user’s current location. - - return: The road name to display in the label, or the empty string to hide the label, or nil to query the map’s vector tiles for the road name. - */ - @objc func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? -} diff --git a/MapboxNavigation/RouteVoiceController.swift b/MapboxNavigation/RouteVoiceController.swift index 121be56d..3b4ce758 100644 --- a/MapboxNavigation/RouteVoiceController.swift +++ b/MapboxNavigation/RouteVoiceController.swift @@ -212,7 +212,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate { utterance = AVSpeechUtterance(string: modifiedInstruction.text) utterance.voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) } else { - if #available(iOS 10.0, *), !ignoreProgress, let routeProgress { + if !ignoreProgress, let routeProgress { utterance = AVSpeechUtterance(attributedString: modifiedInstruction.attributedText(for: routeProgress.currentLegProgress)) } else { utterance = AVSpeechUtterance(string: modifiedInstruction.text) diff --git a/MapboxNavigation/StatusView.swift b/MapboxNavigation/StatusView.swift index 2f01501d..4eea28d7 100644 --- a/MapboxNavigation/StatusView.swift +++ b/MapboxNavigation/StatusView.swift @@ -59,11 +59,11 @@ public class StatusView: UIView { let heightConstraint = heightAnchor.constraint(equalToConstant: 30) heightConstraint.priority = UILayoutPriority(rawValue: 999) heightConstraint.isActive = true - textLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - textLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + textLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + textLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true - activityIndicatorView.rightAnchor.constraint(equalTo: safeRightAnchor, constant: -10).isActive = true - activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + activityIndicatorView.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true + activityIndicatorView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true let recognizer = UIPanGestureRecognizer(target: self, action: #selector(StatusView.pan(_:))) addGestureRecognizer(recognizer) diff --git a/MapboxNavigation/StepsViewController.swift b/MapboxNavigation/StepsViewController.swift index 53977b65..003f8294 100644 --- a/MapboxNavigation/StepsViewController.swift +++ b/MapboxNavigation/StepsViewController.swift @@ -112,26 +112,26 @@ public class StepsViewController: UIViewController { } func setupViews() { - view.translatesAutoresizingMaskIntoConstraints = false + self.view.translatesAutoresizingMaskIntoConstraints = false let backgroundView = StepsBackgroundView() backgroundView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(backgroundView) + self.view.addSubview(backgroundView) self.backgroundView = backgroundView - backgroundView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + backgroundView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + backgroundView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + backgroundView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true + backgroundView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true - let tableView = UITableView(frame: view.bounds, style: .plain) + let tableView = UITableView(frame: self.view.bounds, style: .plain) tableView.separatorColor = .clear tableView.backgroundColor = .clear tableView.backgroundView = nil tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self tableView.dataSource = self - view.addSubview(tableView) + self.view.addSubview(tableView) self.tableView = tableView let dismissButton = DismissButton(type: .custom) @@ -139,13 +139,13 @@ public class StepsViewController: UIViewController { let title = NSLocalizedString("DISMISS_STEPS_TITLE", bundle: .mapboxNavigation, value: "Close", comment: "Dismiss button title on the steps view") dismissButton.setTitle(title, for: .normal) dismissButton.addTarget(self, action: #selector(StepsViewController.tappedDismiss(_:)), for: .touchUpInside) - view.addSubview(dismissButton) + self.view.addSubview(dismissButton) self.dismissButton = dismissButton let bottomView = UIView() bottomView.translatesAutoresizingMaskIntoConstraints = false bottomView.backgroundColor = DismissButton.appearance().backgroundColor - view.addSubview(bottomView) + self.view.addSubview(bottomView) self.bottomView = bottomView let separatorBottomView = SeparatorView() @@ -154,24 +154,24 @@ public class StepsViewController: UIViewController { self.separatorBottomView = separatorBottomView dismissButton.heightAnchor.constraint(equalToConstant: 54).isActive = true - dismissButton.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - dismissButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true - dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + dismissButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + dismissButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true + dismissButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true bottomView.topAnchor.constraint(equalTo: dismissButton.bottomAnchor).isActive = true - bottomView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - bottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - bottomView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + bottomView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true + bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true separatorBottomView.topAnchor.constraint(equalTo: dismissButton.topAnchor).isActive = true separatorBottomView.leadingAnchor.constraint(equalTo: dismissButton.leadingAnchor).isActive = true separatorBottomView.trailingAnchor.constraint(equalTo: dismissButton.trailingAnchor).isActive = true separatorBottomView.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true - tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + tableView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true tableView.bottomAnchor.constraint(equalTo: dismissButton.topAnchor).isActive = true - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true tableView.register(StepTableViewCell.self, forCellReuseIdentifier: self.cellId) } diff --git a/MapboxNavigation/UIView.swift b/MapboxNavigation/UIView.swift index 92001615..91512381 100644 --- a/MapboxNavigation/UIView.swift +++ b/MapboxNavigation/UIView.swift @@ -89,53 +89,6 @@ extension UIView { return view } - var safeArea: UIEdgeInsets { - guard #available(iOS 11.0, *) else { return .zero } - return safeAreaInsets - } - - var safeTopAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.topAnchor - } - return topAnchor - } - - var safeLeftAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.leftAnchor - } - return leftAnchor - } - - var safeLeadingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.leadingAnchor - } - return leadingAnchor - } - - var safeBottomAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.bottomAnchor - } - return bottomAnchor - } - - var safeRightAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.rightAnchor - } - return rightAnchor - } - - var safeTrailingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.trailingAnchor - } - return trailingAnchor - } - var imageRepresentation: UIImage? { let size = CGSize(width: frame.size.width, height: frame.size.height) UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) diff --git a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift index a9d1e9ec..b6ec64ea 100644 --- a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift @@ -16,12 +16,12 @@ class NavigationViewControllerTests: XCTestCase { lazy var dependencies: (navigationViewController: NavigationViewController, startLocation: CLLocation, poi: [CLLocation], endLocation: CLLocation, voice: RouteVoiceController) = { let voice = FakeVoiceController() - let nav = NavigationViewController(for: initialRoute, - dayStyle: DayStyle(demoStyle: ()), + let nav = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), directions: Directions(accessToken: "garbage", host: nil), voiceController: voice) nav.delegate = self + nav.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = nav.routeController! let firstCoord = routeController.routeProgress.currentLegProgress.nearbyCoordinates.first! @@ -82,17 +82,17 @@ class NavigationViewControllerTests: XCTestCase { routeController.locationManager(routeController.locationManager, didUpdateLocations: [taylorStreetLocation]) - let wayNameView = (navigationViewController.mapViewController?.navigationView.wayNameView)! + let wayNameView = navigationViewController.mapViewController.navigationView.wayNameView let currentRoadName = wayNameView.text XCTAssertEqual(currentRoadName, roadName, "Expected: \(roadName); Actual: \(String(describing: currentRoadName))") XCTAssertFalse(wayNameView.isHidden, "WayNameView should be visible.") } func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceMoreThanOnceWithOneStyle() { - let navigationViewController = NavigationViewController(for: initialRoute, - dayStyle: DayStyle(demoStyle: ()), + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = navigationViewController.routeController! navigationViewController.styleManager.delegate = self @@ -108,7 +108,8 @@ class NavigationViewControllerTests: XCTestCase { // If tunnel flags are enabled and we need to switch styles, we should not force refresh the map style because we have only 1 style. func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceWhenOnlyOneStyle() { - let navigationViewController = NavigationViewController(for: initialRoute, dayStyle: DayStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = navigationViewController.routeController! navigationViewController.styleManager.delegate = self @@ -123,7 +124,8 @@ class NavigationViewControllerTests: XCTestCase { } func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceMoreThanOnceWithTwoStyles() { - let navigationViewController = NavigationViewController(for: initialRoute, dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = navigationViewController.routeController! navigationViewController.styleManager.delegate = self @@ -150,7 +152,7 @@ class NavigationViewControllerTests: XCTestCase { routeController.locationManager(routeController.locationManager, didUpdateLocations: [turkStreetLocation]) - let wayNameView = (navigationViewController.mapViewController?.navigationView.wayNameView)! + let wayNameView = navigationViewController.mapViewController.navigationView.wayNameView let currentRoadName = wayNameView.text XCTAssertEqual(currentRoadName, roadName, "Expected: \(roadName); Actual: \(String(describing: currentRoadName))") XCTAssertTrue(wayNameView.isHidden, "WayNameView should be hidden.") @@ -161,14 +163,14 @@ class NavigationViewControllerTests: XCTestCase { // We break the communication between CLLocation and MBRouteController // Intent: Prevent the routecontroller from being fed real location updates - navigationViewController.routeController.locationManager.delegate = nil + navigationViewController.routeController?.locationManager.delegate = nil let routeController = navigationViewController.routeController! // Identify a location without a custom road name. let fultonStreetLocation = self.dependencies.poi[2] - navigationViewController.mapViewController!.labelRoadNameCompletionHandler = { defaultRaodNameAssigned in + navigationViewController.mapViewController.labelRoadNameCompletionHandler = { defaultRaodNameAssigned in XCTAssertTrue(defaultRaodNameAssigned, "label road name was not successfully set") } @@ -181,13 +183,13 @@ class NavigationViewControllerTests: XCTestCase { // wait for the style to load -- routes won't show without it. wait(for: [styleLoaded], timeout: 5) - navigationViewController.route = self.initialRoute + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) runUntil { - !(navigationViewController.mapView!.annotations?.isEmpty ?? true) + !(navigationViewController.mapView.annotations?.isEmpty ?? true) } - guard let annotations = navigationViewController.mapView?.annotations else { return XCTFail("Annotations not found.") } + guard let annotations = navigationViewController.mapView.annotations else { return XCTFail("Annotations not found.") } let firstDestination = self.initialRoute.routeOptions.waypoints.last!.coordinate let destinations = annotations.filter(self.annotationFilter(matching: firstDestination)) @@ -196,7 +198,7 @@ class NavigationViewControllerTests: XCTestCase { // lets set the second route navigationViewController.route = self.newRoute - guard let newAnnotations = navigationViewController.mapView?.annotations else { return XCTFail("New annotations not found.") } + guard let newAnnotations = navigationViewController.mapView.annotations else { return XCTFail("New annotations not found.") } let secondDestination = self.newRoute.routeOptions.waypoints.last!.coordinate // do we have a destination on the second route? @@ -257,7 +259,8 @@ class NavigationViewControllerTestable: NavigationViewController { dayStyle: Style, styleLoaded: XCTestExpectation) { self.styleLoadedExpectation = styleLoaded - super.init(for: route, dayStyle: dayStyle, directions: Directions(accessToken: "abc", host: ""), voiceController: FakeVoiceController()) + super.init(dayStyle: dayStyle, directions: Directions(accessToken: "abc", host: ""), voiceController: FakeVoiceController()) + self.startNavigation(with: route) } @objc(initWithRoute:dayStyle:nightStyle:directions:routeController:locationManager:voiceController:) @@ -265,13 +268,13 @@ class NavigationViewControllerTestable: NavigationViewController { fatalError("init(for:directions:dayStyle:nightStyle:routeController:locationManager:voiceController:) has not been implemented") } - func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { + override func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { self.styleLoadedExpectation.fulfill() } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("This initalizer is not supported in this testing subclass.") + + @objc(initWithDayStyle:nightStyle:directions:voiceController:) + required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, voiceController: RouteVoiceController = RouteVoiceController()) { + fatalError("init(dayStyle:nightStyle:directions:voiceController:) has not been implemented") } } diff --git a/README.md b/README.md index c7cb5fd8..69afaeb1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,51 @@ All issues are covered with this SDK. - Transitioned from the [Mapbox SDK](https://github.com/mapbox/mapbox-gl-native-ios) (version 4.3) to [Maplibre Maps SDK](https://github.com/maplibre/maplibre-gl-native) (version 6.0.0) - Added optional config parameter in NavigationMapView constructor to customize certain properties like route line color +# Migrating from v2 to v3 + +MaplibreNavigation v3 allows you to start a navigation in an existing Map, so no modal ViewController needs to be presented over an existing map as before. This results in some breaking changes. + +### Step 1: + +Replace your ViewController that hosts the mapView with a `NavigationViewController`. We suggest to create a subclass of `NavigationViewController` and override init & call `super.init(dayStyle:)` or `super.init(dayStyleURL:)`. With v3, NavigationViewController will not start navigation until you request it. + +### Step 2: + +Start the navigation by calling `startNavigation(with: route)`. If you want to simulate your route, you need to pass in the optional locationManager parameter, otherwise the real location of the device will be used. + +```swift +#if targetEnvironment(simulator) + let locationManager = SimulatedLocationManager(route: route) + locationManager.speedMultiplier = 2 + self.startNavigation(with: route, locationManager: locationManager) +#else + self.startNavigation(with: route) +#endif +``` + +### Step 3: + +Make your ViewController conform to `NavigationViewControllerDelegate` and implement `navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController)` to transition back to normal map mode. This delegate is called when the user arrives at the destination or cancels the navigation. + +```swift +extension SceneDelegate: NavigationViewControllerDelegate { + func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) { + navigationViewController.endNavigation() + } +} +``` + +NOTE: You've probably used this function previously, it was removed as we don't dismiss a ViewController anymore. + +```swift +@objc optional func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) +``` + +### Backwards compatibility + +If you want to keep the old way of presenting a navigation modally, you can still do that. Simply call `startNavigation(with: route)` right after creating the `NavigationViewController`. + + # Getting Started If you are looking to include this inside your project, you have to follow the the following steps: @@ -42,114 +87,15 @@ Install this package using the [Swift Package Manager](https://www.swift.org/doc - **Have a bug to report?** [Open an issue](https://github.com/maplibre/maplibre-navigation-ios/issues). If possible, include the version of Maplibre Services, a full log, and a project that shows the issue. - **Have a feature request?** [Open an issue](https://github.com/maplibre/maplibre-navigation-ios/issues/new). Tell us what the feature should do and why you want the feature. -## Sample code +## Sample code -A demo app is currently not available. Please check the Mapbox repository or documentation for examples, especially on the forked version. You can try the provided demo app, which you need to first run `carthage update --platform iOS --use-xcframeworks` for in the root of this project. +We do provide a limited example app but its not functional right out of the box. -In order to see the map or calculate a route you need your own Maptile and Direction services. +1. Open Secrets.xcconfig and add a mapbox api token. This is needed to obtain a route. A free mapbox account is enough for evaluation. +2. You need a maptile source. The example uses a demo source which only displays country borders. +3. Tap the play button to start a navigation -Use the following code as inspiration: - -``` -import MapLibre -import MapboxDirections -import MapboxCoreNavigation -import MapboxNavigation - -class ViewController: UIViewController { - var navigationView: NavigationMapView? - - // Keep `RouteController` in memory (class scope), - // otherwise location updates won't be triggered - public var mapboxRouteController: RouteController? - - override func viewDidLoad() { - super.viewDidLoad() - - let navigationView = NavigationMapView( - frame: .zero, - // Tile loading can take a while - styleURL: URL(string: "your style URL here"), - config: MNConfig()) - self.navigationView = navigationView - view.addSubview(navigationView) - navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - - let waypoints = [ - CLLocation(latitude: 52.032407, longitude: 5.580310), - CLLocation(latitude: 51.768686, longitude: 4.6827956) - ].map { Waypoint(location: $0) } - - let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) - options.shapeFormat = .polyline6 - options.distanceMeasurementSystem = .metric - options.attributeOptions = [] - - print("[\(type(of:self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") - - /// URL is based on the base URL in the Info.plist called `MGLMapboxAPIBaseURL` - /// - Note: Your routing provider could be strict about the user-agent of this app before allowing the call to work - Directions.shared.calculate(options) { (waypoints, routes, error) in - guard let route = routes?.first else { return } - - let simulatedLocationManager = SimulatedLocationManager(route: route) - simulatedLocationManager.speedMultiplier = 20 - - let mapboxRouteController = RouteController( - along: route, - directions: Directions.shared, - locationManager: simulatedLocationManager) - self.mapboxRouteController = mapboxRouteController - mapboxRouteController.delegate = self - mapboxRouteController.resume() - - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassVisualInstructionPoint(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassSpokenInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: nil) - - navigationView.showRoutes([route], legIndex: 0) - } - } -} - -// MARK: - RouteControllerDelegate - -extension ViewController: RouteControllerDelegate { - @objc public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { - let camera = MLNMapCamera( - lookingAtCenter: locations.first!.coordinate, - acrossDistance: 500, - pitch: 0, - heading: 0 - ) - - navigationView?.setCamera(camera, animated: true) - } - - @objc func didPassVisualInstructionPoint(notification: NSNotification) { - guard let currentVisualInstruction = currentStepProgress(from: notification)?.currentVisualInstruction else { return } - - print(String( - format: "didPassVisualInstructionPoint primary text: %@ and secondary text: %@", - String(describing: currentVisualInstruction.primaryInstruction.text), - String(describing: currentVisualInstruction.secondaryInstruction?.text))) - } - - @objc func didPassSpokenInstructionPoint(notification: NSNotification) { - guard let currentSpokenInstruction = currentStepProgress(from: notification)?.currentSpokenInstruction else { return } - - print("didPassSpokenInstructionPoint text: \(currentSpokenInstruction.text)") - } - - private func currentStepProgress(from notification: NSNotification) -> RouteStepProgress? { - let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress - return routeProgress?.currentLegProgress.currentStepProgress - } -} -``` +[![MapLibre Logo](.github/navigation.png)](https://maplibre.org) ## Community @@ -161,4 +107,4 @@ Read the [CONTRIBUTING.md](CONTRIBUTING.md) guide in order to get familiar with Code is [licensed](LICENSE.md) under MIT and ISC. ISC is meant to be functionally equivalent to the MIT license. -Copyright (c) 2022 MapLibre contributors +Copyright (c) 2022 MapLibre contributors \ No newline at end of file