diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-04-06 14:00:32 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-04-06 14:00:32 +0200 |
| commit | 2cc9013dfe0b5336d7b7c3146a5c8702e3007477 (patch) | |
| tree | 301fc97135385fc854dbf88d150315c97a208723 | |
| parent | 1b5f141c9d4f05698a02455d000231ce70350379 (diff) | |
| parent | 97580ad1aacb78bac6a261ba48a9f87f29595df1 (diff) | |
| download | mullvadvpn-2cc9013dfe0b5336d7b7c3146a5c8702e3007477.tar.xz mullvadvpn-2cc9013dfe0b5336d7b7c3146a5c8702e3007477.zip | |
Merge branch 'add-a-search-bar-to-location-selection-ios-42'
9 files changed, 306 insertions, 135 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 6572a6bd59..bcaecc10dc 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -23,6 +23,9 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Added +- Add search functionality to location selection view. + ### Changed - Changed key rotation interval from 4 to 14 days. - Delay tunnel reconnection after a WireGuard private key rotates. Accounts for latency in key diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 9931a2cbac..4841866011 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -358,6 +358,8 @@ 58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */; }; 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; }; 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; + 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; + 7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */; }; E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; }; E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; }; E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; @@ -939,6 +941,8 @@ 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseButton.swift; sourceTree = "<group>"; }; 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; }; 58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; }; + 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; }; + 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Appearance.swift"; sourceTree = "<group>"; }; 7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = "<group>"; }; 7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = "<group>"; }; 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesCellFactory.swift; sourceTree = "<group>"; }; @@ -1421,10 +1425,12 @@ 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */, 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */, 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */, - 5807E2BF2432038B00F5FF30 /* String+Split.swift */, E158B35F285381C60002F069 /* String+AccountFormatting.swift */, + 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */, + 5807E2BF2432038B00F5FF30 /* String+Split.swift */, 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, + 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */, 5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */, ); path = Extensions; @@ -2594,6 +2600,7 @@ 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */, 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */, 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */, + 7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */, 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */, 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */, 586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */, @@ -2755,6 +2762,7 @@ 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, 583FE00E29C0D586006E85F9 /* OutOfTimeCoordinator.swift in Sources */, + 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */, 58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */, 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */, 58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */, diff --git a/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift index ff7b4a7164..f5a69fce9d 100644 --- a/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift @@ -59,13 +59,7 @@ class SelectLocationCoordinator: Coordinator, Presentable, RelayCacheTrackerObse controller.setCachedRelays(cachedRelays) } - let relayConstraints = tunnelManager.settings.relayConstraints - - controller.setSelectedRelayLocation( - relayConstraints.location.value, - animated: false, - scrollPosition: .middle - ) + controller.relayLocation = tunnelManager.settings.relayConstraints.location.value navigationController.pushViewController(controller, animated: false) } diff --git a/ios/MullvadVPN/Extensions/String+FuzzyMatch.swift b/ios/MullvadVPN/Extensions/String+FuzzyMatch.swift new file mode 100644 index 0000000000..3f6b377f5c --- /dev/null +++ b/ios/MullvadVPN/Extensions/String+FuzzyMatch.swift @@ -0,0 +1,36 @@ +// +// String+FuzzyMatch.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-04-02. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension String { + func fuzzyMatch(_ needle: String) -> Bool { + guard !needle.isEmpty else { return false } + + let haystack = lowercased() + let needle = needle.lowercased() + + var indices: [Index] = [] + var remainder = needle[...].utf8 + + for index in haystack.utf8.indices { + let character = haystack.utf8[index] + + if character == remainder[remainder.startIndex] { + indices.append(index) + remainder.removeFirst() + + if remainder.isEmpty { + return !indices.isEmpty + } + } + } + + return false + } +} diff --git a/ios/MullvadVPN/Extensions/UISearchBar+Appearance.swift b/ios/MullvadVPN/Extensions/UISearchBar+Appearance.swift new file mode 100644 index 0000000000..065293575f --- /dev/null +++ b/ios/MullvadVPN/Extensions/UISearchBar+Appearance.swift @@ -0,0 +1,51 @@ +// +// UISearchBar+Appearance.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-04-04. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +extension UISearchBar { + struct SearchBarAppearance { + let placeholderTextColor: UIColor + let textColor: UIColor + let backgroundColor: UIColor + let leftViewTintColor: UIColor + + static var active: SearchBarAppearance { + return SearchBarAppearance( + placeholderTextColor: .SearchTextField.placeholderTextColor, + textColor: .SearchTextField.textColor, + backgroundColor: .SearchTextField.backgroundColor, + leftViewTintColor: .SearchTextField.leftViewTintColor + ) + } + + static var inactive: SearchBarAppearance { + return SearchBarAppearance( + placeholderTextColor: .SearchTextField.inactivePlaceholderTextColor, + textColor: .SearchTextField.inactiveTextColor, + backgroundColor: .SearchTextField.inactiveBackgroundColor, + leftViewTintColor: .SearchTextField.inactiveLeftViewTintColor + ) + } + + func apply(to searchBar: UISearchBar) { + let textField = searchBar.searchTextField + + textField.leftView?.tintColor = leftViewTintColor + textField.tintColor = textColor + textField.textColor = textColor + textField.backgroundColor = backgroundColor + textField.attributedPlaceholder = NSAttributedString( + string: searchBar.placeholder ?? "", + attributes: [ + .foregroundColor: placeholderTextColor, + ] + ) + } + } +} diff --git a/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift b/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift index 17c6be3c0a..a2a4d4ef32 100644 --- a/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift +++ b/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift @@ -192,13 +192,13 @@ struct PinnableEdges { return firstView.topAnchor.constraint(equalTo: secondView.topAnchor, constant: inset) case let .bottom(inset): - return firstView.bottomAnchor.constraint(equalTo: secondView.bottomAnchor, constant: inset) + return firstView.bottomAnchor.constraint(equalTo: secondView.bottomAnchor, constant: -inset) case let .leading(inset): return firstView.leadingAnchor.constraint(equalTo: secondView.leadingAnchor, constant: inset) case let .trailing(inset): - return firstView.trailingAnchor.constraint(equalTo: secondView.trailingAnchor, constant: inset) + return firstView.trailingAnchor.constraint(equalTo: secondView.trailingAnchor, constant: -inset) } } } diff --git a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift index 07522f6cf5..de2472680a 100644 --- a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift +++ b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift @@ -36,6 +36,17 @@ extension UIColor { static let invalidInputTextColor = UIColor.dangerColor } + enum SearchTextField { + static let placeholderTextColor = TextField.placeholderTextColor + static let inactivePlaceholderTextColor = UIColor.white + static let textColor = TextField.textColor + static let inactiveTextColor = UIColor.white + static let backgroundColor = TextField.backgroundColor + static let inactiveBackgroundColor = UIColor.secondaryColor + static let leftViewTintColor = UIColor.primaryColor + static let inactiveLeftViewTintColor = UIColor.white + } + enum AppButton { static let normalTitleColor = UIColor.white static let highlightedTitleColor = UIColor.lightGray diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift index 62d9b3a4e8..9366879b89 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift @@ -34,8 +34,7 @@ final class LocationDataSource: UITableViewDiffableDataSource<Int, RelayLocation private var nodeByLocation = [RelayLocation: Node]() private var locationList = [RelayLocation]() - private var rootNode = makeRootNode() - private(set) var selectedRelayLocation: RelayLocation? + private var currentSearchString = "" private let tableView: UITableView private let locationCellFactory: LocationCellFactory @@ -51,6 +50,7 @@ final class LocationDataSource: UITableViewDiffableDataSource<Int, RelayLocation ) } + var selectedRelayLocation: RelayLocation? var didSelectRelayLocation: ((RelayLocation) -> Void)? init(tableView: UITableView) { @@ -73,21 +73,6 @@ final class LocationDataSource: UITableViewDiffableDataSource<Int, RelayLocation registerClasses() } - func setSelectedRelayLocation( - _ relayLocation: RelayLocation?, - animated: Bool - ) { - selectedRelayLocation = relayLocation - - let selectedLocationTree = selectedRelayLocation?.ascendants ?? [] - selectedLocationTree.forEach { location in - nodeByLocation[location]?.showsChildren = true - } - - updateCellFactory(with: nodeByLocation) - updateDataSnapshot(with: locationList, animated: animated) - } - func setRelays(_ response: REST.ServerRelaysResponse) { let rootNode = Self.makeRootNode() var nodeByLocation = [RelayLocation: Node]() @@ -153,43 +138,77 @@ final class LocationDataSource: UITableViewDiffableDataSource<Int, RelayLocation rootNode.sortChildrenRecursive() rootNode.computeActiveChildrenRecursive() self.nodeByLocation = nodeByLocation - self.rootNode = rootNode locationList = rootNode.flatRelayLocationList() - updateCellFactory(with: nodeByLocation) - updateDataSnapshot(with: locationList) + filterRelays(by: currentSearchString) } func indexPathForSelectedRelay() -> IndexPath? { return selectedRelayLocation.flatMap { indexPath(for: $0) } } - private func updateDataSnapshot( - with locations: [RelayLocation], - animated: Bool = false, - completion: (() -> Void)? = nil - ) { - var snapshot = NSDiffableDataSourceSnapshot<Int, RelayLocation>() - snapshot.appendSections([0]) + func filterRelays(by searchString: String) { + currentSearchString = searchString - for location in locations { - snapshot.appendItems([location]) + if currentSearchString.isEmpty { + return resetLocationList() + } - guard let countryNode = nodeByLocation[location], countryNode.showsChildren else { - continue + var filteredLocations = [RelayLocation]() + + locationList.forEach { location in + guard let countryNode = nodeByLocation[location] else { return } + countryNode.showsChildren = false + + if searchString.isEmpty || countryNode.displayName.fuzzyMatch(searchString) { + filteredLocations.append(countryNode.location) } for cityNode in countryNode.children { - snapshot.appendItems([cityNode.location]) + cityNode.showsChildren = false - guard cityNode.showsChildren else { - continue - } + let relaysContainSearchString = cityNode.children.contains(where: { node in + node.displayName.fuzzyMatch(searchString) + }) - snapshot.appendItems(cityNode.children.map { $0.location }) + if cityNode.displayName.fuzzyMatch(searchString) || relaysContainSearchString { + if !filteredLocations.contains(countryNode.location) { + filteredLocations.append(countryNode.location) + } + + filteredLocations.append(cityNode.location) + countryNode.showsChildren = true + + if relaysContainSearchString { + filteredLocations.append(contentsOf: cityNode.children.map { $0.location }) + cityNode.showsChildren = true + } + } } } + updateDataSnapshot(with: filteredLocations, reloadExisting: true) { [weak self] in + self?.scrollToTop(animated: false) + } + } + + private func updateDataSnapshot( + with locations: [RelayLocation], + reloadExisting: Bool = false, + animated: Bool = false, + completion: (() -> Void)? = nil + ) { + updateCellFactory(with: nodeByLocation) + + var snapshot = NSDiffableDataSourceSnapshot<Int, RelayLocation>() + + snapshot.appendSections([0]) + snapshot.appendItems(locations) + + if reloadExisting { + snapshot.reloadItems(locations) + } + apply(snapshot, animatingDifferences: animated, completion: completion) } @@ -206,22 +225,56 @@ final class LocationDataSource: UITableViewDiffableDataSource<Int, RelayLocation locationCellFactory.nodeByLocation = nodeByLocation } + private func setSelectedRelayLocation( + _ relayLocation: RelayLocation?, + animated: Bool, + completion: (() -> Void)? = nil + ) { + selectedRelayLocation = relayLocation + var locationList = snapshot().itemIdentifiers + + guard let selectedRelayLocation = selectedRelayLocation, + !locationList.contains(selectedRelayLocation) else { return } + + let selectedLocationTree = selectedRelayLocation.ascendants + [selectedRelayLocation] + + guard let topLocation = selectedLocationTree.first, + let topNode = nodeByLocation[topLocation], + let indexPath = indexPath(for: topLocation) + else { + return + } + + selectedLocationTree.forEach { location in + nodeByLocation[location]?.showsChildren = true + } + + locationList.addLocations(topNode.flatRelayLocationList(), at: indexPath.row + 1) + updateDataSnapshot(with: locationList, reloadExisting: true, animated: animated, completion: completion) + } + private func toggleChildren( _ relayLocation: RelayLocation, show: Bool, animated: Bool ) { - guard let node = nodeByLocation[relayLocation] else { return } + guard let node = nodeByLocation[relayLocation], + let indexPath = indexPath(for: node.location), + let cell = tableView.cellForRow(at: indexPath) else { return } node.showsChildren = show + locationCellFactory.configureCell(cell, item: node.location, indexPath: indexPath) - if let indexPath = indexPath(for: node.location), - let cell = tableView.cellForRow(at: indexPath) - { - locationCellFactory.configureCell(cell, item: node.location, indexPath: indexPath) + var locationList = snapshot().itemIdentifiers + let locationsToEdit = node.flatRelayLocationList() + + if show { + locationList.addLocations(locationsToEdit, at: indexPath.row + 1) + } else { + locationsToEdit.forEach { nodeByLocation[$0]?.showsChildren = false } + locationList.removeLocations(locationsToEdit) } - updateCellFactory(with: nodeByLocation) updateDataSnapshot(with: locationList, animated: animated) { [weak self] in guard let visibleIndexPaths = self?.tableView.indexPathsForVisibleRows else { return } @@ -257,9 +310,24 @@ final class LocationDataSource: UITableViewDiffableDataSource<Int, RelayLocation } } + private func resetLocationList() { + nodeByLocation.values.forEach { $0.showsChildren = false } + + updateDataSnapshot(with: locationList, reloadExisting: true) + setSelectedRelayLocation(selectedRelayLocation, animated: false) + + if let indexPath = indexPathForSelectedRelay() { + tableView.scrollToRow(at: indexPath, at: .middle, animated: false) + } + } + private func item(for indexPath: IndexPath) -> LocationDataSourceItemProtocol? { return itemIdentifier(for: indexPath).flatMap { nodeByLocation[$0] } } + + private func scrollToTop(animated: Bool) { + tableView.setContentOffset(.zero, animated: animated) + } } extension LocationDataSource: UITableViewDelegate { @@ -284,7 +352,11 @@ extension LocationDataSource: UITableViewDelegate { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let item = item(for: indexPath) else { return } + guard let item = item(for: indexPath), + item.location != selectedRelayLocation + else { + return + } if let indexPath = indexPathForSelectedRelay(), let cell = tableView.cellForRow(at: indexPath) @@ -392,18 +464,9 @@ extension LocationDataSource { } } - func countChildrenRecursive(where condition: @escaping (Node) -> Bool) -> Int { - return children.reduce(into: 0) { numVisibleChildren, node in - numVisibleChildren += 1 - if condition(node) { - numVisibleChildren += node.countChildrenRecursive(where: condition) - } - } - } - - func flatRelayLocationList() -> [RelayLocation] { + func flatRelayLocationList(includeHiddenChildren: Bool = false) -> [RelayLocation] { return children.reduce(into: []) { array, node in - Self.flatten(node: node, into: &array) + Self.flatten(node: node, into: &array, includeHiddenChildren: includeHiddenChildren) } } @@ -425,11 +488,11 @@ extension LocationDataSource { } } - private class func flatten(node: Node, into array: inout [RelayLocation]) { + private class func flatten(node: Node, into array: inout [RelayLocation], includeHiddenChildren: Bool) { array.append(node.location) - if node.showsChildren { + if includeHiddenChildren || node.showsChildren { for child in node.children { - Self.flatten(node: child, into: &array) + Self.flatten(node: child, into: &array, includeHiddenChildren: includeHiddenChildren) } } } @@ -443,3 +506,19 @@ private func lexicalSortComparator(_ a: String, _ b: String) -> Bool { private func fileSortComparator(_ a: String, _ b: String) -> Bool { return a.localizedStandardCompare(b) == .orderedAscending } + +private extension Array where Element == RelayLocation { + mutating func addLocations(_ locations: [RelayLocation], at index: Int) { + if index < count { + insert(contentsOf: locations, at: index) + } else { + append(contentsOf: locations) + } + } + + mutating func removeLocations(_ locations: [RelayLocation]) { + removeAll(where: { location in + locations.contains(location) + }) + } +} diff --git a/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift index e846213c03..608aeb5db3 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift @@ -11,13 +11,12 @@ import MullvadTypes import RelayCache import UIKit -final class SelectLocationViewController: UIViewController, UITableViewDelegate { - private var tableView: UITableView? +final class SelectLocationViewController: UIViewController { + private let searchBar = UISearchBar() + private let tableView = UITableView() private var dataSource: LocationDataSource? private var cachedRelays: CachedRelays? - private var relayLocation: RelayLocation? - private var scrollPosition: UITableView.ScrollPosition? - private var isViewAppeared = false + var relayLocation: RelayLocation? override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent @@ -26,21 +25,13 @@ final class SelectLocationViewController: UIViewController, UITableViewDelegate var didSelectRelay: ((RelayLocation) -> Void)? var didFinish: (() -> Void)? - var scrollToSelectedRelayOnViewWillAppear = true - - init() { - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: - View lifecycle override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .secondaryColor + navigationItem.title = NSLocalizedString( "NAVIGATION_TITLE", tableName: "SelectLocation", @@ -53,32 +44,26 @@ final class SelectLocationViewController: UIViewController, UITableViewDelegate action: #selector(handleDone) ) - setupTableView() setupDataSource() - } + setupTableView() + setupSearchBar() - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + let searchBarTopMargin: CGFloat = splitViewController == nil ? -16 : 0 + view.addConstrainedSubviews([searchBar, tableView]) { + searchBar.pinEdges( + .init([.top(searchBarTopMargin), .leading(8), .trailing(8)]), + to: view.safeAreaLayoutGuide + ) - if let indexPath = dataSource?.indexPathForSelectedRelay(), - scrollToSelectedRelayOnViewWillAppear, !isViewAppeared - { - tableView?.scrollToRow(at: indexPath, at: scrollPosition ?? .middle, animated: false) + tableView.pinEdgesToSuperview(.all().excluding(.top)) + tableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor) } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - isViewAppeared = true - - tableView?.flashScrollIndicators() - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - isViewAppeared = false + tableView.flashScrollIndicators() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -87,7 +72,7 @@ final class SelectLocationViewController: UIViewController, UITableViewDelegate coordinator.animate(alongsideTransition: nil) { context in guard let indexPath = self.dataSource?.indexPathForSelectedRelay() else { return } - self.tableView?.scrollToRow(at: indexPath, at: .middle, animated: false) + self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) } } @@ -99,56 +84,60 @@ final class SelectLocationViewController: UIViewController, UITableViewDelegate dataSource?.setRelays(cachedRelays.relays) } - func setSelectedRelayLocation( - _ relayLocation: RelayLocation?, - animated: Bool, - scrollPosition: UITableView.ScrollPosition - ) { - self.relayLocation = relayLocation - self.scrollPosition = scrollPosition - - dataSource?.setSelectedRelayLocation(relayLocation, animated: animated) - } - // MARK: - Private - private func setupTableView() { - let tableView = UITableView(frame: view.bounds, style: .plain) - tableView.backgroundColor = .clear - tableView.backgroundColor = .secondaryColor - tableView.separatorColor = .secondaryColor - tableView.separatorInset = .zero - tableView.estimatedRowHeight = 53 - tableView.indicatorStyle = .white - tableView.delegate = self - - view.backgroundColor = .secondaryColor - - view.addConstrainedSubviews([tableView]) { - tableView.pinEdgesToSuperview() - } - - self.tableView = tableView + @objc private func handleDone() { + didFinish?() } private func setupDataSource() { - guard let tableView = tableView else { return } - dataSource = LocationDataSource(tableView: tableView) dataSource?.didSelectRelayLocation = { [weak self] location in self?.didSelectRelay?(location) } + dataSource?.selectedRelayLocation = relayLocation + if let cachedRelays = cachedRelays { dataSource?.setRelays(cachedRelays.relays) } + } - if let relayLocation = relayLocation { - dataSource?.setSelectedRelayLocation(relayLocation, animated: false) - } + private func setupTableView() { + tableView.backgroundColor = view.backgroundColor + tableView.separatorColor = .secondaryColor + tableView.separatorInset = .zero + tableView.estimatedRowHeight = 53 + tableView.indicatorStyle = .white + tableView.keyboardDismissMode = .onDrag } - @objc private func handleDone() { - didFinish?() + private func setupSearchBar() { + searchBar.delegate = self + searchBar.searchBarStyle = .minimal + searchBar.layer.cornerRadius = 8 + searchBar.clipsToBounds = true + searchBar.placeholder = NSLocalizedString( + "SEARCHBAR_PLACEHOLDER", + tableName: "SelectLocation", + value: "Search for...", + comment: "" + ) + + UISearchBar.SearchBarAppearance.inactive.apply(to: searchBar) + } +} + +extension SelectLocationViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + dataSource?.filterRelays(by: searchText) + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + UISearchBar.SearchBarAppearance.active.apply(to: searchBar) + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + UISearchBar.SearchBarAppearance.inactive.apply(to: searchBar) } } |
