summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-01-08 15:15:03 +0100
committerAndrej Mihajlov <and@mullvad.net>2020-01-08 15:15:03 +0100
commit69a36607f22a7a7cb87d4b4d0319b9e8bb118416 (patch)
tree7f809baa81b130f456d3c48e28bc65e2638bfd01
parent233690c4e1bd67cc971ff36516f96bcfa29056cc (diff)
parent281287c86ee32a8c43cc6fcfde1c565a8ac77527 (diff)
downloadmullvadvpn-69a36607f22a7a7cb87d4b4d0319b9e8bb118416.tar.xz
mullvadvpn-69a36607f22a7a7cb87d4b4d0319b9e8bb118416.zip
Merge branch 'localize-errors'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme2
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme (renamed from ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/RelaySelectorTests.xcscheme)31
-rw-r--r--ios/MullvadVPN/Account.swift42
-rw-r--r--ios/MullvadVPN/AccountExpiry.swift4
-rw-r--r--ios/MullvadVPN/AccountViewController.swift12
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard4
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift13
-rw-r--r--ios/MullvadVPN/LoginViewController.swift20
-rw-r--r--ios/MullvadVPN/MullvadAPI.swift29
-rw-r--r--ios/MullvadVPN/SettingsAccountCell.swift13
-rw-r--r--ios/MullvadVPN/TunnelManager.swift60
-rw-r--r--ios/MullvadVPN/UIAlertController+Error.swift52
13 files changed, 253 insertions, 33 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index afc0800ad1..ea382d9c6d 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -59,6 +59,7 @@
5894FC492296A8090017471D /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5894FC482296A8090017471D /* CustomButton.swift */; };
589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F6227B64450039131E /* BasicTableViewCell.swift */; };
58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; };
+ 58A8BE8323A0F362006B74AC /* UIAlertController+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8BE8223A0F362006B74AC /* UIAlertController+Error.swift */; };
58ADDB3C227B1BD200FAFEA7 /* JsonRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */; };
58ADDB3E227B1CD900FAFEA7 /* MullvadAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */; };
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
@@ -182,6 +183,7 @@
5894E725236B2801008A2793 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
5894FC482296A8090017471D /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
589AB4F6227B64450039131E /* BasicTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicTableViewCell.swift; sourceTree = "<group>"; };
+ 58A8BE8223A0F362006B74AC /* UIAlertController+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Error.swift"; sourceTree = "<group>"; };
58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonRpc.swift; sourceTree = "<group>"; };
58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAPI.swift; sourceTree = "<group>"; };
58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; };
@@ -348,6 +350,7 @@
58AEEF6A2344A46200C9BBD5 /* TunnelConfigurationManager.swift */,
5845F837236C466400B2D93C /* TunnelControlViewController.swift */,
5835B7CB233B76CB0096D79F /* TunnelManager.swift */,
+ 58A8BE8223A0F362006B74AC /* UIAlertController+Error.swift */,
587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
581CBCE52296B97300727D7F /* ViewControllerIdentifier.swift */,
@@ -643,6 +646,7 @@
5894FC492296A8090017471D /* CustomButton.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
587AD7CA2342283900E93A53 /* Account.swift in Sources */,
+ 58A8BE8323A0F362006B74AC /* UIAlertController+Error.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */,
5888AD89227B18C40051EB06 /* RelayList.swift in Sources */,
diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme
index d894a24a28..405be68500 100644
--- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme
+++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme
@@ -32,7 +32,7 @@
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
- BlueprintIdentifier = "584B26DF237433ED0073B10E"
+ BlueprintIdentifier = "58B0A29F238EE67E00BC001D"
BuildableName = "MullvadVPNTests.xctest"
BlueprintName = "MullvadVPNTests"
ReferencedContainer = "container:MullvadVPN.xcodeproj">
diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/RelaySelectorTests.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme
index 0f05e5b7f1..0c2fdada19 100644
--- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/RelaySelectorTests.xcscheme
+++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme
@@ -5,6 +5,22 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "58B0A29F238EE67E00BC001D"
+ BuildableName = "MullvadVPNTests.xctest"
+ BlueprintName = "MullvadVPNTests"
+ ReferencedContainer = "container:MullvadVPN.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
@@ -16,9 +32,9 @@
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
- BlueprintIdentifier = "584B26F0237434D00073B10E"
- BuildableName = "RelaySelectorTests.xctest"
- BlueprintName = "RelaySelectorTests"
+ BlueprintIdentifier = "58B0A29F238EE67E00BC001D"
+ BuildableName = "MullvadVPNTests.xctest"
+ BlueprintName = "MullvadVPNTests"
ReferencedContainer = "container:MullvadVPN.xcodeproj">
</BuildableReference>
</TestableReference>
@@ -41,6 +57,15 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "58B0A29F238EE67E00BC001D"
+ BuildableName = "MullvadVPNTests.xctest"
+ BlueprintName = "MullvadVPNTests"
+ ReferencedContainer = "container:MullvadVPN.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index 0d98134769..7feb876837 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -8,6 +8,7 @@
import Combine
import Foundation
+import NetworkExtension
import os
/// A enum describing the errors emitted by `Account`
@@ -25,6 +26,47 @@ enum AccountLoginError: Error {
case tunnelConfiguration(TunnelManagerError)
}
+extension AccountError: LocalizedError {
+ var errorDescription: String? {
+ switch self {
+ case .login:
+ return NSLocalizedString("Log in error", comment: "")
+
+ case .logout:
+ return NSLocalizedString("Log out error", comment: "")
+ }
+ }
+
+ var failureReason: String? {
+ switch self {
+ case .login(.invalidAccount):
+ return NSLocalizedString("Invalid account", comment: "")
+
+ case .login(.tunnelConfiguration(.setAccount(let setAccountError))):
+ switch setAccountError {
+ case .pushWireguardKey(.transport(.network)):
+ return NSLocalizedString("Network error", comment: "")
+
+ case .pushWireguardKey(.server(let serverError)):
+ return serverError.errorDescription ?? serverError.message
+
+ case .setup(.saveTunnel(let systemError as NEVPNError))
+ where systemError.code == .configurationReadWriteFailed:
+ return NSLocalizedString("Permission denied to add a VPN profile", comment: "")
+
+ default:
+ return NSLocalizedString("Internal error", comment: "")
+ }
+
+ case .logout:
+ return NSLocalizedString("Internal error", comment: "")
+
+ default:
+ return nil
+ }
+ }
+}
+
/// A enum holding the `UserDefaults` string keys
private enum UserDefaultsKeys: String {
case accountToken = "accountToken"
diff --git a/ios/MullvadVPN/AccountExpiry.swift b/ios/MullvadVPN/AccountExpiry.swift
index 493de6dc5b..aa4f4c5c06 100644
--- a/ios/MullvadVPN/AccountExpiry.swift
+++ b/ios/MullvadVPN/AccountExpiry.swift
@@ -28,8 +28,8 @@ class AccountExpiry {
return date < Date()
}
- var formattedRemainingTime: String {
- return relativeFormatter.string(from: Date(), to: date)!
+ var formattedRemainingTime: String? {
+ return relativeFormatter.string(from: Date(), to: date)
}
var formattedDate: String {
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index c51a8e6977..51d8013737 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -31,8 +31,14 @@ class AccountViewController: UIViewController {
@IBAction func doLogout() {
logoutSubscriber = Account.shared.logout()
.receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { (_) in
- self.performSegue(withIdentifier: SegueIdentifier.Account.logout.rawValue, sender: self)
+ .sink(receiveCompletion: { (completion) in
+ switch completion {
+ case .failure(let error):
+ self.presentError(error, preferredStyle: .alert)
+
+ case .finished:
+ self.performSegue(withIdentifier: SegueIdentifier.Account.logout.rawValue, sender: self)
+ }
})
}
@@ -45,7 +51,7 @@ class AccountViewController: UIViewController {
let accountExpiry = AccountExpiry(date: expiryDate)
if accountExpiry.isExpired {
- expiryLabel.text = NSLocalizedString("OUT OF TIME", tableName: "Settings", comment: "")
+ expiryLabel.text = NSLocalizedString("OUT OF TIME", comment: "")
expiryLabel.textColor = .dangerColor
} else {
expiryLabel.text = accountExpiry.formattedDate
diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard
index 82f6917ce1..b1c99c9877 100644
--- a/ios/MullvadVPN/Base.lproj/Main.storyboard
+++ b/ios/MullvadVPN/Base.lproj/Main.storyboard
@@ -123,8 +123,8 @@
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
- <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter your account number" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XSV-Lk-dj4">
- <rect key="frame" x="24" y="47" width="327" height="21"/>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter your account number" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XSV-Lk-dj4">
+ <rect key="frame" x="24" y="47" width="327" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.60359589041095896" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 8e9c4f09b7..7d3361a512 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -96,7 +96,10 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (completion) in
if case .failure(let error) = completion {
- os_log(.error, "Failed to start the tunnel: %{public}s", error.localizedDescription)
+ os_log(.error, "Failed to start the tunnel: %{public}s",
+ error.localizedDescription)
+
+ self.presentError(error, preferredStyle: .alert)
}
})
}
@@ -104,7 +107,7 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie
private func disconnectTunnel() {
startStopTunnelSubscriber = TunnelManager.shared.stopTunnel()
.receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { (_) in
+ .sink(receiveCompletion: { (completion) in
// no-op
})
}
@@ -151,13 +154,13 @@ private extension TunnelState {
func textForSecureLabel() -> String {
switch self {
case .connecting, .reconnecting:
- return NSLocalizedString("Creating secure connection", tableName: "Connect", comment: "")
+ return NSLocalizedString("Creating secure connection", comment: "")
case .connected:
- return NSLocalizedString("Secure connection", tableName: "Connect", comment: "")
+ return NSLocalizedString("Secure connection", comment: "")
case .disconnecting, .disconnected:
- return NSLocalizedString("Unsecured connection", tableName: "Connect", comment: "")
+ return NSLocalizedString("Unsecured connection", comment: "")
}
}
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift
index 5f2733f6a4..e44ac1e454 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/LoginViewController.swift
@@ -259,36 +259,32 @@ private extension LoginState {
var localizedTitle: String {
switch self {
case .default:
- return NSLocalizedString("Login", tableName: "Login", comment: "")
+ return NSLocalizedString("Login", comment: "")
case .authenticating:
- return NSLocalizedString("Logging in...", tableName: "Login", comment: "")
+ return NSLocalizedString("Logging in...", comment: "")
case .failure:
- return NSLocalizedString("Login failed", tableName: "Login", comment: "")
+ return NSLocalizedString("Login failed", comment: "")
case .success:
- return NSLocalizedString("Logged in", tableName: "Login", comment: "")
+ return NSLocalizedString("Logged in", comment: "")
}
}
var localizedMessage: String {
switch self {
case .default:
- return NSLocalizedString("Enter your account number", tableName: "Login", comment: "")
+ return NSLocalizedString("Enter your account number", comment: "")
case .authenticating:
- return NSLocalizedString("Checking account number", tableName: "Login", comment: "")
+ return NSLocalizedString("Checking account number", comment: "")
case .failure(let error):
- if case .login(.invalidAccount) = error {
- return NSLocalizedString("Invalid account number", tableName: "Login", comment: "")
- } else {
- return NSLocalizedString("Internal error", tableName: "Login", comment: "")
- }
+ return error.failureReason ?? ""
case .success:
- return NSLocalizedString("Correct account number", tableName: "Login", comment: "")
+ return NSLocalizedString("Correct account number", comment: "")
}
}
}
diff --git a/ios/MullvadVPN/MullvadAPI.swift b/ios/MullvadVPN/MullvadAPI.swift
index be87728b7f..055b55e078 100644
--- a/ios/MullvadVPN/MullvadAPI.swift
+++ b/ios/MullvadVPN/MullvadAPI.swift
@@ -204,3 +204,32 @@ class MullvadAPI {
return decoder
}
}
+
+
+extension JsonRpcResponseError: LocalizedError
+ where
+ ResponseCode == MullvadAPI.ResponseCode
+{
+ var errorDescription: String? {
+ switch code {
+ case .accountDoesNotExist:
+ return NSLocalizedString("Invalid account", comment: "")
+
+ case .tooManyWireguardKeys:
+ return NSLocalizedString("Too many public WireGuard keys", comment: "")
+
+ case .other:
+ return nil
+ }
+ }
+
+ var recoverySuggestion: String? {
+ switch code {
+ case .tooManyWireguardKeys:
+ return NSLocalizedString("Remove unused WireGuard keys", comment: "")
+
+ default:
+ return nil
+ }
+ }
+}
diff --git a/ios/MullvadVPN/SettingsAccountCell.swift b/ios/MullvadVPN/SettingsAccountCell.swift
index cc959c7c46..ce5705448e 100644
--- a/ios/MullvadVPN/SettingsAccountCell.swift
+++ b/ios/MullvadVPN/SettingsAccountCell.swift
@@ -24,14 +24,17 @@ class SettingsAccountCell: SettingsCell {
let accountExpiry = AccountExpiry(date: accountExpiryDate)
if accountExpiry.isExpired {
- expiryLabel.text = NSLocalizedString("OUT OF TIME", tableName: "Settings", comment: "")
+ expiryLabel.text = NSLocalizedString("OUT OF TIME", comment: "")
expiryLabel.textColor = .dangerColor
} else {
- let remainingTime = accountExpiry.formattedRemainingTime
- let localizedString = NSLocalizedString("%@ left", tableName: "Settings", comment: "")
- let formattedString = String(format: localizedString, remainingTime)
+ if let remainingTime = accountExpiry.formattedRemainingTime {
+ let localizedString = NSLocalizedString("%@ left", comment: "")
+ let formattedString = String(format: localizedString, remainingTime)
- expiryLabel.text = formattedString.uppercased()
+ expiryLabel.text = formattedString.uppercased()
+ } else {
+ expiryLabel.text = ""
+ }
expiryLabel.textColor = .white
}
} else {
diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift
index f1b0ab8c59..3019fd2343 100644
--- a/ios/MullvadVPN/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager.swift
@@ -41,6 +41,66 @@ enum TunnelManagerError: Error {
case regenerateWireguardPrivateKey(RegenerateWireguardPrivateKeyError)
}
+extension TunnelManagerError: LocalizedError {
+
+ var errorDescription: String? {
+ switch self {
+ case .regenerateWireguardPrivateKey:
+ return NSLocalizedString("Cannot regenerate the private key", comment: "")
+
+ case .setAccount:
+ return NSLocalizedString("Cannot set up the tunnel", comment: "")
+
+ case .getWireguardPublicKey:
+ return NSLocalizedString("Cannot retrieve the public key", comment: "")
+
+ case .startTunnel:
+ return NSLocalizedString("Cannot start the tunnel", comment: "")
+
+ default:
+ return nil
+ }
+ }
+
+ var failureReason: String? {
+ switch self {
+
+ case .setAccount(.pushWireguardKey(let pushError)),
+ .regenerateWireguardPrivateKey(.replaceWireguardKey(let pushError)):
+ switch pushError {
+ case .transport(.network(let urlError)):
+ return urlError.localizedDescription
+
+ case .server(let serverError):
+ return serverError.errorDescription
+
+ default:
+ return NSLocalizedString("Internal error", comment: "")
+ }
+
+ default:
+ return nil
+ }
+ }
+
+ var recoverySuggestion: String? {
+ switch self {
+ case .regenerateWireguardPrivateKey(.replaceWireguardKey(let pushError)):
+ switch pushError {
+ case .server(let serverError) where serverError.code == .tooManyWireguardKeys:
+ return NSLocalizedString("Remove unused WireGuard keys and try again.", comment: "")
+
+ default:
+ return nil
+ }
+
+ default:
+ return nil
+ }
+ }
+
+}
+
enum TunnelIpcRequestError: Error {
/// IPC is not set yet
case missingIpc
diff --git a/ios/MullvadVPN/UIAlertController+Error.swift b/ios/MullvadVPN/UIAlertController+Error.swift
new file mode 100644
index 0000000000..b76958b0ef
--- /dev/null
+++ b/ios/MullvadVPN/UIAlertController+Error.swift
@@ -0,0 +1,52 @@
+//
+// UIAlertController+Error.swift
+// MullvadVPN
+//
+// Created by pronebird on 11/12/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+/// An extension for presenting `LocalizedError` subclasses in `UIAlertController`
+extension UIAlertController {
+
+ convenience init<Error>(_ error: Error, preferredStyle: UIAlertController.Style)
+ where Error: LocalizedError
+ {
+ let title = error.errorDescription
+ let message = [error.failureReason, error.recoverySuggestion]
+ .compactMap { $0 }
+ .joined(separator: "\n\n")
+
+ self.init(title: title, message: message, preferredStyle: preferredStyle)
+ }
+
+}
+
+extension UIViewController {
+
+ /// Present an instance of `LocalizedError` using `UIAlertController`
+ /// Note: this method adds a default "OK" action when `configurationBlock` is not given
+ func presentError<Error>(
+ _ error: Error,
+ preferredStyle: UIAlertController.Style,
+ configurationBlock: ((UIAlertController) -> Void)? = nil,
+ completionBlock: (() -> Void)? = nil)
+ where Error: LocalizedError
+ {
+ let alertController = UIAlertController(error, preferredStyle: preferredStyle)
+
+ if let configurationBlock = configurationBlock {
+ configurationBlock(alertController)
+ } else {
+ alertController.addAction(
+ UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel)
+ )
+ }
+
+ self.present(alertController, animated: true, completion: completionBlock)
+ }
+
+}