summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNikolay Davydov <nik.davidov@gmail.com>2022-08-23 12:02:09 +0400
committerAndrej Mihajlov <and@mullvad.net>2022-08-23 13:34:27 +0200
commitbf44a57198cf08468c8f38de31194c189e8f5416 (patch)
tree47bb1a13b3236e5e2ce95b81cc9eb4c72f0a62e7
parent066614cfbe34fc9d05f3156405214d1ec3523480 (diff)
downloadmullvadvpn-bf44a57198cf08468c8f38de31194c189e8f5416.tar.xz
mullvadvpn-bf44a57198cf08468c8f38de31194c189e8f5416.zip
Add shortcuts manager
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift1
-rw-r--r--ios/MullvadVPN/SettingsCell.swift14
-rw-r--r--ios/MullvadVPN/ShortcutsDataSource.swift202
-rw-r--r--ios/MullvadVPN/ShortcutsManager.swift64
-rw-r--r--ios/MullvadVPN/ShortcutsViewController.swift51
6 files changed, 229 insertions, 107 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 0c7c5a77d5..eb9a155cbe 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -307,6 +307,7 @@
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB45260A028D00A621A8 /* GeoJSON.swift */; };
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
+ 753D6C0C28B4BF3E0052D9E1 /* ShortcutsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753D6C0B28B4BF3E0052D9E1 /* ShortcutsManager.swift */; };
75FD0C2128B108570021E33E /* ShortcutsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */; };
75FD0C2328B109860021E33E /* ShortcutsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */; };
75FD0C2528B117D30021E33E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */; };
@@ -601,6 +602,7 @@
58FEEB45260A028D00A621A8 /* GeoJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoJSON.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>"; };
+ 753D6C0B28B4BF3E0052D9E1 /* ShortcutsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsManager.swift; sourceTree = "<group>"; };
75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsDataSource.swift; sourceTree = "<group>"; };
75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsDataSourceDelegate.swift; sourceTree = "<group>"; };
75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = "<group>"; };
@@ -968,6 +970,7 @@
75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */,
75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */,
75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */,
+ 753D6C0B28B4BF3E0052D9E1 /* ShortcutsManager.swift */,
584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
58CCA01122424D11004F3011 /* SettingsViewController.swift */,
@@ -1469,6 +1472,7 @@
5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */,
5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */,
5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
+ 753D6C0C28B4BF3E0052D9E1 /* ShortcutsManager.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */,
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */,
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index e9ba018d5b..246e348c48 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -147,6 +147,7 @@ class SceneDelegate: UIResponder {
RelayCache.Tracker.shared.startPeriodicUpdates()
TunnelManager.shared.startPeriodicPrivateKeyRotation()
AddressCache.Tracker.shared.startPeriodicUpdates()
+ ShortcutsManager.shared.updateVoiceShortcuts()
setShowsPrivacyOverlay(false)
}
diff --git a/ios/MullvadVPN/SettingsCell.swift b/ios/MullvadVPN/SettingsCell.swift
index 3aaf1ee67b..16f48a1628 100644
--- a/ios/MullvadVPN/SettingsCell.swift
+++ b/ios/MullvadVPN/SettingsCell.swift
@@ -12,6 +12,7 @@ enum SettingsDisclosureType {
case none
case chevron
case externalLink
+ case tick
var image: UIImage? {
switch self {
@@ -21,6 +22,8 @@ enum SettingsDisclosureType {
return UIImage(named: "IconChevron")
case .externalLink:
return UIImage(named: "IconExtlink")
+ case .tick:
+ return .iconTickSmall
}
}
}
@@ -166,3 +169,14 @@ class SettingsCell: UITableViewCell {
contentView.frame = contentView.frame.inset(by: contentInset)
}
}
+
+private extension UIImage {
+ static let iconTickSmall: UIImage? = {
+ guard let image = UIImage(named: "IconTick") else { return nil }
+ let size = CGSize(width: 16, height: 16)
+ return UIGraphicsImageRenderer(size: size).image { context in
+ let rect = CGRect(origin: .zero, size: size)
+ image.draw(in: rect)
+ }
+ }()
+}
diff --git a/ios/MullvadVPN/ShortcutsDataSource.swift b/ios/MullvadVPN/ShortcutsDataSource.swift
index 1719af12c1..429d196246 100644
--- a/ios/MullvadVPN/ShortcutsDataSource.swift
+++ b/ios/MullvadVPN/ShortcutsDataSource.swift
@@ -9,7 +9,11 @@
import IntentsUI
import UIKit
-final class ShortcutsDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
+final class ShortcutsDataSource: NSObject,
+ UITableViewDataSource,
+ UITableViewDelegate,
+ ShortcutsManagerDelegate
+{
private enum CellReuseIdentifiers: String, CaseIterable {
case basicCell
@@ -36,45 +40,86 @@ final class ShortcutsDataSource: NSObject, UITableViewDataSource, UITableViewDel
case shortcuts
}
- enum Item: String, CaseIterable {
- case start
- case reconnect
- case stop
+ struct Item: Hashable {
+ let title: String
+ let shortcut: INShortcut
+ let voiceShortcut: INVoiceShortcut?
+
+ var isAdded: Bool {
+ return voiceShortcut != nil
+ }
}
private var snapshot = DataSourceSnapshot<Section, Item>()
weak var delegate: ShortcutsDataSourceDelegate?
- func configure(_ tableView: UITableView) {
+ weak var tableView: UITableView? {
+ didSet {
+ tableView?.delegate = self
+ tableView?.dataSource = self
+
+ registerClasses()
+ }
+ }
+
+ override init() {
+ super.init()
+ updateDataSnapshot(voiceShortcuts: [])
+ ShortcutsManager.shared.delegate = self
+ ShortcutsManager.shared.updateVoiceShortcuts()
+ }
+
+ private func registerClasses() {
CellReuseIdentifiers.allCases.forEach { cellIdentifier in
- tableView.register(
+ tableView?.register(
cellIdentifier.reusableViewClass,
forCellReuseIdentifier: cellIdentifier.rawValue
)
}
+
HeaderFooterReuseIdentifier.allCases.forEach { reuseIdentifier in
- tableView.register(
+ tableView?.register(
reuseIdentifier.reusableViewClass,
forHeaderFooterViewReuseIdentifier: reuseIdentifier.rawValue
)
}
- tableView.dataSource = self
- tableView.delegate = self
}
- override init() {
- super.init()
- updateDataSnapshot()
- }
+ private func updateDataSnapshot(voiceShortcuts: [INVoiceShortcut]) {
+ var items = [Item]()
+
+ for data in ShortcutData.allCases {
+ guard let shortcut = data.shortcut else { continue }
+ let voiceShortcut = voiceShortcuts.first(where: { voiceShortcut in
+ isVoiceShortcut(voiceShortcut, invokes: shortcut)
+ })
+ let item = Item(
+ title: data.title,
+ shortcut: shortcut,
+ voiceShortcut: voiceShortcut
+ )
+ items.append(item)
+ }
- private func updateDataSnapshot() {
var newSnapshot = DataSourceSnapshot<Section, Item>()
newSnapshot.appendSections([.shortcuts])
- newSnapshot.appendItems(Item.allCases, in: .shortcuts)
+ newSnapshot.appendItems(items, in: .shortcuts)
+
snapshot = newSnapshot
}
+ /// Returns whether the voice shortcut performs the same action as the specified shortcut.
+ private func isVoiceShortcut(
+ _ voiceShortcut: INVoiceShortcut,
+ invokes shortcut: INShortcut
+ ) -> Bool {
+ if let a = voiceShortcut.shortcut.intent, let b = shortcut.intent {
+ return type(of: a) == type(of: b)
+ }
+ return false
+ }
+
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -88,17 +133,15 @@ final class ShortcutsDataSource: NSObject, UITableViewDataSource, UITableViewDel
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = snapshot.itemForIndexPath(indexPath)!
- switch item {
- case .start, .reconnect, .stop:
- let cell = tableView.dequeueReusableCell(
- withIdentifier: CellReuseIdentifiers.basicCell.rawValue,
- for: indexPath
- )
- if let cell = cell as? SettingsCell {
- cell.titleLabel.text = item.title
- }
- return cell
+ let cell = tableView.dequeueReusableCell(
+ withIdentifier: CellReuseIdentifiers.basicCell.rawValue,
+ for: indexPath
+ )
+ if let cell = cell as? SettingsCell {
+ cell.titleLabel.text = item.title
+ cell.disclosureType = item.isAdded ? .tick : .none
}
+ return cell
}
// MARK: - UITableViewDelegate
@@ -126,63 +169,66 @@ final class ShortcutsDataSource: NSObject, UITableViewDataSource, UITableViewDel
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0
}
-}
-extension ShortcutsDataSource.Item {
- var title: String {
- switch self {
- case .start:
- return NSLocalizedString(
- "SHORTCUTS_NAME_START_VPN",
- tableName: "Shortcuts",
- value: "Start VPN",
- comment: ""
- )
- case .reconnect:
- return NSLocalizedString(
- "SHORTCUTS_NAME_RECONNECT_VPN",
- tableName: "Shortcuts",
- value: "Reconnect VPN",
- comment: ""
- )
- case .stop:
- return NSLocalizedString(
- "SHORTCUTS_NAME_STOP_VPN",
- tableName: "Shortcuts",
- value: "Stop VPN",
- comment: ""
- )
- }
+ // MARK: - ShortcutsManagerDelegate
+
+ func shortcutsManager(
+ _ shortcutsManager: ShortcutsManager,
+ didReceiveVoiceShortcuts voiceShortcuts: [INVoiceShortcut]
+ ) {
+ updateDataSnapshot(voiceShortcuts: voiceShortcuts)
+ tableView?.reloadData()
}
+}
- var shortcut: INShortcut? {
- let intent: INIntent
- switch self {
- case .start:
- intent = StartVPNIntent()
- case .reconnect:
- intent = ReconnectVPNIntent()
- case .stop:
- intent = StopVPNIntent()
- }
- intent.suggestedInvocationPhrase = title
- guard let shortcut = INShortcut(intent: intent) else {
- assertionFailure("The shortcut has an invalid intent.")
- return nil
+private extension ShortcutsDataSource {
+ enum ShortcutData: CaseIterable {
+ case start
+ case reconnect
+ case stop
+
+ var title: String {
+ switch self {
+ case .start:
+ return NSLocalizedString(
+ "SHORTCUTS_NAME_START_VPN",
+ tableName: "Shortcuts",
+ value: "Start VPN",
+ comment: ""
+ )
+ case .reconnect:
+ return NSLocalizedString(
+ "SHORTCUTS_NAME_RECONNECT_VPN",
+ tableName: "Shortcuts",
+ value: "Reconnect VPN",
+ comment: ""
+ )
+ case .stop:
+ return NSLocalizedString(
+ "SHORTCUTS_NAME_STOP_VPN",
+ tableName: "Shortcuts",
+ value: "Stop VPN",
+ comment: ""
+ )
+ }
}
- return shortcut
- }
- init?(voiceShortcut: INVoiceShortcut) {
- switch voiceShortcut.shortcut.intent {
- case is StartVPNIntent:
- self = .start
- case is ReconnectVPNIntent:
- self = .reconnect
- case is StopVPNIntent:
- self = .stop
- default:
- return nil
+ var shortcut: INShortcut? {
+ let intent: INIntent
+ switch self {
+ case .start:
+ intent = StartVPNIntent()
+ case .reconnect:
+ intent = ReconnectVPNIntent()
+ case .stop:
+ intent = StopVPNIntent()
+ }
+ intent.suggestedInvocationPhrase = title
+ guard let shortcut = INShortcut(intent: intent) else {
+ assertionFailure("The shortcut has an invalid intent.")
+ return nil
+ }
+ return shortcut
}
}
}
diff --git a/ios/MullvadVPN/ShortcutsManager.swift b/ios/MullvadVPN/ShortcutsManager.swift
new file mode 100644
index 0000000000..4a6309429c
--- /dev/null
+++ b/ios/MullvadVPN/ShortcutsManager.swift
@@ -0,0 +1,64 @@
+//
+// ShortcutsManager.swift
+// MullvadVPN
+//
+// Created by Nikolay Davydov on 23.08.2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import IntentsUI
+import Logging
+
+protocol ShortcutsManagerDelegate: AnyObject {
+ func shortcutsManager(
+ _ shortcutsManager: ShortcutsManager,
+ didReceiveVoiceShortcuts voiceShortcuts: [INVoiceShortcut]
+ )
+}
+
+final class ShortcutsManager {
+ static let shared = ShortcutsManager()
+
+ private init() {}
+
+ private let logger = Logger(label: "ShortcutsManager")
+
+ private var voiceShortcutsByID = [UUID: INVoiceShortcut]() {
+ didSet {
+ let voiceShortcuts = voiceShortcutsByID.map { $0.value }
+ delegate?.shortcutsManager(self, didReceiveVoiceShortcuts: voiceShortcuts)
+ }
+ }
+
+ weak var delegate: ShortcutsManagerDelegate?
+
+ func updateVoiceShortcuts() {
+ guard delegate != nil else { return }
+ INVoiceShortcutCenter.shared.getAllVoiceShortcuts { [weak self] voiceShortcuts, error in
+ guard let self = self else { return }
+ if let error = error {
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to fetch voice shortcuts."
+ )
+ return
+ }
+ let voiceShortcuts = voiceShortcuts ?? []
+ let voiceShortcutsByID = voiceShortcuts
+ .reduce(into: [UUID: INVoiceShortcut]()) { result, voiceShortcut in
+ result[voiceShortcut.identifier] = voiceShortcut
+ }
+ DispatchQueue.main.async {
+ self.voiceShortcutsByID = voiceShortcutsByID
+ }
+ }
+ }
+
+ func addVoiceShortcut(_ voiceShortcut: INVoiceShortcut) {
+ voiceShortcutsByID[voiceShortcut.identifier] = voiceShortcut
+ }
+
+ func deleteVoiceShortcut(withIdentifier identifier: UUID) {
+ voiceShortcutsByID[identifier] = nil
+ }
+}
diff --git a/ios/MullvadVPN/ShortcutsViewController.swift b/ios/MullvadVPN/ShortcutsViewController.swift
index 94b6abba0b..953639dca3 100644
--- a/ios/MullvadVPN/ShortcutsViewController.swift
+++ b/ios/MullvadVPN/ShortcutsViewController.swift
@@ -36,7 +36,7 @@ final class ShortcutsViewController: UITableViewController,
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
- dataSource.configure(tableView)
+ dataSource.tableView = tableView
dataSource.delegate = self
navigationItem.title = NSLocalizedString(
@@ -47,41 +47,28 @@ final class ShortcutsViewController: UITableViewController,
)
}
- private func handleSelectShortcut(_ shortcut: INShortcut, item: ShortcutsDataSource.Item) {
- INVoiceShortcutCenter.shared.getAllVoiceShortcuts { [weak self] shortcuts, error in
- DispatchQueue.main.async {
- guard let self = self else { return }
- let controller: UIViewController
- if let voiceShortcut = shortcuts?.first(where: { voiceShortcut in
- ShortcutsDataSource.Item(voiceShortcut: voiceShortcut) == item
- }) {
- let editShortcutController = INUIEditVoiceShortcutViewController(
- voiceShortcut: voiceShortcut
- )
- editShortcutController.delegate = self
- controller = editShortcutController
- } else {
- let addShortcutController = INUIAddVoiceShortcutViewController(
- shortcut: shortcut
- )
- addShortcutController.delegate = self
- controller = addShortcutController
- }
- controller.modalPresentationStyle = .formSheet
- self.present(controller, animated: true)
- }
- }
- }
-
// MARK: - ShortcutsDataSourceDelegate
func shortcutsDataSource(
_ dataSource: ShortcutsDataSource,
didSelectItem item: ShortcutsDataSource.Item
) {
- if let shortcut = item.shortcut {
- handleSelectShortcut(shortcut, item: item)
+ let controller: UIViewController
+ if let voiceShortcut = item.voiceShortcut {
+ let editShortcutController = INUIEditVoiceShortcutViewController(
+ voiceShortcut: voiceShortcut
+ )
+ editShortcutController.delegate = self
+ controller = editShortcutController
+ } else {
+ let addShortcutController = INUIAddVoiceShortcutViewController(
+ shortcut: item.shortcut
+ )
+ addShortcutController.delegate = self
+ controller = addShortcutController
}
+ controller.modalPresentationStyle = .formSheet
+ present(controller, animated: true)
}
// MARK: - INUIAddVoiceShortcutViewControllerDelegate
@@ -91,6 +78,9 @@ final class ShortcutsViewController: UITableViewController,
didFinishWith voiceShortcut: INVoiceShortcut?,
error: Error?
) {
+ if let voiceShortcut = voiceShortcut {
+ ShortcutsManager.shared.addVoiceShortcut(voiceShortcut)
+ }
controller.dismiss(animated: true)
}
@@ -112,6 +102,9 @@ final class ShortcutsViewController: UITableViewController,
_ controller: INUIEditVoiceShortcutViewController,
didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID
) {
+ ShortcutsManager.shared.deleteVoiceShortcut(
+ withIdentifier: deletedVoiceShortcutIdentifier
+ )
controller.dismiss(animated: true)
}