summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-05-20 13:11:52 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-05-27 14:36:59 +0200
commitf66690efdd956f9f8be76720386a28136f011052 (patch)
tree2ff05c43f8f5d63535fc85e92efe356b739be218
parent488649a3bd05e6391054d61a44f5ac76d0a16791 (diff)
downloadmullvadvpn-f66690efdd956f9f8be76720386a28136f011052.tar.xz
mullvadvpn-f66690efdd956f9f8be76720386a28136f011052.zip
Add preferences
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj16
-rw-r--r--ios/MullvadVPN/CustomSwitch.swift72
-rw-r--r--ios/MullvadVPN/CustomSwitchContainer.swift66
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift126
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift4
-rw-r--r--ios/MullvadVPN/SettingsSwitchCell.swift36
-rw-r--r--ios/MullvadVPN/SettingsViewController.swift12
-rw-r--r--ios/MullvadVPN/UIColor+Palette.swift6
8 files changed, 337 insertions, 1 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index ac4e13422f..9d9d32f83e 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -153,6 +153,10 @@
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; };
58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; };
58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; };
+ 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; };
+ 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; };
+ 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; };
+ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; };
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; };
@@ -369,6 +373,10 @@
58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; };
58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; };
58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; };
+ 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; };
+ 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; };
+ 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; };
+ 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitchContainer.swift; sourceTree = "<group>"; };
58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; };
58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; };
58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -678,6 +686,10 @@
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */,
5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */,
584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */,
+ 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */,
+ 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
+ 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */,
+ 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -1007,8 +1019,10 @@
58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */,
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */,
+ 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */,
5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */,
+ 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
@@ -1070,6 +1084,7 @@
5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
+ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */,
58C3B06924EAA25000C0348E /* StringStreamIterator.swift in Sources */,
@@ -1105,6 +1120,7 @@
58C4CB0124EBE5A700A22D49 /* LogEntryParser.swift in Sources */,
58F840B22464491D0044E708 /* ChainedError.swift in Sources */,
58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */,
+ 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/ios/MullvadVPN/CustomSwitch.swift b/ios/MullvadVPN/CustomSwitch.swift
new file mode 100644
index 0000000000..b804dae7ae
--- /dev/null
+++ b/ios/MullvadVPN/CustomSwitch.swift
@@ -0,0 +1,72 @@
+//
+// CustomSwitch.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/05/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class CustomSwitch: UISwitch {
+
+ /// Returns the private `UISwitch` background view
+ private var backgroundView: UIView? {
+ // Go two levels deep only
+ let subviewsToExamine = subviews.flatMap { (view) -> [UIView] in
+ return [view] + view.subviews
+ }
+
+ // Find the first subview that has background color set.
+ let backgroundView = subviewsToExamine.first { (subview) in
+ return subview.backgroundColor != nil
+ }
+
+ return backgroundView
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ self.tintColor = .clear
+ self.onTintColor = .clear
+
+ updateThumbColor(isOn: self.isOn, animated: false)
+
+ addTarget(self, action: #selector(valueChanged(_:)), for: .valueChanged)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func setOn(_ on: Bool, animated: Bool) {
+ super.setOn(on, animated: animated)
+
+ updateThumbColor(isOn: on, animated: animated)
+ }
+
+ private func updateThumbColor(isOn: Bool, animated: Bool) {
+ let actions = {
+ self.thumbTintColor = isOn ? UIColor.Switch.onThumbColor : UIColor.Switch.offThumbColor
+ self.backgroundView?.backgroundColor = .clear
+ }
+
+ if animated {
+ UIView.animate(withDuration: 0.25, animations: actions)
+ } else {
+ actions()
+ }
+ }
+
+ @objc private func valueChanged(_ sender: Any) {
+ if #available(iOS 13, *) {
+ self.updateThumbColor(isOn: self.isOn, animated: true)
+ } else {
+ // Wait for animations to finish before changing the thumb color to prevent the jumpy behaviour.
+ CATransaction.setCompletionBlock {
+ self.updateThumbColor(isOn: self.isOn, animated: false)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/CustomSwitchContainer.swift b/ios/MullvadVPN/CustomSwitchContainer.swift
new file mode 100644
index 0000000000..ebdcc33b13
--- /dev/null
+++ b/ios/MullvadVPN/CustomSwitchContainer.swift
@@ -0,0 +1,66 @@
+//
+// CustomSwitchContainer.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/05/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class CustomSwitchContainer: UIView {
+ static let borderEdgeInsets = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3)
+
+ private let borderShape: CAShapeLayer = {
+ let shapeLayer = CAShapeLayer()
+ shapeLayer.borderColor = UIColor.Switch.borderColor.cgColor
+ shapeLayer.borderWidth = 2
+ if #available(iOS 13.0, *) {
+ shapeLayer.cornerCurve = .continuous
+ }
+ return shapeLayer
+ }()
+
+ let control = CustomSwitch()
+
+ override var intrinsicContentSize: CGSize {
+ return controlSize()
+ }
+
+ override func sizeThatFits(_ size: CGSize) -> CGSize {
+ return controlSize()
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ addSubview(control)
+ layer.addSublayer(borderShape)
+
+ control.sizeToFit()
+ sizeToFit()
+
+ borderShape.cornerRadius = self.bounds.height * 0.5
+ borderShape.frame = self.bounds
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ control.frame.origin = CGPoint(x: Self.borderEdgeInsets.left, y: Self.borderEdgeInsets.top)
+ }
+
+ // MARK: - Private
+
+ private func controlSize() -> CGSize {
+ var size = control.bounds.size
+ size.width += Self.borderEdgeInsets.left + Self.borderEdgeInsets.right
+ size.height += Self.borderEdgeInsets.top + Self.borderEdgeInsets.bottom
+ return size
+ }
+
+}
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift
new file mode 100644
index 0000000000..4220d273e5
--- /dev/null
+++ b/ios/MullvadVPN/PreferencesViewController.swift
@@ -0,0 +1,126 @@
+//
+// PreferencesViewController.swift
+// MullvadVPN
+//
+// Created by pronebird on 19/05/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+import Logging
+
+class PreferencesViewController: UITableViewController, TunnelObserver {
+
+ private let logger = Logger(label: "PreferencesViewController")
+ private var dnsSettings: DNSSettings?
+
+ private enum CellIdentifier: String {
+ case switchCell
+ }
+
+ private let staticDataSource = PreferencesTableViewDataSource()
+
+ init() {
+ super.init(style: .grouped)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableView.backgroundColor = .secondaryColor
+ tableView.separatorColor = .secondaryColor
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.estimatedRowHeight = 60
+ tableView.sectionHeaderHeight = 18
+ tableView.sectionFooterHeight = 18
+
+ tableView.dataSource = staticDataSource
+ tableView.delegate = staticDataSource
+
+ tableView.register(SettingsSwitchCell.self, forCellReuseIdentifier: CellIdentifier.switchCell.rawValue)
+
+ navigationItem.title = NSLocalizedString("Preferences", comment: "Navigation title")
+ navigationItem.largeTitleDisplayMode = .always
+
+ TunnelManager.shared.addObserver(self)
+ self.dnsSettings = TunnelManager.shared.tunnelSettings?.interface.dnsSettings
+
+ setupDataSource()
+ }
+
+ // MARK: - TunnelObserver
+
+ func tunnelStateDidChange(tunnelState: TunnelState) {
+ // no-op
+ }
+
+ func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) {
+ DispatchQueue.main.async {
+ if tunnelSettings?.interface.dnsSettings != self.dnsSettings {
+ self.dnsSettings = tunnelSettings?.interface.dnsSettings
+ self.tableView.reloadData()
+ }
+ }
+ }
+
+ // MARK: - Private
+
+ private func setupDataSource() {
+ let blockAdvertisingRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.switchCell.rawValue) { (indexPath, cell) in
+ let cell = cell as! SettingsSwitchCell
+
+ cell.titleLabel.text = NSLocalizedString("Block ads", comment: "")
+ cell.switchControl.setOn(self.dnsSettings?.blockAdvertising ?? false, animated: false)
+ cell.action = { [weak self] (isOn) in
+ self?.dnsSettings?.blockAdvertising = isOn
+ self?.saveDNSSettings()
+ }
+ }
+ blockAdvertisingRow.isSelectable = false
+
+ let blockTrackingRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.switchCell.rawValue) { (indexPath, cell) in
+ let cell = cell as! SettingsSwitchCell
+
+ cell.titleLabel.text = NSLocalizedString("Block trackers", comment: "")
+ cell.switchControl.setOn(self.dnsSettings?.blockTracking ?? false, animated: false)
+ cell.action = { [weak self] (isOn) in
+ self?.dnsSettings?.blockTracking = isOn
+ self?.saveDNSSettings()
+ }
+ }
+ blockTrackingRow.isSelectable = false
+
+ let section = StaticTableViewSection()
+ section.addRows([blockAdvertisingRow, blockTrackingRow])
+ staticDataSource.addSections([section])
+ }
+
+ private func saveDNSSettings() {
+ guard let dnsSettings = dnsSettings else { return }
+
+ TunnelManager.shared.setDNSSettings(dnsSettings) { [weak self] (result) in
+ if case .failure(let error) = result {
+ self?.logger.error(chainedError: error, message: "Failed to save DNS settings")
+ }
+ }
+ }
+
+}
+
+class PreferencesTableViewDataSource: StaticTableViewDataSource {
+
+ // MARK: - UITableViewDelegate
+
+ func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+ return 24
+ }
+
+ func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+ return 0.01
+ }
+
+}
diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift
index 59ff50d499..0bb586f8b6 100644
--- a/ios/MullvadVPN/SettingsNavigationController.swift
+++ b/ios/MullvadVPN/SettingsNavigationController.swift
@@ -11,6 +11,7 @@ import UIKit
enum SettingsNavigationRoute {
case account
+ case preferences
case wireguardKeys
case problemReport
}
@@ -79,6 +80,9 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont
controller.delegate = self
pushViewController(controller, animated: animated)
+ case .preferences:
+ pushViewController(PreferencesViewController(), animated: animated)
+
case .wireguardKeys:
pushViewController(WireguardKeysViewController(), animated: animated)
diff --git a/ios/MullvadVPN/SettingsSwitchCell.swift b/ios/MullvadVPN/SettingsSwitchCell.swift
new file mode 100644
index 0000000000..d644c5cf77
--- /dev/null
+++ b/ios/MullvadVPN/SettingsSwitchCell.swift
@@ -0,0 +1,36 @@
+//
+// SettingsSwitchCell.swift
+// MullvadVPN
+//
+// Created by pronebird on 19/05/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class SettingsSwitchCell: SettingsCell {
+
+ let switchContainer = CustomSwitchContainer()
+ var switchControl: CustomSwitch {
+ return switchContainer.control
+ }
+
+ var action: ((Bool) -> Void)?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ self.accessoryView = switchContainer
+
+ switchControl.addTarget(self, action: #selector(switchValueDidChange), for: .valueChanged)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func switchValueDidChange() {
+ self.action?(self.switchControl.isOn)
+ }
+
+}
diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift
index 31eef92835..67a2d78d99 100644
--- a/ios/MullvadVPN/SettingsViewController.swift
+++ b/ios/MullvadVPN/SettingsViewController.swift
@@ -103,6 +103,16 @@ class SettingsViewController: UITableViewController, AccountObserver {
self?.settingsNavigationController?.navigate(to: .account, animated: true)
}
+ let preferencesRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.basicCell.rawValue) { (_, cell) in
+ let cell = cell as! SettingsCell
+ cell.titleLabel.text = NSLocalizedString("Preferences", comment: "")
+ cell.accessoryType = .disclosureIndicator
+ }
+
+ preferencesRow.actionBlock = { [weak self] (indexPath) in
+ self?.settingsNavigationController?.navigate(to: .preferences, animated: true)
+ }
+
let wireguardKeyRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.basicCell.rawValue) { (_, cell) in
let cell = cell as! SettingsCell
@@ -117,7 +127,7 @@ class SettingsViewController: UITableViewController, AccountObserver {
self.accountRow = accountRow
- topSection.addRows([accountRow, wireguardKeyRow])
+ topSection.addRows([accountRow, preferencesRow, wireguardKeyRow])
staticDataSource.addSections([topSection])
}
diff --git a/ios/MullvadVPN/UIColor+Palette.swift b/ios/MullvadVPN/UIColor+Palette.swift
index b883cbf2b7..b50b31fe70 100644
--- a/ios/MullvadVPN/UIColor+Palette.swift
+++ b/ios/MullvadVPN/UIColor+Palette.swift
@@ -41,6 +41,12 @@ extension UIColor {
static let disabledTitleColor = UIColor.lightGray
}
+ enum Switch {
+ static let borderColor = UIColor(white: 1.0, alpha: 0.8)
+ static let onThumbColor = successColor
+ static let offThumbColor = dangerColor
+ }
+
// Relay availability indicator view
enum RelayStatusIndicator {
static let activeColor = successColor.withAlphaComponent(0.9)