diff --git a/CHANGELOG.md b/CHANGELOG.md index 237f1a686..f7336034f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Guide: https://keepachangelog.com/en/1.0.0/ +- [Offline] Add public `AbstractSearchEngine.offlineEngineReady` to mark when offline searches are ready. + - [Demo] Add OfflineDemoViewController to MapboxSearch.xcodeproj > Demo application. - [Demo] Remove support for `--offline` launch argument. diff --git a/MapboxSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MapboxSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index c3e68929e..000000000 --- a/MapboxSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,32 +0,0 @@ -{ - "pins" : [ - { - "identity" : "cwlcatchexception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlCatchException.git", - "state" : { - "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", - "version" : "2.1.2" - } - }, - { - "identity" : "cwlpreconditiontesting", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state" : { - "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", - "version" : "2.1.2" - } - }, - { - "identity" : "swifter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter", - "state" : { - "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version" : "1.5.0" - } - } - ], - "version" : 2 -} diff --git a/Sources/MapboxSearch/PublicAPI/Engine/AbstractSearchEngine.swift b/Sources/MapboxSearch/PublicAPI/Engine/AbstractSearchEngine.swift index 88bc571fa..04ebd4843 100644 --- a/Sources/MapboxSearch/PublicAPI/Engine/AbstractSearchEngine.swift +++ b/Sources/MapboxSearch/PublicAPI/Engine/AbstractSearchEngine.swift @@ -32,6 +32,9 @@ public class AbstractSearchEngine: FeedbackManagerDelegate { /// `OfflineManager` with `default` TileStore. public private(set) var offlineManager: SearchOfflineManager + /// Block offline searches until the tileset is ready + public var offlineEngineReady = false + // Manager to send raw events to Mapbox Telemetry let eventsManager: EventsManager diff --git a/Sources/MapboxSearch/PublicAPI/Engine/SearchEngine.swift b/Sources/MapboxSearch/PublicAPI/Engine/SearchEngine.swift index d8db6b1da..e478a553a 100644 --- a/Sources/MapboxSearch/PublicAPI/Engine/SearchEngine.swift +++ b/Sources/MapboxSearch/PublicAPI/Engine/SearchEngine.swift @@ -143,6 +143,7 @@ public class SearchEngine: AbstractSearchEngine { case .enabled: offlineManager.registerCurrentTileStore { [weak self] in self?.offlineMode = mode + self?.offlineEngineReady = true completion?() } case .disabled: @@ -195,18 +196,6 @@ public class SearchEngine: AbstractSearchEngine { override var dataResolvers: [IndexableDataResolver] { super.dataResolvers + [self] } - var engineSearchFunction: (String, [String], CoreSearchOptions, @escaping (CoreSearchResponseProtocol?) -> Void) - -> Void - { - offlineMode == .disabled ? engine.search : engine.searchOffline - } - - var engineReverseGeocodingFunction: (CoreReverseGeoOptions, @escaping (CoreSearchResponseProtocol?) -> Void) - -> Void - { - offlineMode == .disabled ? engine.reverseGeocoding : engine.reverseGeocodingOffline - } - func retrieve(suggestion: SearchSuggestion) { guard let responseProvider = suggestion as? CoreResponseProvider else { assertionFailure() @@ -230,9 +219,22 @@ public class SearchEngine: AbstractSearchEngine { } let options = options?.merged(defaultSearchOptions) ?? defaultSearchOptions + let coreOptions = options.toCore(apiType: engineApi) - engineSearchFunction(queryValueString, [], options.toCore(apiType: engineApi)) { [weak self] response in - self?.processResponse(response, suggestion: nil) + if offlineMode == .enabled { + guard offlineEngineReady else { + assertionFailure("Attempted offline search before engine was ready") + return + } + + engine + .searchOffline(query: queryValueString, categories: [], options: coreOptions) { [weak self] response in + self?.processResponse(response, suggestion: nil) + } + } else { + engine.search(forQuery: queryValueString, categories: [], options: coreOptions) { [weak self] response in + self?.processResponse(response, suggestion: nil) + } } } @@ -465,7 +467,8 @@ extension SearchEngine { userActivityReporter.reportActivity(forComponent: "search-engine-reverse-geocoding") } - engineReverseGeocodingFunction(options.toCore()) { [weak self] response in + let coreOptions = options.toCore() + let searchCompletionBlock: ((CoreSearchResponseProtocol?) -> Void) = { [weak self] response in guard let self else { assertionFailure("Owning object was deallocated") return @@ -495,6 +498,17 @@ extension SearchEngine { completion(.failure(wrappedError)) } } + + if offlineMode == .enabled { + guard offlineEngineReady else { + assertionFailure("Attempted offline search before engine was ready") + return + } + + engine.reverseGeocodingOffline(for: coreOptions, completion: searchCompletionBlock) + } else { + engine.reverseGeocoding(for: coreOptions, completion: searchCompletionBlock) + } } } diff --git a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift index 52fad5780..0f9ad9471 100644 --- a/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift +++ b/Sources/MapboxSearch/PublicAPI/Offline/SearchOfflineManager.swift @@ -15,6 +15,8 @@ public class SearchOfflineManager { self.tileStore = tileStore } + // MARK: - Tile Store setup + func registerCurrentTileStore(completion: (() -> Void)?) { setTileStore(tileStore, completion: completion) } diff --git a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift index b705f2262..1f33e6bcb 100644 --- a/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift +++ b/Tests/MapboxSearchIntegrationTests/OfflineIntegrationTests.swift @@ -1,5 +1,3 @@ -@testable import MapboxSearch - import CoreGraphics import CoreLocation import MapboxCommon @@ -7,7 +5,7 @@ import MapboxCommon import XCTest /// Note: ``OfflineIntegrationTests`` does not use Mocked data. -class OfflineIntegrationTests: MockServerIntegrationTestCase { +class OfflineIntegrationTests: XCTestCase { let delegate = SearchEngineDelegateStub() let searchEngine = SearchEngine() @@ -52,10 +50,22 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { acceptExpired: true )! - let cancelable = searchEngine.offlineManager.tileStore.loadTileRegion(id: regionId, options: options) { _ in - } completion: { result in - completion(result) - } + let cancelable = searchEngine.offlineManager.tileStore + .loadTileRegion(id: regionId, options: options) { progress in +// let sizePercent = CGFloat(progress.loadedResourceSize) / CGFloat(max(1, progress.completedResourceSize)) +// NSLog("@@ progress size: \(sizePercent)% (loaded: \(progress.loadedResourceSize), completed: +// \(progress.completedResourceSize))") + + let countPercent = CGFloat(progress.loadedResourceCount) / CGFloat(max( + 1, + progress.requiredResourceCount + )) + NSLog( + "@@ progress count: \(countPercent)% (loaded: \(progress.loadedResourceCount), required: \(progress.requiredResourceCount))" + ) + } completion: { result in + completion(result) + } return cancelable } @@ -65,22 +75,23 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { // MARK: - Tests - func testLoadData() throws { + func testLoadDataOfflineSearch() 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) + let booleanIsTruePredicate = NSPredicate(block: { input, _ in + guard let input = input as? Bool else { + return false + } + return input == true }) - searchEngine.offlineManager.engine.addOfflineIndexObserver(for: offlineIndexObserver) + + let engineReadyExpectation = expectation( + for: booleanIsTruePredicate, + evaluatedWith: searchEngine.offlineEngineReady + ) // Perform the offline fetch - let loadDataExpectation = expectation(description: "Load Data") + let loadDataExpectation = XCTestExpectation(description: "Load Data") _ = loadData { result in switch result { case .success(let region): @@ -93,7 +104,7 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { loadDataExpectation.fulfill() } wait( - for: [loadDataExpectation, indexChangedExpectation], + for: [engineReadyExpectation, loadDataExpectation], timeout: 200, enforceOrder: true ) @@ -109,19 +120,66 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { XCTAssertFalse(searchEngine.suggestions.isEmpty) } + func testLoadDataReverseGeocodingOffline() throws { + clearData() + + let booleanIsTruePredicate = NSPredicate(block: { input, _ in + guard let input = input as? Bool else { + return false + } + return input == true + }) + + let engineReadyExpectation = expectation( + for: booleanIsTruePredicate, + evaluatedWith: searchEngine.offlineEngineReady + ) + + // Perform the offline fetch + let loadDataExpectation = XCTestExpectation(description: "Load Data") + _ = loadData { 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: [engineReadyExpectation, loadDataExpectation], + timeout: 200, + enforceOrder: true + ) + + let options = ReverseGeocodingOptions(point: dcLocation, languages: ["en"]) + searchEngine.reverse(options: options) { result in + switch result { + case .success(let success): + XCTAssertFalse(success.isEmpty) + case .failure(let failure): + XCTFail(failure.localizedDescription) + } + } + } + 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) + let booleanIsTruePredicate = NSPredicate(block: { input, _ in + guard let input = input as? Bool else { + return false + } + return input == true }) - searchEngine.offlineManager.engine.addOfflineIndexObserver(for: offlineIndexObserver) + + // Set up index observer before the fetch starts to validate changes after it completes + let engineReadyExpectation = expectation( + for: booleanIsTruePredicate, + evaluatedWith: searchEngine.offlineEngineReady + ) // Perform the offline fetch let spanishTileset = SearchOfflineManager.createTilesetDescriptor( @@ -141,14 +199,14 @@ class OfflineIntegrationTests: MockServerIntegrationTestCase { loadDataExpectation.fulfill() } wait( - for: [loadDataExpectation, indexChangedExpectation], + for: [engineReadyExpectation, loadDataExpectation], timeout: 200, enforceOrder: true ) let offlineUpdateExpectation = delegate.offlineUpdateExpectation - searchEngine.search(query: "café") - wait(for: [offlineUpdateExpectation], timeout: 10) + searchEngine.search(query: "cafe") + wait(for: [offlineUpdateExpectation], timeout: 100) XCTAssertNil(delegate.error) XCTAssertNil(delegate.error?.localizedDescription)