From aa07728f030649555fa3df3f73a167cf59819aba Mon Sep 17 00:00:00 2001 From: Jack Alto <384288+aokj4ck@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:05:19 -0400 Subject: [PATCH 1/3] Add OfflineIndexObserver for insight to Offline state (#206) ### Description - Update tests to expect offline index observer events - Update changelog - Add debug description strings for CoreOfflineIndexChangeEventType ### Checklist - [x] Update `CHANGELOG` --- CHANGELOG.md | 2 ++ MapboxSearch.xcodeproj/project.pbxproj | 16 ++++++++++ .../xcshareddata/xcschemes/Demo.xcscheme | 2 +- .../InternalAPI/CoreAliases.swift | 6 ++++ .../CoreSearchEngineProtocol.swift | 4 +++ .../CoreOfflineIndexChangeEventType.swift | 18 ++++++++++++ .../PublicAPI/DefaultLocationProvider.swift | 3 +- .../Offline/OfflineIndexObserver.swift | 29 +++++++++++++++++++ .../OfflineIntegrationTests.swift | 13 +++++++++ .../Stubs&Models/CoreSearchEngineStub.swift | 4 +++ 10 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 Sources/MapboxSearch/InternalAPI/Offline/CoreOfflineIndexChangeEventType.swift create mode 100644 Sources/MapboxSearch/PublicAPI/Offline/OfflineIndexObserver.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 895fea231..863458b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Guide: https://keepachangelog.com/en/1.0.0/ +- [Offline] Added OfflineIndexObserver which accepts two blocks for indexChanged or error events. This can be assigned to the offline search engine to receive state updates. + ## 2.0.0-rc.3 - [Core] Add `SearchResultAccuracy.proximate` case which "is a known address point but does not intersect a known rooftop/parcel." diff --git a/MapboxSearch.xcodeproj/project.pbxproj b/MapboxSearch.xcodeproj/project.pbxproj index 59c762adb..b65450aaf 100644 --- a/MapboxSearch.xcodeproj/project.pbxproj +++ b/MapboxSearch.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 0477904D2B890F4E00A99528 /* search-box-retrieve-minsk.json in Resources */ = {isa = PBXBuildFile; fileRef = 0477904C2B890F4E00A99528 /* search-box-retrieve-minsk.json */; }; 0477904E2B890F4E00A99528 /* search-box-retrieve-minsk.json in Resources */ = {isa = PBXBuildFile; fileRef = 0477904C2B890F4E00A99528 /* search-box-retrieve-minsk.json */; }; 0477904F2B890F4E00A99528 /* search-box-retrieve-minsk.json in Resources */ = {isa = PBXBuildFile; fileRef = 0477904C2B890F4E00A99528 /* search-box-retrieve-minsk.json */; }; + 0484BCDF2BC4865C003CF408 /* OfflineIndexObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484BCDE2BC4865C003CF408 /* OfflineIndexObserver.swift */; }; + 0484BCE22BC49A23003CF408 /* CoreOfflineIndexChangeEventType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484BCE12BC49A23003CF408 /* CoreOfflineIndexChangeEventType.swift */; }; 048823482B6B0A9D00C770AA /* category-hotel-search-along-route-jp.json in Resources */ = {isa = PBXBuildFile; fileRef = 04AB0B7C2B6B043C00FDE7D5 /* category-hotel-search-along-route-jp.json */; }; 048823492B6B0A9D00C770AA /* category-hotel-search-along-route-jp.json in Resources */ = {isa = PBXBuildFile; fileRef = 04AB0B7C2B6B043C00FDE7D5 /* category-hotel-search-along-route-jp.json */; }; 0488234A2B6B0A9E00C770AA /* category-hotel-search-along-route-jp.json in Resources */ = {isa = PBXBuildFile; fileRef = 04AB0B7C2B6B043C00FDE7D5 /* category-hotel-search-along-route-jp.json */; }; @@ -540,6 +542,8 @@ 046818E02B87FF5C0082B188 /* search-box-suggestions-minsk.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-box-suggestions-minsk.json"; sourceTree = ""; }; 047790482B890A8500A99528 /* search-box-recursion.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-box-recursion.json"; sourceTree = ""; }; 0477904C2B890F4E00A99528 /* search-box-retrieve-minsk.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-box-retrieve-minsk.json"; sourceTree = ""; }; + 0484BCDE2BC4865C003CF408 /* OfflineIndexObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineIndexObserver.swift; sourceTree = ""; }; + 0484BCE12BC49A23003CF408 /* CoreOfflineIndexChangeEventType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreOfflineIndexChangeEventType.swift; sourceTree = ""; }; 04970F8C2B7A97C900213763 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 04AB0B4A2B6AADB700FDE7D5 /* mapbox.places.san.francisco.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = mapbox.places.san.francisco.json; sourceTree = ""; }; 04AB0B792B6AF37800FDE7D5 /* DiscoverIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverIntegrationTests.swift; sourceTree = ""; }; @@ -1052,6 +1056,14 @@ path = sbs; sourceTree = ""; }; + 0484BCE02BC49A18003CF408 /* Offline */ = { + isa = PBXGroup; + children = ( + 0484BCE12BC49A23003CF408 /* CoreOfflineIndexChangeEventType.swift */, + ); + path = Offline; + sourceTree = ""; + }; 04970F8B2B7A97C900213763 /* Resources */ = { isa = PBXGroup; children = ( @@ -1554,6 +1566,7 @@ F9274FF127394AE600708F37 /* TileRegionError.swift */, F9274FEA2732BF0E00708F37 /* SearchTileStore.swift */, F9274FF6273AAE7200708F37 /* TileRegionLoadOptions+Search.swift */, + 0484BCDE2BC4865C003CF408 /* OfflineIndexObserver.swift */, ); path = Offline; sourceTree = ""; @@ -1731,6 +1744,7 @@ FEEDD2C12508DFE400DC0A98 /* InternalAPI */ = { isa = PBXGroup; children = ( + 0484BCE02BC49A18003CF408 /* Offline */, 04C127562B62FFD000884325 /* Engine */, 148DE66E285777050085684D /* Common */, F98BECBB2577B9150081D3BC /* Telemetry */, @@ -2664,6 +2678,7 @@ F9E8146225418E7D00F6378E /* EventsManager.swift in Sources */, FEEDD3002508DFE400DC0A98 /* CLLocationCoordinate2DCodable.swift in Sources */, FEEDD3042508DFE400DC0A98 /* SearchResultType.swift in Sources */, + 0484BCE22BC49A23003CF408 /* CoreOfflineIndexChangeEventType.swift in Sources */, F91FD66C258CCB41008CB8E1 /* CoreResultType+Extensions.swift in Sources */, FEEDD3152508DFE400DC0A98 /* ServerSearchResult.swift in Sources */, 140E47A2298BC90E00677E30 /* Discover.swift in Sources */, @@ -2671,6 +2686,7 @@ 04C127552B62F6BC00884325 /* ApiType.swift in Sources */, FE059C5A251A0AE6001F7701 /* SearchCategorySuggestionImpl.swift in Sources */, FEEDD2F62508DFE400DC0A98 /* CoreSearchResultResponse.swift in Sources */, + 0484BCDF2BC4865C003CF408 /* OfflineIndexObserver.swift in Sources */, FE260A6725C063880037B725 /* ReverseGeocodingOptions.swift in Sources */, FECA461026D3F81800BC7B18 /* MapboxSearchUserAgent.swift in Sources */, FEEDD2F32508DFE400DC0A98 /* CoreBoundingBox.swift in Sources */, diff --git a/MapboxSearch.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/MapboxSearch.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme index 80438c09f..fe0ff092e 100644 --- a/MapboxSearch.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme +++ b/MapboxSearch.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -126,7 +126,7 @@ diff --git a/Sources/MapboxSearch/InternalAPI/CoreAliases.swift b/Sources/MapboxSearch/InternalAPI/CoreAliases.swift index ad1aa541b..1c69bb309 100644 --- a/Sources/MapboxSearch/InternalAPI/CoreAliases.swift +++ b/Sources/MapboxSearch/InternalAPI/CoreAliases.swift @@ -33,6 +33,12 @@ typealias CoreUserActivityReporterOptions = MapboxCoreSearch.UserActivityReporte typealias CoreReverseGeoOptions = MapboxCoreSearch.ReverseGeoOptions +// Offline +typealias CoreOfflineIndexObserver = MapboxCoreSearch.OfflineIndexObserver +typealias CoreOfflineIndexChangeEvent = MapboxCoreSearch.OfflineIndexChangeEvent +typealias CoreOfflineIndexError = MapboxCoreSearch.OfflineIndexError +typealias CoreOfflineIndexChangeEventType = MapboxCoreSearch.OfflineIndexChangeEventType + let mapboxCoreSearchErrorDomain = "MapboxCoreSearchErrorDomain" extension CoreSearchEngine { diff --git a/Sources/MapboxSearch/InternalAPI/CoreSearchEngineProtocol.swift b/Sources/MapboxSearch/InternalAPI/CoreSearchEngineProtocol.swift index 9a398a4c3..198432e1f 100644 --- a/Sources/MapboxSearch/InternalAPI/CoreSearchEngineProtocol.swift +++ b/Sources/MapboxSearch/InternalAPI/CoreSearchEngineProtocol.swift @@ -81,6 +81,10 @@ protocol CoreSearchEngineProtocol { func setTileStore(_ tileStore: MapboxCommon.TileStore, completion: (() -> Void)?) func setTileStore(_ tileStore: MapboxCommon.TileStore) + + func addOfflineIndexObserver(for observer: CoreOfflineIndexObserver) + + func removeOfflineIndexObserver(for observer: CoreOfflineIndexObserver) } extension CoreSearchEngine: CoreSearchEngineProtocol { diff --git a/Sources/MapboxSearch/InternalAPI/Offline/CoreOfflineIndexChangeEventType.swift b/Sources/MapboxSearch/InternalAPI/Offline/CoreOfflineIndexChangeEventType.swift new file mode 100644 index 000000000..0be3ffce2 --- /dev/null +++ b/Sources/MapboxSearch/InternalAPI/Offline/CoreOfflineIndexChangeEventType.swift @@ -0,0 +1,18 @@ +// Copyright © 2024 Mapbox. All rights reserved. + +import Foundation + +extension CoreOfflineIndexChangeEventType: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .added: + return "Added" + case .removed: + return "Removed" + case .updated: + return "Updated" + @unknown default: + return "Unknown" + } + } +} diff --git a/Sources/MapboxSearch/PublicAPI/DefaultLocationProvider.swift b/Sources/MapboxSearch/PublicAPI/DefaultLocationProvider.swift index 828a9f200..5abc1a0c1 100644 --- a/Sources/MapboxSearch/PublicAPI/DefaultLocationProvider.swift +++ b/Sources/MapboxSearch/PublicAPI/DefaultLocationProvider.swift @@ -5,8 +5,7 @@ import CoreLocation /// Would not trigger Location Permission dialogs but will retrieve permission changes notification. /// Suitable for `SearchEngine` for providing user location by default without additional efforts. public class DefaultLocationProvider { - // not used anywhere as public - /* public */ let locationManager: CLLocationManager + let locationManager: CLLocationManager #if FAST_LOCATION_CHANGES_TRACKING let locationRequestFrequency: TimeInterval = 1 diff --git a/Sources/MapboxSearch/PublicAPI/Offline/OfflineIndexObserver.swift b/Sources/MapboxSearch/PublicAPI/Offline/OfflineIndexObserver.swift new file mode 100644 index 000000000..a2ea14724 --- /dev/null +++ b/Sources/MapboxSearch/PublicAPI/Offline/OfflineIndexObserver.swift @@ -0,0 +1,29 @@ +// Copyright © 2024 Mapbox. All rights reserved. + +import Foundation + +class OfflineIndexObserver: CoreOfflineIndexObserver { + enum Types { + typealias indexChangedBlock = (CoreOfflineIndexChangeEvent) -> Void + typealias errorBlock = (CoreOfflineIndexError) -> Void + } + + private var onIndexChangedBlock: Types.indexChangedBlock + private var onErrorBlock: Types.errorBlock + + init( + onIndexChangedBlock: @escaping Types.indexChangedBlock = { _ in }, + onErrorBlock: @escaping Types.errorBlock = { _ in } + ) { + self.onIndexChangedBlock = onIndexChangedBlock + self.onErrorBlock = onErrorBlock + } + + func onIndexChanged(for event: CoreOfflineIndexChangeEvent) { + onIndexChangedBlock(event) + } + + func onError(forError error: CoreOfflineIndexError) { + onErrorBlock(error) + } +} diff --git a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift index 1ab179677..1f836bf6c 100644 --- a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift +++ b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift @@ -57,6 +57,18 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase Date: Tue, 9 Apr 2024 14:09:57 -0400 Subject: [PATCH 2/3] [SSDK-617] Fix offline tests (#199) ### Description Fixes [SSDK-617](https://mapbox.atlassian.net/browse/SSDK-617) - [Offline] Change default tileset name to `mbx-main` - [Tests] Fix Offline tests and re-enable. - This fix depends on the earlier PR https://github.com/mapbox/mapbox-search-ios/pull/206 to add OfflineIndexObserver - [Tests] Add `offlineResultsUpdated` delegate function to `SearchEngineDelegateStub`. - [Tests] Demonstrate providing a `Geometry(point: NSValue(mkCoordinate: CLLocationCoordinate2D))` with `TileRegionLoadOptions.build` function. - [Core] Increment minimum MapboxCommon version from 24.0.0 to 24.2.0. ### Checklist - [x] Update `CHANGELOG` --- CHANGELOG.md | 8 ++++ Cartfile | 2 +- Cartfile.resolved | 2 +- Package.swift | 2 +- Sources/Demo/MapRootController.swift | 7 +++- .../Offline/SearchOfflineManager.swift | 2 +- Tests/Demo.xctestplan | 3 +- .../OfflineIntegrationTests.swift | 37 +++++++++++++------ .../SearchEngineDelegateStub.swift | 35 +++++------------- 9 files changed, 54 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 863458b10..8ae47e89d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ Guide: https://keepachangelog.com/en/1.0.0/ - [Offline] Added OfflineIndexObserver which accepts two blocks for indexChanged or error events. This can be assigned to the offline search engine to receive state updates. +- [Offline] Change default tileset name to `mbx-main` +- [Tests] Fix Offline tests and re-enable. +- [Tests] Add `offlineResultsUpdated` delegate function to `SearchEngineDelegateStub`. +- [Tests] Demonstrate providing a `Geometry(point: NSValue(mkCoordinate: CLLocationCoordinate2D))` with `TileRegionLoadOptions.build` function. +- [Core] Increment minimum MapboxCommon version from 24.0.0 to 24.2.0. + +**MapboxCommon**: v24.2.0 + ## 2.0.0-rc.3 - [Core] Add `SearchResultAccuracy.proximate` case which "is a known address point but does not intersect a known rooftop/parcel." diff --git a/Cartfile b/Cartfile index f4021b2b4..530d47dbc 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ binary "https://api.mapbox.com/downloads/v2/carthage/search-core-sdk/MapboxCoreSearch.xcframework.json" == 2.0.0-alpha.14 -binary "https://api.mapbox.com/downloads/v2/carthage/mapbox-common/MapboxCommon.json" == 24.0.0 +binary "https://api.mapbox.com/downloads/v2/carthage/mapbox-common/MapboxCommon.json" == 24.2.0 \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index a1c6204fe..0cf38f59e 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ -binary "https://api.mapbox.com/downloads/v2/carthage/mapbox-common/MapboxCommon.json" "24.0.0" +binary "https://api.mapbox.com/downloads/v2/carthage/mapbox-common/MapboxCommon.json" "24.2.0" binary "https://api.mapbox.com/downloads/v2/carthage/search-core-sdk/MapboxCoreSearch.xcframework.json" "2.0.0-alpha.14" diff --git a/Package.swift b/Package.swift index c2df3da6a..e36ab2f8c 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import Foundation let (coreSearchVersion, coreSearchVersionHash) = ("2.0.0-alpha.14", "c3e61341f2beb1b8043f3c71caccdd9bea12a23f221cb90eb452e2abe299c3e0") -let commonMinVersion = Version("24.0.0") +let commonMinVersion = Version("24.2.0") let commonMaxVersion = Version("25.0.0") let package = Package( diff --git a/Sources/Demo/MapRootController.swift b/Sources/Demo/MapRootController.swift index 1ab8c2a86..38d191188 100644 --- a/Sources/Demo/MapRootController.swift +++ b/Sources/Demo/MapRootController.swift @@ -31,7 +31,12 @@ class MapRootController: UIViewController { engine.setOfflineMode(.enabled) { let descriptor = SearchOfflineManager.createDefaultTilesetDescriptor() - let dcLocation = NSValue(cgPoint: CGPoint(x: 38.89992081005698, y: -77.03399849939174)) + + let dcLocation = NSValue(mkCoordinate: CLLocationCoordinate2D( + latitude: 38.89992081005698, + longitude: -77.03399849939174 + )) + guard let options = MapboxCommon.TileRegionLoadOptions.build( geometry: Geometry(point: dcLocation), descriptors: [descriptor], diff --git a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift index 81353e648..cf8a898cc 100644 --- a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift +++ b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift @@ -2,7 +2,7 @@ import Foundation /// OfflineManager handles `TileStore`s and responsible for creating Search `TilsetDescriptor`s public class SearchOfflineManager { - static let defaultDatasetName = "test-dataset" + static let defaultDatasetName = "mbx-main" var engine: CoreSearchEngineProtocol diff --git a/Tests/Demo.xctestplan b/Tests/Demo.xctestplan index 4adbfc008..7dcacae8b 100644 --- a/Tests/Demo.xctestplan +++ b/Tests/Demo.xctestplan @@ -35,8 +35,7 @@ "testTargets" : [ { "skippedTests" : [ - "MockServerIntegrationTestCase", - "OfflineIntegrationTests" + "MockServerIntegrationTestCase" ], "target" : { "containerPath" : "container:MapboxSearch.xcodeproj", diff --git a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift index 1f836bf6c..200916951 100644 --- a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift +++ b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift @@ -6,14 +6,16 @@ import MapboxCommon @testable import MapboxSearch import XCTest -class OfflineIntegrationTests: MockServerIntegrationTestCase { +/// Note: ``OfflineIntegrationTests`` does not use Mocked data. +class OfflineIntegrationTests: MockServerIntegrationTestCase { let delegate = SearchEngineDelegateStub() let searchEngine = SearchEngine() - let dataset = "test-dataset" - let dcLocation = CGPoint(x: 38.89992081005698, y: -77.03399849939174) + let dcLocation = CLLocationCoordinate2D(latitude: 38.89992081005698, longitude: -77.03399849939174) let regionId = "dc" + // MARK: - Helpers and set up + override func setUpWithError() throws { try super.setUpWithError() @@ -36,13 +38,15 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase) -> Void) -> SearchCancelable { + /// This will use the default dataset defined at ``SearchOfflineManager.defaultDatasetName`` let descriptor = SearchOfflineManager.createDefaultTilesetDescriptor() - let dcLocationValue = NSValue(cgPoint: dcLocation) + let dcLocationValue = NSValue(mkCoordinate: dcLocation) let options = MapboxCommon.TileRegionLoadOptions.build( geometry: Geometry(point: dcLocationValue), descriptors: [descriptor], acceptExpired: true )! + let cancelable = searchEngine.offlineManager.tileStore.loadTileRegion(id: regionId, options: options) { _ in } completion: { result in completion(result) @@ -54,6 +58,8 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase Date: Tue, 9 Apr 2024 17:07:13 -0400 Subject: [PATCH 3/3] [SSDK-622] Add language parameter to offline tileset descriptor creation (#202) ### Description Fixes [SSDK-622](https://mapbox.atlassian.net/browse/SSDK-622) Add optional `language` parameter that has two modes: 1. Absent language parameter / `nil` value will use existing behavior of _just_ the dataset name. 2. A given language parameter will be appended to the dataset name. ### Checklist - [x] Update `CHANGELOG` - [x] Add a non-English language test after #199 is merged --- CHANGELOG.md | 3 + .../InternalAPI/CoreSearchEngineStatics.swift | 57 ++++++++++++++++-- .../Offline/SearchOfflineManager.swift | 37 ++++++++++-- .../OfflineIntegrationTests.swift | 59 ++++++++++++++++++- 4 files changed, 144 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae47e89d..2471ab400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Guide: https://keepachangelog.com/en/1.0.0/ +- [Offline] Add optional `language` parameter to SearchOfflineManager.createTilesetDescriptor and SearchOfflineManager.createPlacesTilesetDescriptor functions. +- [Tests] Add Spanish language offline search test. + - [Offline] Added OfflineIndexObserver which accepts two blocks for indexChanged or error events. This can be assigned to the offline search engine to receive state updates. - [Offline] Change default tileset name to `mbx-main` diff --git a/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift b/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift index 71e4c3ffd..b7b53b270 100644 --- a/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift +++ b/Sources/MapboxSearch/InternalAPI/CoreSearchEngineStatics.swift @@ -1,11 +1,60 @@ import Foundation enum CoreSearchEngineStatics { - static func createTilesetDescriptor(dataset: String, version: String) -> MapboxCommon.TilesetDescriptor { - CoreSearchEngine.createTilesetDescriptor(forDataset: dataset, version: version) + enum Constants { + static let delimiter = "_" } - static func createPlacesTilesetDescriptor(dataset: String, version: String) -> MapboxCommon.TilesetDescriptor { - CoreSearchEngine.createPlacesTilesetDescriptor(forDataset: dataset, version: version) + static func createTilesetDescriptor(dataset: String, version: String, language: String? = nil) -> MapboxCommon + .TilesetDescriptor { + let identifier: String + if let language { + if ISOLanguages.contains(language: language) { + identifier = dataset + Constants.delimiter + language + } else { + _Logger.searchSDK + .warning( + "Provided language code '\(language)' for tileset is non-ISO. Dataset '\(dataset)' without language will be used." + ) + identifier = dataset + } + } else { + identifier = dataset + } + return CoreSearchEngine.createTilesetDescriptor(forDataset: identifier, version: version) + } + + static func createPlacesTilesetDescriptor(dataset: String, version: String, language: String? = nil) -> MapboxCommon + .TilesetDescriptor { + let identifier: String + if let language { + if ISOLanguages.contains(language: language) { + identifier = dataset + Constants.delimiter + language + } else { + _Logger.searchSDK + .warning( + "Provided language code '\(language)' for places tileset is non-ISO. Dataset '\(dataset)' without language will be used." + ) + identifier = dataset + } + } else { + identifier = dataset + } + return CoreSearchEngine.createPlacesTilesetDescriptor(forDataset: identifier, version: version) + } +} + +enum ISOLanguages { + static func contains(language: String) -> Bool { + var validLanguage: Bool + if #available(iOS 16, *) { + validLanguage = Locale.LanguageCode.isoLanguageCodes + .map(\.identifier) + .contains(language) + } else { + validLanguage = Locale.isoLanguageCodes + .contains(language) + } + return validLanguage } } diff --git a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift index cf8a898cc..52fad5780 100644 --- a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift +++ b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift @@ -41,26 +41,53 @@ public class SearchOfflineManager { engine.setTileStore(searchTileStore.commonTileStore, completion: completion) } - /// Creates TilesetDescriptor for offline search index data with provided dataset name and version. + // MARK: - Tileset with name, version, and language parameters + + /// Creates TilesetDescriptor for offline search index data with provided dataset name, version, and language. + /// Providing nil or excluding the language parameter will use the dataset name as-is. + /// Providing a language will append it to the name. /// - Parameters: /// - dataset: dataset name /// - version: dataset version + /// - language: Provide a ISO 639-1 Code language from NSLocale. Values will be appended to the place dataset + /// name. /// - Returns: TilesetDescriptor for TileStore - public static func createTilesetDescriptor(dataset: String, version: String? = nil) -> MapboxCommon + public static func createTilesetDescriptor( + dataset: String, + version: String? = nil, + language: String? = nil + ) -> MapboxCommon .TilesetDescriptor { - CoreSearchEngineStatics.createTilesetDescriptor(dataset: dataset, version: version ?? "") + CoreSearchEngineStatics.createTilesetDescriptor( + dataset: dataset, + version: version ?? "", + language: language + ) } /// Creates TilesetDescriptor for offline search boundaries with provided dataset name and version. + /// Providing nil or excluding the language parameter will use the places dataset name as-is. + /// Providing a language will append it to the name. /// - Parameters: /// - dataset: dataset name /// - version: dataset version + /// - language: Provide a ISO 639-1 Code language from NSLocale. Values will be appended to the dataset name. /// - Returns: TilesetDescriptor for TileStore - public static func createPlacesTilesetDescriptor(dataset: String, version: String? = nil) -> MapboxCommon + public static func createPlacesTilesetDescriptor( + dataset: String, + version: String? = nil, + language: String? = nil + ) -> MapboxCommon .TilesetDescriptor { - CoreSearchEngineStatics.createPlacesTilesetDescriptor(dataset: dataset, version: version ?? "") + CoreSearchEngineStatics.createPlacesTilesetDescriptor( + dataset: dataset, + version: version ?? "", + language: language + ) } + // MARK: - Default tileset + /// Creates TilesetDescriptor for offline search index data using default dataset name. /// - Returns: TilesetDescriptor for TileStore public static func createDefaultTilesetDescriptor() -> MapboxCommon.TilesetDescriptor { diff --git a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift index 200916951..b705f2262 100644 --- a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift +++ b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift @@ -36,10 +36,15 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { wait(for: [setTileStoreExpectation], timeout: 10) } - func loadData(completion: @escaping (Result) -> Void) + func loadData( + tilesetDescriptor: TilesetDescriptor? = nil, + completion: @escaping (Result) -> Void + ) -> SearchCancelable { - /// This will use the default dataset defined at ``SearchOfflineManager.defaultDatasetName`` - let descriptor = SearchOfflineManager.createDefaultTilesetDescriptor() + /// A nil tilesetDescriptor parameter will fallback to the default dataset defined at + /// ``SearchOfflineManager.defaultDatasetName`` + let descriptor = tilesetDescriptor ?? SearchOfflineManager.createDefaultTilesetDescriptor() + let dcLocationValue = NSValue(mkCoordinate: dcLocation) let options = MapboxCommon.TileRegionLoadOptions.build( geometry: Geometry(point: dcLocationValue), @@ -104,6 +109,54 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { XCTAssertFalse(searchEngine.suggestions.isEmpty) } + func testSpanishLanguageSupport() throws { + clearData() + + // Set up index observer before the fetch starts to validate changes after it completes + let indexChangedExpectation = expectation(description: "Received offline index changed event") + let offlineIndexObserver = OfflineIndexObserver(onIndexChangedBlock: { changeEvent in + _Logger.searchSDK.info("Index changed: \(changeEvent)") + indexChangedExpectation.fulfill() + }, onErrorBlock: { error in + _Logger.searchSDK.error("Encountered error in OfflineIndexObserver \(error)") + XCTFail(error.debugDescription) + }) + searchEngine.offlineManager.engine.addOfflineIndexObserver(for: offlineIndexObserver) + + // Perform the offline fetch + let spanishTileset = SearchOfflineManager.createTilesetDescriptor( + dataset: "mbx-main", + language: "es" + ) + let loadDataExpectation = expectation(description: "Load Data") + _ = loadData(tilesetDescriptor: spanishTileset) { result in + switch result { + case .success(let region): + XCTAssert(region.id == self.regionId) + XCTAssert(region.completedResourceCount > 0) + XCTAssertEqual(region.requiredResourceCount, region.completedResourceCount) + case .failure(let error): + XCTFail("Unable to load Region, \(error.localizedDescription)") + } + loadDataExpectation.fulfill() + } + wait( + for: [loadDataExpectation, indexChangedExpectation], + timeout: 200, + enforceOrder: true + ) + + let offlineUpdateExpectation = delegate.offlineUpdateExpectation + searchEngine.search(query: "café") + wait(for: [offlineUpdateExpectation], timeout: 10) + + XCTAssertNil(delegate.error) + XCTAssertNil(delegate.error?.localizedDescription) + XCTAssertNotNil(searchEngine.responseInfo) + XCTAssertFalse(delegate.resolvedResults.isEmpty) + XCTAssertFalse(searchEngine.suggestions.isEmpty) + } + func testNoData() { clearData()