diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-08-23 13:34:49 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-08-23 13:34:49 +0200 |
| commit | e7c9140c4c2555affdd4ed879cd1a3593bdea7cf (patch) | |
| tree | 47bb1a13b3236e5e2ce95b81cc9eb4c72f0a62e7 | |
| parent | ea00c0c6c1fe38d01bee3ef0fe60d362749b64b0 (diff) | |
| parent | bf44a57198cf08468c8f38de31194c189e8f5416 (diff) | |
| download | mullvadvpn-e7c9140c4c2555affdd4ed879cd1a3593bdea7cf.tar.xz mullvadvpn-e7c9140c4c2555affdd4ed879cd1a3593bdea7cf.zip | |
Merge branch 'add-shortcuts'
| -rw-r--r-- | ios/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/SceneDelegate.swift | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsCell.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsDataSource.swift | 20 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsNavigationController.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsViewController.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsDataSource.swift | 234 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsDataSourceDelegate.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsManager.swift | 64 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsViewController.swift | 116 |
11 files changed, 485 insertions, 1 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 8d297676f1..ab0a074415 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th - Add ability to manage registered devices if too many devices detected during log-in. - Add intents: start VPN, stop VPN, reconnect VPN (acts as start VPN when the tunnel is down, otherwise picks new relay). +- Add menu item to control shortcuts. ### Changed - When logged into an account with no time left, a new view is shown instead of account settings, diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8d67e122ac..eb9a155cbe 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -307,6 +307,10 @@ 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 */; }; E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; }; E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; }; E1187ABF289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABE289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift */; }; @@ -598,6 +602,10 @@ 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>"; }; E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; }; E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; }; E1187ABE289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RESTCreateApplePaymentResponse+Localization.swift"; sourceTree = "<group>"; }; @@ -959,6 +967,10 @@ 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */, 580F8B88281A79A7002E0998 /* SettingsManager */, 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */, + 75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */, + 75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */, + 75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */, + 753D6C0B28B4BF3E0052D9E1 /* ShortcutsManager.swift */, 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */, 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, 58CCA01122424D11004F3011 /* SettingsViewController.swift */, @@ -1391,6 +1403,7 @@ 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, 58554F7D280D6FE000013055 /* RESTURLSession.swift in Sources */, 5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */, + 75FD0C2128B108570021E33E /* ShortcutsDataSource.swift in Sources */, 589D28822846306C00F9A7B3 /* GroupOperation.swift in Sources */, 5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */, 5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */, @@ -1414,6 +1427,7 @@ 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */, 58CCA01822426713004F3011 /* AccountViewController.swift in Sources */, 5820674E26E6510200655B05 /* REST.swift in Sources */, + 75FD0C2528B117D30021E33E /* ShortcutsViewController.swift in Sources */, 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */, 58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */, 58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */, @@ -1458,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 */, @@ -1479,6 +1494,7 @@ 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */, + 75FD0C2328B109860021E33E /* ShortcutsDataSourceDelegate.swift in Sources */, 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */, 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */, 5868BD33261DCD2600E6027F /* CustomSplitViewController.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/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift index cb457b042a..44e4891570 100644 --- a/ios/MullvadVPN/SettingsDataSource.swift +++ b/ios/MullvadVPN/SettingsDataSource.swift @@ -43,6 +43,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab enum Item: String { case account case preferences + case shortcuts case version case problemReport case faq @@ -92,7 +93,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab if TunnelManager.shared.deviceState.isLoggedIn { newSnapshot.appendSections([.main]) - newSnapshot.appendItems([.account, .preferences], in: .main) + newSnapshot.appendItems([.account, .preferences, .shortcuts], in: .main) } newSnapshot.appendSections([.version, .problemReport]) @@ -152,6 +153,23 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab return cell + case .shortcuts: + let cell = tableView.dequeueReusableCell( + withIdentifier: CellReuseIdentifiers.basicCell.rawValue, + for: indexPath + ) as! SettingsCell + cell.titleLabel.text = NSLocalizedString( + "SHORTCUTS_CELL_LABEL", + tableName: "Settings", + value: "Shortcuts", + comment: "" + ) + cell.detailTitleLabel.text = nil + cell.accessibilityIdentifier = nil + cell.disclosureType = .chevron + + return cell + case .version: let cell = tableView.dequeueReusableCell( withIdentifier: CellReuseIdentifiers.basicCell.rawValue, diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift index f462f603c3..0476ac23fa 100644 --- a/ios/MullvadVPN/SettingsNavigationController.swift +++ b/ios/MullvadVPN/SettingsNavigationController.swift @@ -13,6 +13,7 @@ enum SettingsNavigationRoute { case root case account case preferences + case shortcuts case problemReport } @@ -119,6 +120,9 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont case .preferences: return PreferencesViewController() + case .shortcuts: + return ShortcutsViewController() + case .problemReport: return ProblemReportViewController() } diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift index 910e933319..0048c2eed3 100644 --- a/ios/MullvadVPN/SettingsViewController.swift +++ b/ios/MullvadVPN/SettingsViewController.swift @@ -102,6 +102,8 @@ extension SettingsDataSource.Item { return .account case .preferences: return .preferences + case .shortcuts: + return .shortcuts case .version: return nil case .problemReport: diff --git a/ios/MullvadVPN/ShortcutsDataSource.swift b/ios/MullvadVPN/ShortcutsDataSource.swift new file mode 100644 index 0000000000..429d196246 --- /dev/null +++ b/ios/MullvadVPN/ShortcutsDataSource.swift @@ -0,0 +1,234 @@ +// +// ShortcutsDataSource.swift +// MullvadVPN +// +// Created by Nikolay Davydov on 20.08.2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import IntentsUI +import UIKit + +final class ShortcutsDataSource: NSObject, + UITableViewDataSource, + UITableViewDelegate, + ShortcutsManagerDelegate +{ + private enum CellReuseIdentifiers: String, CaseIterable { + case basicCell + + var reusableViewClass: AnyClass { + switch self { + case .basicCell: + return SettingsCell.self + } + } + } + + private enum HeaderFooterReuseIdentifier: String, CaseIterable { + case spacer + + var reusableViewClass: AnyClass { + switch self { + case .spacer: + return EmptyTableViewHeaderFooterView.self + } + } + } + + enum Section: String { + case shortcuts + } + + 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? + + 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( + cellIdentifier.reusableViewClass, + forCellReuseIdentifier: cellIdentifier.rawValue + ) + } + + HeaderFooterReuseIdentifier.allCases.forEach { reuseIdentifier in + tableView?.register( + reuseIdentifier.reusableViewClass, + forHeaderFooterViewReuseIdentifier: reuseIdentifier.rawValue + ) + } + } + + 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) + } + + var newSnapshot = DataSourceSnapshot<Section, Item>() + newSnapshot.appendSections([.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 { + let sectionIdentifier = snapshot.section(at: section)! + return snapshot.numberOfItems(in: sectionIdentifier) ?? 0 + } + + func numberOfSections(in tableView: UITableView) -> Int { + return snapshot.numberOfSections() + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let item = snapshot.itemForIndexPath(indexPath)! + 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 + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let item = snapshot.itemForIndexPath(indexPath) else { return } + delegate?.shortcutsDataSource(self, didSelectItem: item) + tableView.deselectRow(at: indexPath, animated: true) + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return tableView.dequeueReusableHeaderFooterView( + withIdentifier: HeaderFooterReuseIdentifier.spacer.rawValue + ) + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return nil + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return UIMetrics.sectionSpacing + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return 0 + } + + // MARK: - ShortcutsManagerDelegate + + func shortcutsManager( + _ shortcutsManager: ShortcutsManager, + didReceiveVoiceShortcuts voiceShortcuts: [INVoiceShortcut] + ) { + updateDataSnapshot(voiceShortcuts: voiceShortcuts) + tableView?.reloadData() + } +} + +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: "" + ) + } + } + + 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/ShortcutsDataSourceDelegate.swift b/ios/MullvadVPN/ShortcutsDataSourceDelegate.swift new file mode 100644 index 0000000000..2fde666721 --- /dev/null +++ b/ios/MullvadVPN/ShortcutsDataSourceDelegate.swift @@ -0,0 +1,14 @@ +// +// ShortcutsDataSourceDelegate.swift +// MullvadVPN +// +// Created by Nikolay Davydov on 20.08.2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +protocol ShortcutsDataSourceDelegate: AnyObject { + func shortcutsDataSource( + _ dataSource: ShortcutsDataSource, + didSelectItem item: ShortcutsDataSource.Item + ) +} 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 new file mode 100644 index 0000000000..953639dca3 --- /dev/null +++ b/ios/MullvadVPN/ShortcutsViewController.swift @@ -0,0 +1,116 @@ +// +// ShortcutsViewController.swift +// MullvadVPN +// +// Created by Nikolay Davydov on 20.08.2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import IntentsUI +import UIKit + +final class ShortcutsViewController: UITableViewController, + ShortcutsDataSourceDelegate, + INUIAddVoiceShortcutViewControllerDelegate, + INUIEditVoiceShortcutViewControllerDelegate +{ + private let dataSource = ShortcutsDataSource() + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + 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 + + dataSource.tableView = tableView + dataSource.delegate = self + + navigationItem.title = NSLocalizedString( + "NAVIGATION_TITLE", + tableName: "Shortcuts", + value: "Shortcuts", + comment: "" + ) + } + + // MARK: - ShortcutsDataSourceDelegate + + func shortcutsDataSource( + _ dataSource: ShortcutsDataSource, + didSelectItem item: ShortcutsDataSource.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 + + func addVoiceShortcutViewController( + _ controller: INUIAddVoiceShortcutViewController, + didFinishWith voiceShortcut: INVoiceShortcut?, + error: Error? + ) { + if let voiceShortcut = voiceShortcut { + ShortcutsManager.shared.addVoiceShortcut(voiceShortcut) + } + controller.dismiss(animated: true) + } + + func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) { + controller.dismiss(animated: true) + } + + // MARK: - INUIEditVoiceShortcutViewControllerDelegate + + func editVoiceShortcutViewController( + _ controller: INUIEditVoiceShortcutViewController, + didUpdate voiceShortcut: INVoiceShortcut?, + error: Error? + ) { + controller.dismiss(animated: true) + } + + func editVoiceShortcutViewController( + _ controller: INUIEditVoiceShortcutViewController, + didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID + ) { + ShortcutsManager.shared.deleteVoiceShortcut( + withIdentifier: deletedVoiceShortcutIdentifier + ) + controller.dismiss(animated: true) + } + + func editVoiceShortcutViewControllerDidCancel( + _ controller: INUIEditVoiceShortcutViewController + ) { + controller.dismiss(animated: true) + } +} |
