From 54a55c15d48193840a6bf9ada880adb512fce65c Mon Sep 17 00:00:00 2001 From: Abhishek Date: Sat, 6 Feb 2021 22:33:12 -0600 Subject: [PATCH] Add timezones to search results during Onboarding! --- .../OnboardingSearchController.swift | 169 +++++++++++------- .../General/PreferencesViewController.swift | 37 +--- .../General/SearchDataSource.swift | 41 ++++- 3 files changed, 145 insertions(+), 102 deletions(-) diff --git a/Clocker/Onboarding/OnboardingSearchController.swift b/Clocker/Onboarding/OnboardingSearchController.swift index 3f8de824..70bf4047 100644 --- a/Clocker/Onboarding/OnboardingSearchController.swift +++ b/Clocker/Onboarding/OnboardingSearchController.swift @@ -12,7 +12,7 @@ class OnboardingSearchController: NSViewController { @IBOutlet private var accessoryLabel: NSTextField! @IBOutlet var undoButton: NSButton! - private var results: [TimezoneData] = [] + private var searchResultsDataSource: SearchDataSource! private var dataTask: URLSessionDataTask? = .none private var themeDidChangeNotification: NSObjectProtocol? @@ -31,6 +31,9 @@ class OnboardingSearchController: NSViewController { view.wantsLayer = true + searchResultsDataSource = SearchDataSource(with: searchBar, location: .onboarding) + + resultsTableView.isHidden = true resultsTableView.delegate = self resultsTableView.setAccessibility("ResultsTableView") resultsTableView.dataSource = self @@ -68,23 +71,68 @@ class OnboardingSearchController: NSViewController { @objc func doubleClickAction(_: NSTableView?) { [accessoryLabel].forEach { $0?.isHidden = false } - if resultsTableView.selectedRow >= 0, resultsTableView.selectedRow < results.count { - let selectedTimezone = results[resultsTableView.selectedRow] + if resultsTableView.selectedRow >= 0, resultsTableView.selectedRow < searchResultsDataSource.resultsCount() { + let selectedType = searchResultsDataSource.placeForRow(resultsTableView.selectedRow) + switch selectedType { + case .city: + let filteredGoogleResult = searchResultsDataSource.retrieveFilteredResultFromGoogleAPI(resultsTableView.selectedRow) + addTimezoneToDefaults(filteredGoogleResult!) + return + case .timezone: + cleanupAfterInstallingTimezone() + return + } + } + } + + private func cleanupAfterInstallingTimezone() { + let data = TimezoneData() + data.setLabel(CLEmptyString) + + let currentSelection = searchResultsDataSource.retrieveSelectedTimezone(resultsTableView.selectedRow) + + let metaInfo = metadata(for: currentSelection) + data.timezoneID = metaInfo.0.name + data.formattedAddress = metaInfo.1.formattedName + data.selectionType = .timezone + data.isSystemTimezone = metaInfo.0.name == NSTimeZone.system.identifier + + let operationObject = TimezoneDataOperations(with: data) + operationObject.saveObject() + + searchResultsDataSource.cleanupFilterArray() + searchResultsDataSource.timezoneFilteredArray = [] + searchResultsDataSource.calculateChangesets() + searchBar.stringValue = CLEmptyString + + accessoryLabel.stringValue = "Added \(metaInfo.1.formattedName)." + undoButton.isHidden = false + setupLabelHidingTimer() + + resultsTableView.reloadData() + resultsTableView.isHidden = true + } - addTimezoneToDefaults(selectedTimezone) + private func metadata(for selection: TimezoneMetadata) -> (NSTimeZone, TimezoneMetadata) { + if selection.formattedName == "Anywhere on Earth" { + return (NSTimeZone(name: "GMT-1200")!, selection) + } else if selection.formattedName == "UTC" { + return (NSTimeZone(name: "GMT")!, selection) + } else { + return (selection.timezone, selection) } } - private func addTimezoneToDefaults(_ timezone: TimezoneData) { - func setupLabelHidingTimer() { - Timer.scheduledTimer(withTimeInterval: 5, - repeats: false) { _ in - OperationQueue.main.addOperation { - self.accessoryLabel.stringValue = CLEmptyString - } + private func setupLabelHidingTimer() { + Timer.scheduledTimer(withTimeInterval: 5, + repeats: false) { _ in + OperationQueue.main.addOperation { + self.setInfoLabel(CLEmptyString) } } + } + private func addTimezoneToDefaults(_ timezone: TimezoneData) { if resultsTableView.selectedRow == -1 { setInfoLabel(PreferencesConstants.noTimezoneSelectedErrorMessage) setupLabelHidingTimer() @@ -131,22 +179,10 @@ class OnboardingSearchController: NSViewController { return false } - // Extracting this out for tests - private func decodeTimezone(from data: Data) -> Timezone? { - let jsonDecoder = JSONDecoder() - do { - let decodedObject = try jsonDecoder.decode(Timezone.self, from: data) - return decodedObject - } catch { - Logger.info("decodedObject error: \n\(error)") - return nil - } - } - private func fetchTimezone(for latitude: Double, and longitude: Double, _ dataObject: TimezoneData) { if NetworkManager.isConnected() == false || ProcessInfo.processInfo.arguments.contains("mockTimezoneDown") { setInfoLabel(PreferencesConstants.noInternetConnectivityError) - results = [] + searchResultsDataSource.cleanupFilterArray() resultsTableView.reloadData() return } @@ -166,8 +202,8 @@ class OnboardingSearchController: NSViewController { return } - if error == nil, let json = response, let response = self.decodeTimezone(from: json) { - if self.resultsTableView.selectedRow >= 0, self.resultsTableView.selectedRow < self.results.count { + if error == nil, let json = response, let response = json.decodeTimezone() { + if self.resultsTableView.selectedRow >= 0, self.resultsTableView.selectedRow < self.searchResultsDataSource.resultsCount() { var filteredAddress = "Error" if let address = dataObject.formattedAddress { @@ -250,11 +286,6 @@ class OnboardingSearchController: NSViewController { accessoryLabel.isHidden = false - if NetworkManager.isConnected() == false { - setInfoLabel(PreferencesConstants.noInternetConnectivityError) - return - } - NSObject.cancelPreviousPerformRequests(withTarget: self) perform(#selector(OnboardingSearchController.actualSearch), with: nil, afterDelay: 0.2) } @@ -268,6 +299,7 @@ class OnboardingSearchController: NSViewController { @objc func actualSearch() { func setupForError() { + searchResultsDataSource.calculateChangesets() resultsTableView.isHidden = true } @@ -300,11 +332,18 @@ class OnboardingSearchController: NSViewController { return } - self.results = [] + self.searchResultsDataSource.cleanupFilterArray() + self.searchResultsDataSource.timezoneFilteredArray = [] if let errorPresent = error { - self.presentErrorMessage(errorPresent.localizedDescription) - setupForError() + self.findLocalSearchResultsForTimezones() + if self.searchResultsDataSource.timezoneFilteredArray.count == 0 { + self.presentErrorMessage(errorPresent.localizedDescription) + setupForError() + return + } + + self.prepareUIForPresentingResults() return } @@ -314,7 +353,7 @@ class OnboardingSearchController: NSViewController { return } - let searchResults = self.decode(from: data) + let searchResults = data.decode() if searchResults?.status == "ZERO_RESULTS" { self.setInfoLabel("No results! 😔 Try entering the exact name.") @@ -323,10 +362,8 @@ class OnboardingSearchController: NSViewController { } self.appendResultsToFilteredArray(searchResults!.results) - - self.setInfoLabel(CLEmptyString) - - self.resultsTableView.reloadData() + self.findLocalSearchResultsForTimezones() + self.prepareUIForPresentingResults() } }) } @@ -339,12 +376,25 @@ class OnboardingSearchController: NSViewController { } } + private func findLocalSearchResultsForTimezones() { + let lowercasedSearchString = searchBar.stringValue.lowercased() + searchResultsDataSource.searchTimezones(lowercasedSearchString) + } + + private func prepareUIForPresentingResults() { + setInfoLabel(CLEmptyString) + if searchResultsDataSource.calculateChangesets() { + resultsTableView.isHidden = false + resultsTableView.reloadData() + } + } + private func appendResultsToFilteredArray(_ results: [SearchResult.Result]) { - results.forEach { - let location = $0.geometry.location + let finalTimezones: [TimezoneData] = results.map { (result) -> TimezoneData in + let location = result.geometry.location let latitude = location.lat let longitude = location.lng - let formattedAddress = $0.formattedAddress + let formattedAddress = result.formattedAddress let totalPackage = [ "latitude": latitude, @@ -352,27 +402,17 @@ class OnboardingSearchController: NSViewController { CLTimezoneName: formattedAddress, CLCustomLabel: formattedAddress, CLTimezoneID: CLEmptyString, - CLPlaceIdentifier: $0.placeId, + CLPlaceIdentifier: result.placeId, ] as [String: Any] - self.results.append(TimezoneData(with: totalPackage)) + return TimezoneData(with: totalPackage) } - } - // Extracting this out for tests - private func decode(from data: Data) -> SearchResult? { - let jsonDecoder = JSONDecoder() - do { - let decodedObject = try jsonDecoder.decode(SearchResult.self, from: data) - return decodedObject - } catch { - Logger.info("decodedObject error: \n\(error)") - return nil - } + searchResultsDataSource.setFilteredArrayValue(finalTimezones) } private func resetSearchView() { - results = [] + searchResultsDataSource.cleanupFilterArray() resultsTableView.reloadData() searchBar.stringValue = CLEmptyString searchBar.placeholderString = "Press Enter to Search" @@ -386,13 +426,18 @@ class OnboardingSearchController: NSViewController { extension OnboardingSearchController: NSTableViewDataSource { func numberOfRows(in _: NSTableView) -> Int { - return results.count + return searchResultsDataSource.resultsCount() } func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? { - if let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCellView"), owner: self) as? ResultTableViewCell, row >= 0, row < results.count { - let currentTimezone = results[row] - result.result.stringValue = " \(currentTimezone.formattedAddress ?? "Place Name")" + if let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCellView"), owner: self) as? ResultTableViewCell, row >= 0, row < searchResultsDataSource.resultsCount() { + let currentSelection = searchResultsDataSource.retrieveResult(row) + if let timezone = currentSelection as? TimezoneMetadata { + result.result.stringValue = " \(timezone.formattedName)" + } else if let location = currentSelection as? TimezoneData { + result.result.stringValue = " \(location.formattedAddress ?? "Place Name")" + } + result.result.textColor = Themer.shared().mainTextColor() return result } @@ -403,7 +448,7 @@ extension OnboardingSearchController: NSTableViewDataSource { extension OnboardingSearchController: NSTableViewDelegate { func tableView(_: NSTableView, heightOfRow row: Int) -> CGFloat { - if row == 0, results.isEmpty { + if row == 0, searchResultsDataSource.resultsCount() == 0 { return 30 } @@ -411,7 +456,7 @@ extension OnboardingSearchController: NSTableViewDelegate { } func tableView(_: NSTableView, shouldSelectRow row: Int) -> Bool { - return results.isEmpty ? row != 0 : true + return searchResultsDataSource.resultsCount() == 0 ? row != 0 : true } func tableView(_: NSTableView, rowViewForRow _: Int) -> NSTableRowView? { diff --git a/Clocker/Preferences/General/PreferencesViewController.swift b/Clocker/Preferences/General/PreferencesViewController.swift index 2d0f088a..a7dece02 100644 --- a/Clocker/Preferences/General/PreferencesViewController.swift +++ b/Clocker/Preferences/General/PreferencesViewController.swift @@ -438,7 +438,7 @@ extension PreferencesViewController { return } - let searchResults = self.decode(from: data) + let searchResults = data.decode() if searchResults?.status == "ZERO_RESULTS" { self.findLocalSearchResultsForTimezones() @@ -460,8 +460,6 @@ extension PreferencesViewController { private func findLocalSearchResultsForTimezones() { let lowercasedSearchString = searchField.stringValue.lowercased() searchResultsDataSource.searchTimezones(lowercasedSearchString) - - Logger.info(searchResultsDataSource.timezoneFilteredArray.debugDescription) } private func generateSearchURL() -> String { @@ -520,30 +518,6 @@ extension PreferencesViewController { } } - // Extracting this out for tests - private func decode(from data: Data) -> SearchResult? { - let jsonDecoder = JSONDecoder() - do { - let decodedObject = try jsonDecoder.decode(SearchResult.self, from: data) - return decodedObject - } catch { - Logger.info("decodedObject error: \n\(error)") - return nil - } - } - - // Extracting this out for tests - private func decodeTimezone(from data: Data) -> Timezone? { - let jsonDecoder = JSONDecoder() - do { - let decodedObject = try jsonDecoder.decode(Timezone.self, from: data) - return decodedObject - } catch { - Logger.info("decodedObject error: \n\(error)") - return nil - } - } - private func resetSearchView() { if dataTask?.state == .running { dataTask?.cancel() @@ -576,7 +550,7 @@ extension PreferencesViewController { return } - if error == nil, let json = response, let timezone = strongSelf.decodeTimezone(from: json) { + if error == nil, let json = response, let timezone = json.decodeTimezone() { if strongSelf.availableTimezoneTableView.selectedRow >= 0 { strongSelf.installTimezone(timezone) } @@ -597,7 +571,7 @@ extension PreferencesViewController { } private func installTimezone(_ timezone: Timezone) { - guard let dataObject = searchResultsDataSource.retrieveFilteredResult(availableTimezoneTableView.selectedRow) else { + guard let dataObject = searchResultsDataSource.retrieveFilteredResultFromGoogleAPI(availableTimezoneTableView.selectedRow) else { assertionFailure("Data was unexpectedly nil") return } @@ -742,7 +716,7 @@ extension PreferencesViewController { } private func cleanupAfterInstallingCity() { - guard let dataObject = searchResultsDataSource.retrieveFilteredResult(availableTimezoneTableView.selectedRow) else { + guard let dataObject = searchResultsDataSource.retrieveFilteredResultFromGoogleAPI(availableTimezoneTableView.selectedRow) else { assertionFailure("Data was unexpectedly nil") return } @@ -763,8 +737,7 @@ extension PreferencesViewController { let data = TimezoneData() data.setLabel(CLEmptyString) - let currentSelection = searchResultsDataSource.retrieveSelectedTimezone(searchField.stringValue, - availableTimezoneTableView.selectedRow) + let currentSelection = searchResultsDataSource.retrieveSelectedTimezone(availableTimezoneTableView.selectedRow) let metaInfo = metadata(for: currentSelection) data.timezoneID = metaInfo.0.name diff --git a/Clocker/Preferences/General/SearchDataSource.swift b/Clocker/Preferences/General/SearchDataSource.swift index 11bb779e..5885f194 100644 --- a/Clocker/Preferences/General/SearchDataSource.swift +++ b/Clocker/Preferences/General/SearchDataSource.swift @@ -8,6 +8,11 @@ enum RowType { case timezone } +enum SearchLocation { + case onboarding + case preferences +} + struct TimezoneMetadata { let timezone: NSTimeZone let tags: Set @@ -18,6 +23,7 @@ struct TimezoneMetadata { class SearchDataSource: NSObject { private var searchField: NSSearchField! private var finalArray: [RowType] = [] + private var location: SearchLocation = .preferences private var dataTask: URLSessionDataTask? = .none private var timezoneMetadataDictionary: [String: [String]] = ["GMT+5:30": ["india", "indian", "kolkata", "calcutta", "mumbai", "delhi", "hyderabad", "noida"], @@ -27,13 +33,14 @@ class SearchDataSource: NSObject { "EST": ["florida", "new york"], "EDT": ["florida", "new york"]] - private var filteredArray: [TimezoneData] = [] - private var timezoneArray: [TimezoneMetadata] = [] - var timezoneFilteredArray: [TimezoneMetadata] = [] + private var filteredArray: [TimezoneData] = [] // Filtered results from the Google API + private var timezoneArray: [TimezoneMetadata] = [] // All timezones + var timezoneFilteredArray: [TimezoneMetadata] = [] // Filtered timezones list based on search input - init(with searchField: NSSearchField) { + init(with searchField: NSSearchField, location: SearchLocation = .preferences) { super.init() self.searchField = searchField + self.location = location setupTimezoneDatasource() calculateChangesets() } @@ -50,10 +57,28 @@ class SearchDataSource: NSObject { return finalArray[row] } - func retrieveFilteredResult(_ index: Int) -> TimezoneData? { + // Returns result from finalArray based on the type i.e. city or timezone + func retrieveResult(_ row: Int) -> Any? { + let currentRowType = finalArray[row] + + switch currentRowType { + case .timezone: + let datasource = searchField.stringValue.isEmpty ? timezoneArray : timezoneFilteredArray + return datasource[row % datasource.count] + case .city: + let timezoneData = filteredArray[row % filteredArray.count] + return timezoneData + } + } + + func retrieveFilteredResultFromGoogleAPI(_ index: Int) -> TimezoneData? { return filteredArray[index % filteredArray.count] } + func resultsCount() -> Int { + return finalArray.count + } + private func setupTimezoneDatasource() { timezoneArray = [] @@ -103,7 +128,7 @@ class SearchDataSource: NSObject { } } - if searchField.stringValue.isEmpty { + if searchField.stringValue.isEmpty, location == .preferences { addTimezonesIfNeeded(timezoneArray) } else { filteredArray.forEach { _ in @@ -132,8 +157,8 @@ class SearchDataSource: NSObject { } } - func retrieveSelectedTimezone(_ searchString: String, _ selectedIndex: Int) -> TimezoneMetadata { - return searchString.isEmpty == false ? timezoneFilteredArray[selectedIndex % timezoneFilteredArray.count] : + func retrieveSelectedTimezone(_ selectedIndex: Int) -> TimezoneMetadata { + return searchField.stringValue.isEmpty == false ? timezoneFilteredArray[selectedIndex % timezoneFilteredArray.count] : timezoneArray[selectedIndex] } }