summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@kvadrat.se>2023-05-04 14:10:11 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-06-06 10:09:00 +0200
commit3453eee5db8b569bf4e3c8b069bb35e87701a65d (patch)
tree23a299326fdcd625f70351c7b81fc3f365bc7157 /ios
parent4f60f2368fba733c1aaa968c34d0a33d43e4e90b (diff)
downloadmullvadvpn-3453eee5db8b569bf4e3c8b069bb35e87701a65d.tar.xz
mullvadvpn-3453eee5db8b569bf4e3c8b069bb35e87701a65d.zip
Add custom port selection to settings
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadTypes/RelayConstraints.swift12
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj26
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift3
-rw-r--r--ios/MullvadVPN/Extensions/UITextField+Appearance.swift (renamed from ios/MullvadVPN/Extensions/UISearchBar+Appearance.swift)36
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/UI appearance/UIColor+Palette.swift4
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift8
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift63
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift280
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift7
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift41
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift64
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift78
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift6
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift41
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCell.swift39
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift (renamed from ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift)6
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsInputCell.swift133
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift8
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift7
-rw-r--r--ios/MullvadVPNTests/RelaySelectorTests.swift12
-rw-r--r--ios/RelaySelector/RelaySelector.swift16
22 files changed, 760 insertions, 132 deletions
diff --git a/ios/MullvadTypes/RelayConstraints.swift b/ios/MullvadTypes/RelayConstraints.swift
index 55de62f9b3..22a84aea0e 100644
--- a/ios/MullvadTypes/RelayConstraints.swift
+++ b/ios/MullvadTypes/RelayConstraints.swift
@@ -10,15 +10,17 @@ import Foundation
public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible {
public var location: RelayConstraint<RelayLocation>
+ public var port: RelayConstraint<UInt16>
public var debugDescription: String {
- var output = "RelayConstraints { "
- output += "location: \(String(reflecting: location))"
- output += " }"
- return output
+ return "RelayConstraints { location: \(location), port: \(port) }"
}
- public init(location: RelayConstraint<RelayLocation> = .only(.country("se"))) {
+ public init(
+ location: RelayConstraint<RelayLocation> = .only(.country("se")),
+ port: RelayConstraint<UInt16> = .any
+ ) {
self.location = location
+ self.port = port
}
}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 3d83d27de7..e6389fd0ab 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -360,10 +360,12 @@
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 */; };
+ 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; };
+ 7A42DECD2A09064C00B209BE /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
- 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */; };
+ 7A7AD28F29DEDB1C00480EF1 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsHeaderView.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
- 7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */; };
+ 7AD2DA1529DC4EB900250737 /* UITextField+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UITextField+Appearance.swift */; };
7AF0419E29E957EB00D492DD /* AccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */; };
A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */; };
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; };
@@ -1030,10 +1032,12 @@
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>"; };
+ 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
+ 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
- 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContentBlockersHeaderView.swift; sourceTree = "<group>"; };
+ 7A7AD28E29DEDB1C00480EF1 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
- 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Appearance.swift"; sourceTree = "<group>"; };
+ 7AD2DA1429DC4EB900250737 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = "<group>"; };
7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = "<group>"; };
A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; };
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
@@ -1392,7 +1396,7 @@
5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */,
582BB1AE229566420055B6EF /* SettingsCell.swift */,
5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */,
- 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */,
+ 7A7AD28E29DEDB1C00480EF1 /* SettingsHeaderView.swift */,
58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */,
58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */,
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */,
@@ -1401,6 +1405,8 @@
584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
58CCA01122424D11004F3011 /* SettingsViewController.swift */,
+ 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */,
+ 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -1544,10 +1550,10 @@
E158B35F285381C60002F069 /* String+AccountFormatting.swift */,
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */,
5807E2BF2432038B00F5FF30 /* String+Split.swift */,
- 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
5864211E29F04CED00822139 /* UIBarButtonItem+Blocks.swift */,
+ 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
- 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */,
+ 7AD2DA1429DC4EB900250737 /* UITextField+Appearance.swift */,
5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */,
);
path = Extensions;
@@ -2783,7 +2789,7 @@
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */,
- 7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */,
+ 7AD2DA1529DC4EB900250737 /* UITextField+Appearance.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */,
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */,
@@ -2897,6 +2903,7 @@
5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */,
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
+ 7A42DECD2A09064C00B209BE /* SelectableSettingsCell.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
@@ -2904,7 +2911,7 @@
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */,
586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */,
- 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */,
+ 7A7AD28F29DEDB1C00480EF1 /* SettingsHeaderView.swift in Sources */,
5878F50229CDB989003D4BE2 /* ChangeLogCoordinator.swift in Sources */,
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
@@ -2954,6 +2961,7 @@
58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */,
580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */,
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */,
+ 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */,
5859A55329CD9B1300F66591 /* ChangeLog.swift in Sources */,
58BBB39729717E0C00C8DB7C /* ApplicationCoordinator.swift in Sources */,
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
index 183cada2a3..117c8ea20c 100644
--- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
@@ -659,7 +659,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
let interactorFactory = SettingsInteractorFactory(
storePaymentManager: storePaymentManager,
tunnelManager: tunnelManager,
- apiProxy: apiProxy
+ apiProxy: apiProxy,
+ relayCacheTracker: relayCacheTracker
)
let navigationController = CustomNavigationController()
diff --git a/ios/MullvadVPN/Extensions/UISearchBar+Appearance.swift b/ios/MullvadVPN/Extensions/UITextField+Appearance.swift
index 9aca1a6e15..706d3c8caa 100644
--- a/ios/MullvadVPN/Extensions/UISearchBar+Appearance.swift
+++ b/ios/MullvadVPN/Extensions/UITextField+Appearance.swift
@@ -1,5 +1,5 @@
//
-// UISearchBar+Appearance.swift
+// UITextField+Appearance.swift
// MullvadVPN
//
// Created by Jon Petersson on 2023-04-04.
@@ -8,15 +8,15 @@
import UIKit
-extension UISearchBar {
- struct SearchBarAppearance {
+extension UITextField {
+ struct SearchTextFieldAppearance {
let placeholderTextColor: UIColor
let textColor: UIColor
let backgroundColor: UIColor
let leftViewTintColor: UIColor
- static var active: SearchBarAppearance {
- return SearchBarAppearance(
+ static var active: SearchTextFieldAppearance {
+ return SearchTextFieldAppearance(
placeholderTextColor: .SearchTextField.placeholderTextColor,
textColor: .SearchTextField.textColor,
backgroundColor: .SearchTextField.backgroundColor,
@@ -24,8 +24,8 @@ extension UISearchBar {
)
}
- static var inactive: SearchBarAppearance {
- return SearchBarAppearance(
+ static var inactive: SearchTextFieldAppearance {
+ return SearchTextFieldAppearance(
placeholderTextColor: .SearchTextField.inactivePlaceholderTextColor,
textColor: .SearchTextField.inactiveTextColor,
backgroundColor: .SearchTextField.inactiveBackgroundColor,
@@ -34,23 +34,29 @@ extension UISearchBar {
}
func apply(to searchBar: UISearchBar) {
- let textField = searchBar.searchTextField
-
searchBar.setImage(
UIImage(named: "IconCloseSml")?.withTintColor(leftViewTintColor),
for: .clear,
state: .normal
)
+
+ apply(to: searchBar.searchTextField)
+ }
+
+ func apply(to textField: UITextField) {
textField.leftView?.tintColor = leftViewTintColor
textField.tintColor = textColor
textField.textColor = textColor
textField.backgroundColor = backgroundColor
- textField.attributedPlaceholder = NSAttributedString(
- string: searchBar.placeholder ?? "",
- attributes: [
- .foregroundColor: placeholderTextColor,
- ]
- )
+
+ if let customTextField = textField as? CustomTextField {
+ customTextField.placeholderTextColor = placeholderTextColor
+ } else {
+ textField.attributedPlaceholder = NSAttributedString(
+ string: textField.placeholder ?? "",
+ attributes: [.foregroundColor: placeholderTextColor]
+ )
+ }
}
}
}
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
index 8659f8d666..a11e9433ca 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
@@ -32,7 +32,7 @@ final class RegisteredDeviceInAppNotificationProvider: NotificationProvider,
)
let deviceName = storedDeviceData?.capitalizedName ?? ""
let string = String(format: formattedString, deviceName)
- return NSMutableAttributedString(markdownString: string, font: .systemFont(ofSize: 14.0)) { deviceName in
+ return NSAttributedString(markdownString: string, font: .systemFont(ofSize: 14.0)) { deviceName in
return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor]
}
}
diff --git a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
index 3cf705cdc3..cf70f71585 100644
--- a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
+++ b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
@@ -38,11 +38,11 @@ extension UIColor {
enum SearchTextField {
static let placeholderTextColor = TextField.placeholderTextColor
- static let inactivePlaceholderTextColor = UIColor.white
+ static let inactivePlaceholderTextColor = UIColor(white: 1.0, alpha: 0.4)
static let textColor = TextField.textColor
static let inactiveTextColor = UIColor.white
static let backgroundColor = TextField.backgroundColor
- static let inactiveBackgroundColor = UIColor.secondaryColor
+ static let inactiveBackgroundColor = UIColor(white: 1.0, alpha: 0.1)
static let leftViewTintColor = UIColor.primaryColor
static let inactiveLeftViewTintColor = UIColor.white
}
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 22359687ce..02b30f6974 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -24,6 +24,14 @@ extension UIMetrics {
/// Common layout margins for settings cell presentation
static let settingsCellLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 12)
+ /// Common layout margins for text field in settings input cell presentation
+ static let settingsInputCellTextFieldLayoutMargins = UIEdgeInsets(
+ top: 0,
+ left: 8,
+ bottom: 0,
+ right: 8
+ )
+
/// Common layout margins for location cell presentation
static let selectLocationCellLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 28, bottom: 16, trailing: 12)
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
index 38fc56090f..6455a183e1 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
@@ -12,7 +12,9 @@ protocol PreferencesCellEventHandler {
func addDNSEntry()
func didChangeDNSEntry(with identifier: UUID, inputString: String) -> Bool
func didChangeState(for item: PreferencesDataSource.Item, isOn: Bool)
- func didPressInfoButton(for item: PreferencesDataSource.Item)
+ func showInfo(for item: PreferencesDataSource.InfoButtonItem)
+ func addCustomPort(_ port: UInt16)
+ func selectCustomPortEntry(_ port: UInt16) -> Bool
}
final class PreferencesCellFactory: CellFactoryProtocol {
@@ -88,7 +90,7 @@ final class PreferencesCellFactory: CellFactoryProtocol {
cell.setInfoButtonIsVisible(true)
cell.setOn(viewModel.blockMalware, animated: false)
cell.infoButtonHandler = { [weak self] in
- self?.delegate?.didPressInfoButton(for: .blockMalware)
+ self?.delegate?.showInfo(for: .blockMalware)
}
cell.action = { [weak self] isOn in
self?.delegate?.didChangeState(for: .blockMalware, isOn: isOn)
@@ -132,6 +134,63 @@ final class PreferencesCellFactory: CellFactoryProtocol {
)
}
+ case let .wireGuardPort(port):
+ guard let cell = cell as? SelectableSettingsCell else { return }
+
+ var portString = NSLocalizedString(
+ "WIRE_GUARD_PORT_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Automatic",
+ comment: ""
+ )
+ if let port {
+ portString = String(port)
+ }
+
+ cell.titleLabel.text = portString
+ cell.accessibilityHint = nil
+ cell.applySubCellStyling()
+
+ case .wireGuardCustomPort:
+ guard let cell = cell as? SettingsInputCell else { return }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "WIRE_GUARD_CUSTOM_PORT_CELL_LABEL",
+ tableName: "Preferences",
+ value: "Custom",
+ comment: ""
+ )
+ cell.textField.placeholder = NSLocalizedString(
+ "WIRE_GUARD_CUSTOM_PORT_CELL_INPUT_PLACEHOLDER",
+ tableName: "Preferences",
+ value: "Port",
+ comment: ""
+ )
+
+ cell.accessibilityHint = nil
+ cell.applySubCellStyling()
+
+ cell.inputDidChange = { [weak self] text in
+ let port = UInt16(text) ?? UInt16()
+ cell.isValidInput = self?.delegate?.selectCustomPortEntry(port) ?? false
+ }
+ cell.inputWasConfirmed = { [weak self] text in
+ if let port = UInt16(text), cell.isValidInput {
+ self?.delegate?.addCustomPort(port)
+ }
+ }
+
+ if let port = viewModel.customWireGuardPort {
+ cell.textField.text = String(port)
+
+ // Only update validity if input is invalid. Otherwise the textcolor will be wrong
+ // (active text field color rather than the expected inactive color).
+ let isValidInput = delegate?.selectCustomPortEntry(port) ?? false
+ if !isValidInput {
+ cell.isValidInput = false
+ }
+ }
+
case .useCustomDNS:
guard let cell = cell as? SettingsSwitchCell else { return }
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
index 42aa8f32ea..2be26b07a2 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
@@ -15,31 +15,41 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
typealias InfoButtonHandler = (PreferencesDataSource.Item) -> Void
enum CellReuseIdentifiers: String, CaseIterable {
+ case setting
case settingSwitch
case dnsServer
case addDNSServer
+ case wireGuardPort
+ case wireGuardCustomPort
var reusableViewClass: AnyClass {
switch self {
+ case .setting:
+ return SettingsCell.self
case .settingSwitch:
return SettingsSwitchCell.self
case .dnsServer:
return SettingsDNSTextCell.self
case .addDNSServer:
return SettingsAddDNSEntryCell.self
+ case .wireGuardPort:
+ return SelectableSettingsCell.self
+ case .wireGuardCustomPort:
+ return SettingsInputCell.self
}
}
}
private enum HeaderFooterReuseIdentifiers: String, CaseIterable {
case contentBlockerHeader
+ case wireGuardPortHeader
case customDNSFooter
case spacer
var reusableViewClass: AnyClass {
switch self {
- case .contentBlockerHeader:
- return SettingsContentBlockersHeaderView.self
+ case .contentBlockerHeader, .wireGuardPortHeader:
+ return SettingsHeaderView.self
case .customDNSFooter:
return SettingsStaticTextFooterView.self
case .spacer:
@@ -48,9 +58,16 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
}
+ enum InfoButtonItem {
+ case contentBlockers
+ case blockMalware
+ case wireGuardPorts
+ }
+
enum Section: String, Hashable, CaseIterable {
case contentBlockers
case customDNS
+ case wireGuardPorts
}
enum Item: Hashable {
@@ -59,6 +76,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
case blockMalware
case blockAdultContent
case blockGambling
+ case wireGuardPort(_ port: UInt16?)
+ case wireGuardCustomPort
case useCustomDNS
case addDNSServer
case dnsServer(_ uniqueID: UUID)
@@ -67,6 +86,13 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
return [.blockAdvertising, .blockTracking, .blockMalware, .blockAdultContent, .blockGambling]
}
+ static var wireGuardPorts: [Item] {
+ let defaultPorts = PreferencesViewModel.defaultWireGuardPorts.map {
+ Item.wireGuardPort($0)
+ }
+ return [.wireGuardPort(nil)] + defaultPorts + [.wireGuardCustomPort]
+ }
+
var accessibilityIdentifier: String {
switch self {
case .blockAdvertising:
@@ -79,6 +105,14 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
return "blockGambling"
case .blockAdultContent:
return "blockAdultContent"
+ case let .wireGuardPort(port):
+ if let port {
+ return "wireGuardPort(\(port))"
+ } else {
+ return "wireGuardPort"
+ }
+ case .wireGuardCustomPort:
+ return "wireGuardCustomPort"
case .useCustomDNS:
return "useCustomDNS"
case .addDNSServer:
@@ -102,6 +136,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
return .addDNSServer
case .dnsServer:
return .dnsServer
+ case .wireGuardPort:
+ return .wireGuardPort
+ case .wireGuardCustomPort:
+ return .wireGuardCustomPort
default:
return .settingSwitch
}
@@ -119,6 +157,14 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
weak var delegate: PreferencesDataSourceDelegate?
+ var indexPathForSelectedPort: IndexPath? {
+ let selectedItem: Item = viewModel.customWireGuardPort == nil
+ ? .wireGuardPort(viewModel.wireGuardPort)
+ : .wireGuardCustomPort
+
+ return indexPath(for: selectedItem)
+ }
+
init(tableView: UITableView) {
self.tableView = tableView
@@ -138,6 +184,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
registerClasses()
}
+ func setAvailablePortRanges(_ ranges: [[UInt16]]) {
+ viewModel.availableWireGuardPortRanges = ranges
+ }
+
func setEditing(_ editing: Bool, animated: Bool) {
guard isEditing != editing else { return }
@@ -160,10 +210,37 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
if !editing, viewModelBeforeEditing != viewModel {
delegate?.preferencesDataSource(self, didChangeViewModel: viewModel)
}
+
+ selectRow(at: indexPathForSelectedPort)
+ }
+
+ func revertWireGuardPortCellToLastSelection() {
+ guard let customPortCell = getCustomPortCell(), customPortCell.textField.isEditing else {
+ return
+ }
+
+ customPortCell.textField.resignFirstResponder()
+
+ if customPortCell.isValidInput {
+ customPortCell.confirmInput()
+ } else if let port = viewModel.customWireGuardPort {
+ customPortCell.setInput(String(port))
+ customPortCell.confirmInput()
+ } else {
+ customPortCell.reset()
+
+ Item.wireGuardPorts.forEach { item in
+ if case let .wireGuardPort(port) = item, port == viewModel.wireGuardPort {
+ selectRow(at: item)
+
+ return
+ }
+ }
+ }
}
- func update(from dnsSettings: DNSSettings) {
- let newViewModel = PreferencesViewModel(from: dnsSettings)
+ func update(from tunnelSettings: TunnelSettingsV2) {
+ let newViewModel = PreferencesViewModel(from: tunnelSettings)
let mergedViewModel = viewModel.merged(newViewModel)
if viewModel != mergedViewModel {
@@ -177,6 +254,16 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
// MARK: - UITableViewDataSource
+ func tableView(
+ _ tableView: UITableView,
+ willDisplay cell: UITableViewCell,
+ forRowAt indexPath: IndexPath
+ ) {
+ if indexPath == indexPathForSelectedPort {
+ cell.setSelected(true, animated: false)
+ }
+ }
+
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Disable swipe to delete when not editing the table view
guard isEditing else { return false }
@@ -241,7 +328,34 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
- return false
+ switch itemIdentifier(for: indexPath) {
+ case .wireGuardPort, .wireGuardCustomPort:
+ return true
+ default:
+ return false
+ }
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let item = itemIdentifier(for: indexPath)
+
+ switch item {
+ case let .wireGuardPort(port):
+ viewModel.setWireGuardPort(port)
+
+ if let cell = getCustomPortCell() {
+ cell.reset()
+ cell.textField.resignFirstResponder()
+ }
+
+ delegate?.preferencesDataSource(self, didSelectPort: port)
+
+ case .wireGuardCustomPort:
+ getCustomPortCell()?.textField.becomeFirstResponder()
+
+ default:
+ break
+ }
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
@@ -249,17 +363,23 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
switch sectionIdentifier {
case .contentBlockers:
- let view = tableView
+ guard let view = tableView
.dequeueReusableHeaderFooterView(
withIdentifier: HeaderFooterReuseIdentifiers.contentBlockerHeader.rawValue
- ) as! SettingsContentBlockersHeaderView
+ ) as? SettingsHeaderView else { return nil }
configureContentBlockersHeader(view)
return view
- case .customDNS:
- return tableView.dequeueReusableHeaderFooterView(
- withIdentifier: HeaderFooterReuseIdentifiers.spacer.rawValue
- )
+ case .wireGuardPorts:
+ guard let view = tableView
+ .dequeueReusableHeaderFooterView(
+ withIdentifier: HeaderFooterReuseIdentifiers.contentBlockerHeader.rawValue
+ ) as? SettingsHeaderView else { return nil }
+ configureWireguardPortsHeader(view)
+ return view
+
+ default:
+ return nil
}
}
@@ -271,13 +391,17 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
return nil
case .customDNS:
- let reusableView = tableView
+ guard let view = tableView
.dequeueReusableHeaderFooterView(
- withIdentifier: HeaderFooterReuseIdentifiers
- .customDNSFooter.rawValue
- ) as! SettingsStaticTextFooterView
- configureFooterView(reusableView)
- return reusableView
+ withIdentifier: HeaderFooterReuseIdentifiers.customDNSFooter.rawValue
+ ) as? SettingsStaticTextFooterView else { return nil }
+ configureFooterView(view)
+ return view
+
+ case .wireGuardPorts:
+ return tableView.dequeueReusableHeaderFooterView(
+ withIdentifier: HeaderFooterReuseIdentifiers.spacer.rawValue
+ )
}
}
@@ -285,11 +409,11 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
let sectionIdentifier = snapshot().sectionIdentifiers[section]
switch sectionIdentifier {
- case .contentBlockers:
- return UITableView.automaticDimension
-
case .customDNS:
- return UIMetrics.sectionSpacing
+ return 0
+
+ default:
+ return UITableView.automaticDimension
}
}
@@ -307,6 +431,9 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
case .conflictsWithOtherSettings, .emptyDNSDomains:
return UITableView.automaticDimension
}
+
+ case .wireGuardPorts:
+ return UIMetrics.sectionSpacing
}
}
@@ -378,17 +505,24 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
private func updateSnapshot(animated: Bool = false, completion: (() -> Void)? = nil) {
var newSnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
+ let oldSnapshot = snapshot()
newSnapshot.appendSections(Section.allCases)
- let oldSnapshot = snapshot()
- if oldSnapshot.indexOfSection(.contentBlockers) != nil {
+ if oldSnapshot.sectionIdentifiers.contains(.contentBlockers) {
newSnapshot.appendItems(
oldSnapshot.itemIdentifiers(inSection: .contentBlockers),
toSection: .contentBlockers
)
}
+ if oldSnapshot.sectionIdentifiers.contains(.wireGuardPorts) {
+ newSnapshot.appendItems(
+ oldSnapshot.itemIdentifiers(inSection: .wireGuardPorts),
+ toSection: .wireGuardPorts
+ )
+ }
+
newSnapshot.appendItems([.useCustomDNS], toSection: .customDNS)
let dnsServerItems = viewModel.customDNSDomains.map { entry in
@@ -400,7 +534,18 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
newSnapshot.appendItems([.addDNSServer], toSection: .customDNS)
}
- apply(newSnapshot, completion: completion)
+ applySnapshot(newSnapshot, animated: animated, completion: completion)
+ }
+
+ private func applySnapshot(
+ _ snapshot: NSDiffableDataSourceSnapshot<Section, Item>,
+ animated: Bool,
+ completion: (() -> Void)? = nil
+ ) {
+ apply(snapshot, animatingDifferences: animated) { [weak self] in
+ self?.selectRow(at: self?.indexPathForSelectedPort)
+ completion?()
+ }
}
private func reload(item: Item) {
@@ -500,7 +645,7 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
reloadCustomDNSFooter()
}
- return viewModel.validateDNSDomainUserInput(inputString)
+ return viewModel.isDNSDomainUserInputValid(inputString)
}
private func addDNSServerEntry() {
@@ -565,9 +710,9 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
}
- private func configureContentBlockersHeader(_ reusableView: SettingsContentBlockersHeaderView) {
+ private func configureContentBlockersHeader(_ reusableView: SettingsHeaderView) {
reusableView.titleLabel.text = NSLocalizedString(
- "BLOCK_ADS_CELL_LABEL",
+ "CONTENT_BLOCKERS_HEADER_LABEL",
tableName: "Preferences",
value: "DNS content blockers",
comment: ""
@@ -575,7 +720,7 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
reusableView.infoButtonHandler = { [weak self] in
if let self {
- self.delegate?.preferencesDataSource(self, didPressInfoButton: nil)
+ self.delegate?.preferencesDataSource(self, showInfo: .contentBlockers)
}
}
@@ -595,6 +740,47 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
}
}
+ private func configureWireguardPortsHeader(_ reusableView: SettingsHeaderView) {
+ reusableView.titleLabel.text = NSLocalizedString(
+ "WIRE_GUARD_PORTS_HEADER_LABEL",
+ tableName: "Preferences",
+ value: "WireGuard ports",
+ comment: ""
+ )
+
+ reusableView.infoButtonHandler = { [weak self] in
+ if let self {
+ self.delegate?.preferencesDataSource(self, showInfo: .wireGuardPorts)
+ }
+ }
+
+ reusableView.didCollapseHandler = { [weak self] headerView in
+ guard let self else { return }
+
+ var snapshot = self.snapshot()
+ var updateTimeDelay = 0.0
+
+ if headerView.isExpanded {
+ if let customPortCell = getCustomPortCell(), customPortCell.textField.isEditing {
+ revertWireGuardPortCellToLastSelection()
+ updateTimeDelay = 0.5
+ }
+
+ snapshot.deleteItems(Item.wireGuardPorts)
+ } else {
+ snapshot.appendItems(Item.wireGuardPorts, toSection: .wireGuardPorts)
+ }
+
+ // The update should be delayed when we're reverting an ongoing change, to give the
+ // user just enough time to notice it.
+ DispatchQueue.main.asyncAfter(deadline: .now() + updateTimeDelay) { [weak self] in
+ headerView.isExpanded.toggle()
+
+ self?.applySnapshot(snapshot, animated: true)
+ }
+ }
+ }
+
private func configureFooterView(_ reusableView: SettingsStaticTextFooterView) {
let font = reusableView.titleLabel.font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
@@ -602,6 +788,27 @@ final class PreferencesDataSource: UITableViewDiffableDataSource<
.attributedLocalizedDescription(isEditing: isEditing, preferredFont: font)
reusableView.titleLabel.sizeToFit()
+
+ // Applying background color of table view hides overflow from contracting cells below.
+ reusableView.contentView.backgroundColor = tableView?.backgroundColor
+ }
+
+ private func selectRow(at indexPath: IndexPath?, animated: Bool = false) {
+ tableView?.selectRow(at: indexPath, animated: false, scrollPosition: .none)
+ }
+
+ private func selectRow(at item: Item?, animated: Bool = false) {
+ guard let item else { return }
+
+ tableView?.selectRow(at: indexPath(for: item), animated: false, scrollPosition: .none)
+ }
+
+ private func getCustomPortCell() -> SettingsInputCell? {
+ if let customPortIndexPath = indexPath(for: .wireGuardCustomPort) {
+ return tableView?.cellForRow(at: customPortIndexPath) as? SettingsInputCell
+ }
+
+ return nil
}
}
@@ -642,7 +849,20 @@ extension PreferencesDataSource: PreferencesCellEventHandler {
return handleDNSEntryChange(with: identifier, inputString: inputString)
}
- func didPressInfoButton(for item: Item) {
- delegate?.preferencesDataSource(self, didPressInfoButton: item)
+ func showInfo(for item: InfoButtonItem) {
+ delegate?.preferencesDataSource(self, showInfo: item)
+ }
+
+ func addCustomPort(_ port: UInt16) {
+ viewModel.setWireGuardPort(port)
+ delegate?.preferencesDataSource(self, didSelectPort: port)
+ }
+
+ func selectCustomPortEntry(_ port: UInt16) -> Bool {
+ if indexPathForSelectedPort != indexPath(for: .wireGuardCustomPort) {
+ selectRow(at: .wireGuardCustomPort)
+ }
+
+ return viewModel.isPortWithinValidWireGuardRanges(port)
}
}
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
index b7242814fe..25db963fde 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
@@ -16,6 +16,11 @@ protocol PreferencesDataSourceDelegate: AnyObject {
func preferencesDataSource(
_ dataSource: PreferencesDataSource,
- didPressInfoButton item: PreferencesDataSource.Item?
+ showInfo for: PreferencesDataSource.InfoButtonItem?
+ )
+
+ func preferencesDataSource(
+ _ dataSource: PreferencesDataSource,
+ didSelectPort port: UInt16?
)
}
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift
index 8029669841..283154b00a 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift
@@ -7,26 +7,53 @@
//
import Foundation
+import RelayCache
final class PreferencesInteractor {
private let tunnelManager: TunnelManager
private var tunnelObserver: TunnelObserver?
+ private let relayCacheTracker: RelayCacheTracker
- var dnsSettingsDidChange: ((DNSSettings) -> Void)?
+ var tunnelSettingsDidChange: ((TunnelSettingsV2) -> Void)?
+ var cachedRelaysDidChange: ((CachedRelays) -> Void)?
- init(tunnelManager: TunnelManager) {
+ var tunnelSettings: TunnelSettingsV2 {
+ return tunnelManager.settings
+ }
+
+ var cachedRelays: CachedRelays? {
+ return try? relayCacheTracker.getCachedRelays()
+ }
+
+ init(tunnelManager: TunnelManager, relayCacheTracker: RelayCacheTracker) {
self.tunnelManager = tunnelManager
+ self.relayCacheTracker = relayCacheTracker
+
tunnelObserver =
TunnelBlockObserver(didUpdateTunnelSettings: { [weak self] manager, newSettings in
- self?.dnsSettingsDidChange?(newSettings.dnsSettings)
+ self?.tunnelSettingsDidChange?(newSettings)
})
}
- var dnsSettings: DNSSettings {
- return tunnelManager.settings.dnsSettings
- }
-
func setDNSSettings(_ newDNSSettings: DNSSettings, completion: (() -> Void)? = nil) {
tunnelManager.setDNSSettings(newDNSSettings, completionHandler: completion)
}
+
+ func setPort(_ port: UInt16?, completion: (() -> Void)? = nil) {
+ var relayConstraints = tunnelManager.settings.relayConstraints
+
+ if let port {
+ relayConstraints.port = .only(port)
+ } else {
+ relayConstraints.port = .any
+ }
+
+ tunnelManager.setRelayConstraints(relayConstraints, completionHandler: completion)
+ }
+}
+
+extension PreferencesInteractor: RelayCacheTrackerObserver {
+ func relayCacheTracker(_ tracker: RelayCacheTracker, didUpdateCachedRelays cachedRelays: CachedRelays) {
+ cachedRelaysDidChange?(cachedRelays)
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
index 93ccdaa854..30c01c8797 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
@@ -6,6 +6,7 @@
// Copyright © 2021 Mullvad VPN AB. All rights reserved.
//
+import RelayCache
import UIKit
class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate {
@@ -33,6 +34,8 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
tableView.separatorColor = .secondaryColor
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
+ tableView.estimatedSectionHeaderHeight = tableView.estimatedRowHeight
+ tableView.allowsSelectionDuringEditing = true
dataSource = PreferencesDataSource(tableView: tableView)
dataSource?.delegate = self
@@ -45,24 +48,31 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
)
navigationItem.rightBarButtonItem = editButtonItem
- interactor.dnsSettingsDidChange = { [weak self] newDNSSettings in
- self?.dataSource?.update(from: newDNSSettings)
+ interactor.tunnelSettingsDidChange = { [weak self] newSettings in
+ self?.dataSource?.update(from: newSettings)
+ }
+ dataSource?.update(from: interactor.tunnelSettings)
+
+ dataSource?.setAvailablePortRanges(interactor.cachedRelays?.relays.wireguard.portRanges ?? [])
+ interactor.cachedRelaysDidChange = { [weak self] cachedRelays in
+ self?.dataSource?.setAvailablePortRanges(cachedRelays.relays.wireguard.portRanges)
}
- dataSource?.update(from: interactor.dnsSettings)
tableView.tableHeaderView =
UIView(frame: .init(origin: .zero, size: .init(width: 0, height: UIMetrics.sectionSpacing)))
}
override func setEditing(_ editing: Bool, animated: Bool) {
+ _ = dataSource?.revertWireGuardPortCellToLastSelection()
+
+ super.setEditing(editing, animated: animated)
+
dataSource?.setEditing(editing, animated: animated)
navigationItem.setHidesBackButton(editing, animated: animated)
// Disable swipe to dismiss when editing
isModalInPresentation = editing
-
- super.setEditing(editing, animated: animated)
}
private func showContentBlockerInfo(with message: String) {
@@ -82,6 +92,18 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
alertPresenter.enqueue(alertController, presentingController: self)
}
+ private func humanReadablePortRepresentation(_ ranges: [[UInt16]]) -> String {
+ return ranges
+ .compactMap { range in
+ if let minPort = range.first, let maxPort = range.last {
+ return minPort == maxPort ? String(minPort) : "\(minPort)-\(maxPort)"
+ } else {
+ return nil
+ }
+ }
+ .joined(separator: ", ")
+ }
+
// MARK: - PreferencesDataSourceDelegate
func preferencesDataSource(
@@ -95,11 +117,19 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
func preferencesDataSource(
_ dataSource: PreferencesDataSource,
- didPressInfoButton item: PreferencesDataSource.Item?
+ showInfo item: PreferencesDataSource.InfoButtonItem?
) {
- let message: String
+ var message = ""
switch item {
+ case .contentBlockers:
+ message = NSLocalizedString(
+ "PREFERENCES_CONTENT_BLOCKERS_GENERAL",
+ tableName: "ContentBlockers",
+ value: "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more. This might cause issues on certain websites, services, and programs.",
+ comment: ""
+ )
+
case .blockMalware:
message = NSLocalizedString(
"PREFERENCES_CONTENT_BLOCKERS_MALWARE",
@@ -108,15 +138,27 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
comment: ""
)
- default:
+ case .wireGuardPorts:
+ let portsString = humanReadablePortRepresentation(
+ interactor.cachedRelays?.relays.wireguard
+ .portRanges ?? []
+ )
+
message = NSLocalizedString(
- "PREFERENCES_CONTENT_BLOCKERS_GENERAL",
- tableName: "ContentBlockers",
- value: "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more. This might cause issues on certain websites, services, and programs.",
+ "PREFERENCES_WIRE_GUARD_PORTS_GENERAL",
+ tableName: "WireGuardPorts",
+ value: "The automatic setting will randomly choose from the valid port ranges shown below.\n\nThe custom port can be any value inside the valid ranges:\n\n\(portsString)",
comment: ""
)
+
+ default:
+ assertionFailure("No matching InfoButtonItem")
}
showContentBlockerInfo(with: message)
}
+
+ func preferencesDataSource(_ dataSource: PreferencesDataSource, didSelectPort port: UInt16?) {
+ interactor.setPort(port)
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
index 372c00a25f..0ec1f08ef7 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
@@ -85,7 +85,11 @@ struct PreferencesViewModel: Equatable {
private(set) var blockAdultContent: Bool
private(set) var blockGambling: Bool
private(set) var enableCustomDNS: Bool
+ private(set) var wireGuardPort: UInt16?
var customDNSDomains: [DNSServerEntry]
+ var availableWireGuardPortRanges: [[UInt16]] = []
+
+ static let defaultWireGuardPorts: [UInt16] = [51820, 53]
mutating func setBlockAdvertising(_ newValue: Bool) {
blockAdvertising = newValue
@@ -118,6 +122,10 @@ struct PreferencesViewModel: Equatable {
enableCustomDNS = newValue
}
+ mutating func setWireGuardPort(_ newValue: UInt16?) {
+ wireGuardPort = newValue
+ }
+
/// Precondition for enabling Custom DNS.
var customDNSPrecondition: CustomDNSPrecondition {
if blockAdvertising || blockTracking || blockMalware || blockAdultContent || blockGambling {
@@ -140,7 +148,14 @@ struct PreferencesViewModel: Equatable {
return customDNSPrecondition == .satisfied && enableCustomDNS
}
- init(from dnsSettings: DNSSettings = DNSSettings()) {
+ var customWireGuardPort: UInt16? {
+ return wireGuardPort.flatMap { port in
+ Self.defaultWireGuardPorts.contains(port) ? nil : port
+ }
+ }
+
+ init(from tunnelSettings: TunnelSettingsV2 = TunnelSettingsV2()) {
+ let dnsSettings = tunnelSettings.dnsSettings
blockAdvertising = dnsSettings.blockingOptions.contains(.blockAdvertising)
blockTracking = dnsSettings.blockingOptions.contains(.blockTracking)
blockMalware = dnsSettings.blockingOptions.contains(.blockMalware)
@@ -150,34 +165,13 @@ struct PreferencesViewModel: Equatable {
customDNSDomains = dnsSettings.customDNSDomains.map { ipAddress in
return DNSServerEntry(identifier: UUID(), address: "\(ipAddress)")
}
+ wireGuardPort = tunnelSettings.relayConstraints.port.value
}
/// Produce merged view model keeping entry `identifier` for matching DNS entries.
func merged(_ other: PreferencesViewModel) -> PreferencesViewModel {
- var mergedViewModel = PreferencesViewModel()
-
- mergedViewModel.blockAdvertising = other.blockAdvertising
- mergedViewModel.blockTracking = other.blockTracking
- mergedViewModel.blockMalware = other.blockMalware
- mergedViewModel.blockAdultContent = other.blockAdultContent
- mergedViewModel.blockGambling = other.blockGambling
- mergedViewModel.enableCustomDNS = other.enableCustomDNS
-
- var oldDNSDomains = customDNSDomains
- for otherEntry in other.customDNSDomains {
- let sameEntryIndex = oldDNSDomains.firstIndex { entry in
- return entry.address == otherEntry.address
- }
-
- if let sameEntryIndex {
- let sourceEntry = oldDNSDomains[sameEntryIndex]
-
- mergedViewModel.customDNSDomains.append(sourceEntry)
- oldDNSDomains.remove(at: sameEntryIndex)
- } else {
- mergedViewModel.customDNSDomains.append(otherEntry)
- }
- }
+ var mergedViewModel = other
+ mergedViewModel.customDNSDomains = merge(customDNSDomains, with: other.customDNSDomains)
return mergedViewModel
}
@@ -256,7 +250,39 @@ struct PreferencesViewModel: Equatable {
}
/// Returns true if the given string is empty or a valid IP address.
- func validateDNSDomainUserInput(_ string: String) -> Bool {
+ func isDNSDomainUserInputValid(_ string: String) -> Bool {
return string.isEmpty || AnyIPAddress(string) != nil
}
+
+ /// Returns true if the given port is in within the supported ranges.
+ func isPortWithinValidWireGuardRanges(_ port: UInt16) -> Bool {
+ return availableWireGuardPortRanges.contains { range in
+ if let minPort = range.first, let maxPort = range.last {
+ return (minPort ... maxPort).contains(port)
+ }
+
+ return false
+ }
+ }
+
+ /// Replaces all old domains with new, keeping only those that share the same id and updating their content.
+ private func merge(_ oldDomains: [DNSServerEntry], with newDomains: [DNSServerEntry]) -> [DNSServerEntry] {
+ var oldDomains = oldDomains
+
+ return newDomains.map { otherEntry in
+ let sameEntryIndex = oldDomains.firstIndex { entry in
+ return entry.address == otherEntry.address
+ }
+
+ if let sameEntryIndex {
+ let sourceEntry = oldDomains[sameEntryIndex]
+
+ oldDomains.remove(at: sameEntryIndex)
+
+ return sourceEntry
+ } else {
+ return otherEntry
+ }
+ }
+ }
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift
index 555a1028ba..69243e39c0 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift
@@ -114,7 +114,7 @@ final class SelectLocationViewController: UIViewController {
comment: ""
)
- UISearchBar.SearchBarAppearance.inactive.apply(to: searchBar)
+ UITextField.SearchTextFieldAppearance.inactive.apply(to: searchBar)
}
}
@@ -124,10 +124,10 @@ extension SelectLocationViewController: UISearchBarDelegate {
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
- UISearchBar.SearchBarAppearance.active.apply(to: searchBar)
+ UITextField.SearchTextFieldAppearance.active.apply(to: searchBar)
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
- UISearchBar.SearchBarAppearance.inactive.apply(to: searchBar)
+ UITextField.SearchTextFieldAppearance.inactive.apply(to: searchBar)
}
}
diff --git a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift
new file mode 100644
index 0000000000..1f6f4c865a
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift
@@ -0,0 +1,41 @@
+//
+// SelectableSettingsCell.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-05-08.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class SelectableSettingsCell: SettingsCell {
+ let tickImageView: UIImageView = {
+ let imageView = UIImageView(image: UIImage(named: "IconTick"))
+ imageView.tintColor = .white
+ imageView.alpha = 0
+ return imageView
+ }()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ setLeftView(tickImageView)
+ selectedBackgroundView?.backgroundColor = UIColor.Cell.selectedBackgroundColor
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+
+ setLeftView(tickImageView)
+ }
+
+ override func setSelected(_ selected: Bool, animated: Bool) {
+ super.setSelected(selected, animated: animated)
+
+ tickImageView.alpha = selected ? 1 : 0
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
index 74a5b808e4..a6333462ee 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
@@ -31,9 +31,11 @@ enum SettingsDisclosureType {
class SettingsCell: UITableViewCell {
typealias InfoButtonHandler = () -> Void
+ let contentContainerSubviewMaxCount = 2
let titleLabel = UILabel()
let detailTitleLabel = UILabel()
let disclosureImageView = UIImageView(image: nil)
+ let contentContainer = UIStackView()
var infoButtonHandler: InfoButtonHandler?
var disclosureType: SettingsDisclosureType = .none {
@@ -103,22 +105,23 @@ class SettingsCell: UITableViewCell {
let buttonAreaWidth = UIMetrics.contentLayoutMargins.leading + UIMetrics
.contentLayoutMargins.trailing + buttonWidth
- contentView.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) {
+ let content = UIView()
+ content.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) {
switch style {
case .subtitle:
- titleLabel.pinEdgesToSuperviewMargins(.init([.top(0), .leading(0)]))
- detailTitleLabel.pinEdgesToSuperviewMargins(.all().excluding(.top))
+ titleLabel.pinEdgesToSuperview(.init([.top(0), .leading(0)]))
+ detailTitleLabel.pinEdgesToSuperview(.all().excluding(.top))
detailTitleLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1)
- infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: contentView.trailingAnchor)
+ infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: content.trailingAnchor)
default:
- titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.trailing))
- detailTitleLabel.pinEdgesToSuperviewMargins(.all().excluding(.leading))
+ titleLabel.pinEdgesToSuperview(.all().excluding(.trailing))
+ detailTitleLabel.pinEdgesToSuperview(.all().excluding(.leading))
detailTitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor)
}
infoButton.pinEdgesToSuperview(.init([.top(0)]))
- infoButton.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor)
+ infoButton.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor)
infoButton.leadingAnchor.constraint(
equalTo: titleLabel.trailingAnchor,
constant: -UIMetrics.interButtonSpacing
@@ -126,6 +129,13 @@ class SettingsCell: UITableViewCell {
infoButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor)
infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth)
}
+
+ contentContainer.addArrangedSubview(content)
+ contentContainer.spacing = 12
+
+ contentView.addConstrainedSubviews([contentContainer]) {
+ contentContainer.pinEdgesToSuperviewMargins()
+ }
}
required init?(coder: NSCoder) {
@@ -135,6 +145,7 @@ class SettingsCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
+ removeLeftView()
setInfoButtonIsVisible(false)
setLayoutMargins()
}
@@ -148,6 +159,20 @@ class SettingsCell: UITableViewCell {
infoButton.isHidden = !visible
}
+ func setLeftView(_ view: UIView) {
+ removeLeftView()
+
+ if contentContainer.arrangedSubviews.count <= 1 {
+ contentContainer.insertArrangedSubview(view, at: 0)
+ }
+ }
+
+ func removeLeftView() {
+ if contentContainer.arrangedSubviews.count >= contentContainerSubviewMaxCount {
+ contentContainer.arrangedSubviews.first?.removeFromSuperview()
+ }
+ }
+
@objc private func handleInfoButton(_ sender: UIControl) {
infoButtonHandler?()
}
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift b/ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift
index d9b413ba2e..51bb8e2742 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift
@@ -1,5 +1,5 @@
//
-// SettingsContentBlockersHeaderView.swift
+// SettingsHeaderView.swift
// MullvadVPN
//
// Created by Jon Petersson on 2023-04-06.
@@ -8,9 +8,9 @@
import UIKit
-class SettingsContentBlockersHeaderView: UITableViewHeaderFooterView {
+class SettingsHeaderView: UITableViewHeaderFooterView {
typealias InfoButtonHandler = () -> Void
- typealias CollapseHandler = (SettingsContentBlockersHeaderView) -> Void
+ typealias CollapseHandler = (SettingsHeaderView) -> Void
let titleLabel: UILabel = {
let titleLabel = UILabel()
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInputCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInputCell.swift
new file mode 100644
index 0000000000..8e494c1e11
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsInputCell.swift
@@ -0,0 +1,133 @@
+//
+// SettingsInputCell.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-05-05.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class SettingsInputCell: SelectableSettingsCell {
+ let textField = CustomTextField(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 30)))
+ var toolbarDoneButton = UIBarButtonItem()
+
+ var isValidInput: Bool {
+ didSet {
+ updateTextFieldInputValidity()
+ }
+ }
+
+ var inputDidChange: ((String) -> Void)?
+ var inputWasConfirmed: ((String) -> Void)?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ isValidInput = true
+
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ toolbarDoneButton = UIBarButtonItem(
+ title: NSLocalizedString(
+ "INPUT_CELL_TOOLBAR_BUTTON_DONE",
+ tableName: "Preferences",
+ value: "Done",
+ comment: ""
+ ),
+ style: .done,
+ target: self,
+ action: #selector(confirmInput)
+ )
+
+ accessoryView = textField
+
+ setUpTextField()
+ setUpTextFieldToolbar()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+
+ reset()
+ }
+
+ func reset() {
+ textField.text = nil
+ UITextField.SearchTextFieldAppearance.inactive.apply(to: textField)
+ }
+
+ func setInput(_ text: String) {
+ textField.text = text
+ textFieldDidChange(textField)
+ }
+
+ @objc func confirmInput() {
+ _ = textFieldShouldReturn(textField)
+ }
+
+ @objc private func textFieldDidChange(_ textField: UITextField) {
+ if let text = textField.text {
+ inputDidChange?(text)
+ toolbarDoneButton.isEnabled = isValidInput
+ }
+ }
+
+ private func setUpTextField() {
+ textField.borderStyle = .none
+ textField.layer.cornerRadius = 4
+ textField.font = .preferredFont(forTextStyle: .body)
+ textField.textAlignment = .right
+ textField.delegate = self
+ textField.keyboardType = .numberPad
+ textField.returnKeyType = .done
+ textField.textMargins = UIMetrics.settingsInputCellTextFieldLayoutMargins
+ textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
+
+ UITextField.SearchTextFieldAppearance.inactive.apply(to: textField)
+ }
+
+ private func setUpTextFieldToolbar() {
+ let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44))
+ toolbar.items = [
+ UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil),
+ toolbarDoneButton,
+ ]
+
+ toolbar.sizeToFit()
+
+ textField.inputAccessoryView = toolbar
+ }
+
+ private func updateTextFieldInputValidity() {
+ if isValidInput {
+ textField.textColor = textField.isEditing ? .SearchTextField.textColor : .SearchTextField.inactiveTextColor
+ } else {
+ textField.textColor = UIColor.TextField.invalidInputTextColor
+ }
+ }
+}
+
+extension SettingsInputCell: UITextFieldDelegate {
+ func textFieldDidBeginEditing(_ textField: UITextField) {
+ inputDidChange?(textField.text ?? "")
+ toolbarDoneButton.isEnabled = isValidInput
+
+ UITextField.SearchTextFieldAppearance.active.apply(to: textField)
+ }
+
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+ guard isValidInput else { return false }
+
+ inputWasConfirmed?(textField.text ?? "")
+ textField.resignFirstResponder()
+
+ return true
+ }
+
+ func textFieldDidEndEditing(_ textField: UITextField) {
+ UITextField.SearchTextFieldAppearance.inactive.apply(to: textField)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift
index 521e587570..0b9c719f37 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift
@@ -8,20 +8,24 @@
import Foundation
import MullvadREST
+import RelayCache
final class SettingsInteractorFactory {
private let storePaymentManager: StorePaymentManager
private let tunnelManager: TunnelManager
private let apiProxy: REST.APIProxy
+ private let relayCacheTracker: RelayCacheTracker
init(
storePaymentManager: StorePaymentManager,
tunnelManager: TunnelManager,
- apiProxy: REST.APIProxy
+ apiProxy: REST.APIProxy,
+ relayCacheTracker: RelayCacheTracker
) {
self.storePaymentManager = storePaymentManager
self.tunnelManager = tunnelManager
self.apiProxy = apiProxy
+ self.relayCacheTracker = relayCacheTracker
}
func makeAccountInteractor() -> AccountInteractor {
@@ -32,7 +36,7 @@ final class SettingsInteractorFactory {
}
func makePreferencesInteractor() -> PreferencesInteractor {
- return PreferencesInteractor(tunnelManager: tunnelManager)
+ return PreferencesInteractor(tunnelManager: tunnelManager, relayCacheTracker: relayCacheTracker)
}
func makeProblemReportInteractor() -> ProblemReportInteractor {
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift b/ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift
index a75363d420..e99932dbeb 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift
@@ -30,8 +30,11 @@ class SettingsStaticTextFooterView: UITableViewHeaderFooterView {
.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
titleLabel.trailingAnchor
.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
- titleLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor)
- .withPriority(.defaultLow),
+ contentView.layoutMarginsGuide.bottomAnchor.constraint(
+ equalToSystemSpacingBelow: titleLabel.bottomAnchor,
+ multiplier: 1
+ )
+ .withPriority(.defaultLow),
])
}
diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift
index f8f9eb60bc..fe7f16e4f6 100644
--- a/ios/MullvadVPNTests/RelaySelectorTests.swift
+++ b/ios/MullvadVPNTests/RelaySelectorTests.swift
@@ -51,6 +51,18 @@ class RelaySelectorTests: XCTestCase {
XCTAssertEqual(result.relay.hostname, "se6-wireguard")
}
+ func testSpecificPortConstraint() throws {
+ let constraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard")), port: .only(1))
+
+ let result = try RelaySelector.evaluate(
+ relays: sampleRelays,
+ constraints: constraints,
+ numberOfFailedAttempts: 0
+ )
+
+ XCTAssertEqual(result.endpoint.ipv4Relay.port, 1)
+ }
+
func testRandomPortSelectionWithFailedAttempts() throws {
let constraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard")))
let allPorts = portRanges.flatMap { $0 }
diff --git a/ios/RelaySelector/RelaySelector.swift b/ios/RelaySelector/RelaySelector.swift
index 38a1c4d3ab..956461751b 100644
--- a/ios/RelaySelector/RelaySelector.swift
+++ b/ios/RelaySelector/RelaySelector.swift
@@ -101,12 +101,18 @@ public enum RelaySelector {
rawPortRanges: [[UInt16]],
numberOfFailedAttempts: UInt
) -> UInt16? {
- // 1. First two attempts should pick a random port.
- // 2. The next two should pick port 53.
- // 3. Repeat steps 1 and 2.
- let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
+ switch constraints.port {
+ case let .only(port):
+ return port
- return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
+ case .any:
+ // 1. First two attempts should pick a random port.
+ // 2. The next two should pick port 53.
+ // 3. Repeat steps 1 and 2.
+ let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
+
+ return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
+ }
}
private static func pickRandomRelay(relays: [RelayWithLocation]) -> RelayWithLocation? {