summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-05-14 15:24:36 +0200
committerAndrej Mihajlov <and@mullvad.net>2019-05-14 15:24:36 +0200
commit35fff9db7e386ff575bd683e8844bbebb0229ab0 (patch)
treec8c5f3020549b452e09ef5bfcbd4647eff76318d
parentc6a8e6ddd7405a5560fee9c9651cc4a48660d460 (diff)
parent3d1cdd8c18dd5d98296ff31ea0354357b296a8bf (diff)
downloadmullvadvpn-35fff9db7e386ff575bd683e8844bbebb0229ab0.tar.xz
mullvadvpn-35fff9db7e386ff575bd683e8844bbebb0229ab0.zip
Merge branch 'select-location-ios'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj58
-rw-r--r--ios/MullvadVPN/AccountInputGroupView.swift60
-rw-r--r--ios/MullvadVPN/AccountTextField.swift10
-rw-r--r--ios/MullvadVPN/AccountViewController.swift4
-rw-r--r--ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json3
-rw-r--r--ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json3
-rw-r--r--ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json3
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard155
-rw-r--r--ios/MullvadVPN/BasicTableViewCell.swift27
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift18
-rw-r--r--ios/MullvadVPN/CustomNavigationBar.swift85
-rw-r--r--ios/MullvadVPN/HeaderBarViewController.swift2
-rw-r--r--ios/MullvadVPN/JsonRpc.swift70
-rw-r--r--ios/MullvadVPN/LoginViewController.swift40
-rw-r--r--ios/MullvadVPN/MullvadAPI.swift59
-rw-r--r--ios/MullvadVPN/Optional+Unwrap.swift23
-rw-r--r--ios/MullvadVPN/RelayList.swift35
-rw-r--r--ios/MullvadVPN/RelayStatusIndicatorView.swift79
-rw-r--r--ios/MullvadVPN/SegueIdentifier.swift8
-rw-r--r--ios/MullvadVPN/SelectLocationCell.swift111
-rw-r--r--ios/MullvadVPN/SelectLocationController.swift285
-rw-r--r--ios/MullvadVPN/SettingsViewController.swift24
-rw-r--r--ios/MullvadVPN/TranslucentButtonBlurView.swift12
-rw-r--r--ios/MullvadVPN/UIColor+Helpers.swift37
-rw-r--r--ios/MullvadVPN/UIColor+Palette.swift11
25 files changed, 1122 insertions, 100 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 7b1bf02158..dfad4e7f75 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -9,6 +9,16 @@
/* Begin PBXBuildFile section */
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; };
5867A51C2248F26A005513C0 /* SegueIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867A51B2248F26A005513C0 /* SegueIdentifier.swift */; };
+ 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; };
+ 5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */; };
+ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
+ 5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationController.swift */; };
+ 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; };
+ 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F6227B64450039131E /* BasicTableViewCell.swift */; };
+ 589AB4F9227C50D80039131E /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F8227C50D80039131E /* CustomNavigationBar.swift */; };
+ 58ADDB3C227B1BD200FAFEA7 /* JsonRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */; };
+ 58ADDB3E227B1CD900FAFEA7 /* MullvadAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */; };
+ 58ADDB40227B1E7100FAFEA7 /* Optional+Unwrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3F227B1E7100FAFEA7 /* Optional+Unwrap.swift */; };
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; };
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA00F224249A1004F3011 /* ConnectViewController.swift */; };
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA01122424D11004F3011 /* SettingsViewController.swift */; };
@@ -53,6 +63,16 @@
5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; };
5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; };
5867A51B2248F26A005513C0 /* SegueIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegueIdentifier.swift; sourceTree = "<group>"; };
+ 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = "<group>"; };
+ 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusIndicatorView.swift; sourceTree = "<group>"; };
+ 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; };
+ 5888AD86227B17950051EB06 /* SelectLocationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationController.swift; sourceTree = "<group>"; };
+ 5888AD88227B18C40051EB06 /* RelayList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayList.swift; sourceTree = "<group>"; };
+ 589AB4F6227B64450039131E /* BasicTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicTableViewCell.swift; sourceTree = "<group>"; };
+ 589AB4F8227C50D80039131E /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.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>"; };
+ 58ADDB3F227B1E7100FAFEA7 /* Optional+Unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Unwrap.swift"; sourceTree = "<group>"; };
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; };
58CCA00F224249A1004F3011 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = "<group>"; };
58CCA01122424D11004F3011 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ -115,19 +135,29 @@
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */,
58CCA01D2242787B004F3011 /* AccountTextField.swift */,
58CCA01722426713004F3011 /* AccountViewController.swift */,
- 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */,
58CE5E63224146200008646E /* AppDelegate.swift */,
58CE5E6A224146210008646E /* Assets.xcassets */,
+ 589AB4F6227B64450039131E /* BasicTableViewCell.swift */,
58CCA00F224249A1004F3011 /* ConnectViewController.swift */,
+ 589AB4F8227C50D80039131E /* CustomNavigationBar.swift */,
58F37E7C2243ECCB00C75C97 /* HeaderBarViewController.swift */,
58CE5E6F224146210008646E /* Info.plist */,
+ 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */,
58CE5E6C224146210008646E /* LaunchScreen.storyboard */,
58CE5E65224146200008646E /* LoginViewController.swift */,
58CE5E67224146200008646E /* Main.storyboard */,
+ 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */,
5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */,
+ 58ADDB3F227B1E7100FAFEA7 /* Optional+Unwrap.swift */,
+ 5888AD88227B18C40051EB06 /* RelayList.swift */,
+ 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */,
+ 5867A51B2248F26A005513C0 /* SegueIdentifier.swift */,
+ 5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
+ 5888AD86227B17950051EB06 /* SelectLocationController.swift */,
58CCA01122424D11004F3011 /* SettingsViewController.swift */,
+ 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */,
+ 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
- 5867A51B2248F26A005513C0 /* SegueIdentifier.swift */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -193,6 +223,7 @@
TargetAttributes = {
58CE5E5F224146200008646E = {
CreatedOnToolsVersion = 10.0;
+ LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.ApplicationGroups.iOS = {
enabled = 1;
@@ -201,6 +232,7 @@
};
58CE5E78224146470008646E = {
CreatedOnToolsVersion = 10.0;
+ LastSwiftMigration = 1020;
};
};
};
@@ -249,15 +281,25 @@
buildActionMask = 2147483647;
files = (
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */,
+ 5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */,
+ 58ADDB40227B1E7100FAFEA7 /* Optional+Unwrap.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
+ 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
+ 589AB4F9227C50D80039131E /* CustomNavigationBar.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
+ 58ADDB3E227B1CD900FAFEA7 /* MullvadAPI.swift in Sources */,
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */,
+ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
+ 58ADDB3C227B1BD200FAFEA7 /* JsonRpc.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
58F37E7D2243ECCB00C75C97 /* HeaderBarViewController.swift in Sources */,
+ 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */,
+ 5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */,
5867A51C2248F26A005513C0 /* SegueIdentifier.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
+ 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -351,7 +393,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -406,7 +448,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -433,7 +475,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 4.2;
+ SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -455,7 +497,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 4.2;
+ SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -475,7 +517,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.PacketTunnel;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
- SWIFT_VERSION = 4.2;
+ SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -495,7 +537,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.PacketTunnel;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
- SWIFT_VERSION = 4.2;
+ SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
diff --git a/ios/MullvadVPN/AccountInputGroupView.swift b/ios/MullvadVPN/AccountInputGroupView.swift
index e65443933f..fb890a14fd 100644
--- a/ios/MullvadVPN/AccountInputGroupView.swift
+++ b/ios/MullvadVPN/AccountInputGroupView.swift
@@ -9,101 +9,101 @@
import UIKit
@IBDesignable class AccountInputGroupView: UIView {
-
+
@IBOutlet var textField: UITextField!
-
+
private let borderRadius = CGFloat(8)
private let borderWidth = CGFloat(2)
-
+
private let borderLayer = CAShapeLayer()
private let backgroundLayer = CAShapeLayer()
private let maskLayer = CALayer()
-
+
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
-
+
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
-
+
// MARK: - CALayerDelegate
-
+
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
-
+
guard layer == self.layer else { return }
-
+
// extend the border frame outside of the content area
let borderFrame = layer.bounds.insetBy(dx: -borderWidth * 0.5, dy: -borderWidth * 0.5)
-
+
// create a bezier path for border
let borderPath = borderBezierPath(size: borderFrame.size)
-
+
// update the background layer mask
maskLayer.frame.size = borderFrame.size
maskLayer.contents = backgroundMaskImage(borderPath: borderPath).cgImage
-
+
backgroundLayer.frame = borderFrame
-
+
borderLayer.path = borderPath.cgPath
borderLayer.frame = borderFrame
}
-
+
// MARK: - Notifications
-
+
@objc func textDidBeginEditing() {
updateBorderStyle()
}
-
+
@objc func textDidEndEditing() {
updateBorderStyle()
}
-
+
// MARK: - Private
-
+
private func setup() {
backgroundColor = UIColor.clear
-
+
borderLayer.lineWidth = borderWidth
borderLayer.strokeColor = UIColor.clear.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
-
+
backgroundLayer.backgroundColor = UIColor.white.cgColor
backgroundLayer.mask = maskLayer
-
+
layer.insertSublayer(borderLayer, at: 0)
layer.insertSublayer(backgroundLayer, at: 0)
-
+
addTextFieldNotificationObservers()
}
-
-
+
+
private func addTextFieldNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(textDidBeginEditing), name: UITextField.textDidBeginEditingNotification, object: textField)
NotificationCenter.default.addObserver(self, selector: #selector(textDidEndEditing), name: UITextField.textDidEndEditingNotification, object: textField)
}
-
+
private func updateBorderStyle() {
let borderColor = textField.isEditing ? UIColor.accountTextFieldBorderColor : UIColor.clear
-
+
borderLayer.strokeColor = borderColor.cgColor
}
-
+
private func borderBezierPath(size: CGSize) -> UIBezierPath {
let borderPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: borderRadius)
borderPath.lineWidth = borderWidth
-
+
return borderPath
}
-
+
private func backgroundMaskImage(borderPath: UIBezierPath) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: borderPath.bounds)
return renderer.image { (ctx) in
borderPath.fill()
-
+
// strip out any overlapping pixels between the border and the background
borderPath.stroke(with: .clear, alpha: 0)
}
diff --git a/ios/MullvadVPN/AccountTextField.swift b/ios/MullvadVPN/AccountTextField.swift
index ebf8f11d54..86ec88b41d 100644
--- a/ios/MullvadVPN/AccountTextField.swift
+++ b/ios/MullvadVPN/AccountTextField.swift
@@ -9,25 +9,25 @@
import UIKit
@IBDesignable class AccountTextField: UITextField {
-
+
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
-
+
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
-
+
private func setup() {
backgroundColor = UIColor.clear
}
-
+
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: 14, dy: 12)
}
-
+
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return textRect(forBounds: bounds)
}
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index e9d79a5584..626ec1eb4a 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -9,10 +9,10 @@
import UIKit
class AccountViewController: UIViewController {
-
+
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
-
+
}
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json b/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json
index c2c508144c..e62426b668 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json
+++ b/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json
@@ -19,5 +19,8 @@
"info" : {
"version" : 1,
"author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
}
} \ No newline at end of file
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json b/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json
index 68781197d1..d3ab732193 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json
+++ b/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json
@@ -19,5 +19,8 @@
"info" : {
"version" : 1,
"author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
}
} \ No newline at end of file
diff --git a/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json b/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json
index 667331c0ca..50bd9a5567 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json
+++ b/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json
@@ -19,5 +19,8 @@
"info" : {
"version" : 1,
"author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
}
} \ No newline at end of file
diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard
index b22b75a651..1fb6bc311e 100644
--- a/ios/MullvadVPN/Base.lproj/Main.storyboard
+++ b/ios/MullvadVPN/Base.lproj/Main.storyboard
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
- <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -203,6 +203,9 @@
<state key="normal" title="Select location" backgroundImage="TranslucentNeutralButton">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
+ <connections>
+ <segue destination="hOC-Ab-N3D" kind="presentation" id="LlL-R9-hNI"/>
+ </connections>
</button>
</subviews>
<constraints>
@@ -336,6 +339,22 @@
</objects>
<point key="canvasLocation" x="3084" y="27"/>
</scene>
+ <!--Navigation Controller-->
+ <scene sceneID="oT4-Ap-qrZ">
+ <objects>
+ <navigationController id="hOC-Ab-N3D" sceneMemberID="viewController">
+ <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="kmu-Ab-x1c" customClass="CustomNavigationBar" customModule="MullvadVPN" customModuleProvider="target">
+ <rect key="frame" x="0.0" y="20" width="375" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </navigationBar>
+ <connections>
+ <segue destination="FxZ-7F-3yi" kind="relationship" relationship="rootViewController" id="cFv-eb-G19"/>
+ </connections>
+ </navigationController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="GCK-Z5-Jwh" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="1260" y="841"/>
+ </scene>
<!--Header Bar View Controller-->
<scene sceneID="XNS-uo-8Yd">
<objects>
@@ -345,17 +364,17 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LogoIcon" translatesAutoresizingMaskIntoConstraints="NO" id="cKg-hE-JsS">
- <rect key="frame" x="11" y="12" width="49" height="50"/>
+ <rect key="frame" x="11" y="29" width="16" height="16"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uXv-Tf-PET">
- <rect key="frame" x="335" y="25" width="24" height="24"/>
+ <rect key="frame" x="343" y="26" width="16" height="22"/>
<state key="normal" image="IconSettings"/>
<connections>
<action selector="handleSettingsButton" destination="rCI-6x-aLd" eventType="touchUpInside" id="TaM-cZ-TvJ"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="MULLVAD VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dqy-A0-TdV">
- <rect key="frame" x="68" y="22" width="168" height="30"/>
+ <rect key="frame" x="35" y="22" width="168" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -380,17 +399,137 @@
</objects>
<point key="canvasLocation" x="1326" y="-509"/>
</scene>
+ <!--Select location-->
+ <scene sceneID="Kar-Ys-a6u">
+ <objects>
+ <tableViewController id="FxZ-7F-3yi" customClass="SelectLocationController" customModule="MullvadVPN" customModuleProvider="target" sceneMemberID="viewController">
+ <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="56" sectionHeaderHeight="28" sectionFooterHeight="28" id="LKX-4h-vIx">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="0.098039215690000001" green="0.18039215689999999" blue="0.27058823529999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <color key="separatorColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <view key="tableHeaderView" contentMode="scaleToFill" id="YMi-O0-jT1">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="159"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <subviews>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" text="Select location" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sfD-OR-Col">
+ <rect key="frame" x="28" y="28" width="335" height="29"/>
+ <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
+ <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" verticalCompressionResistancePriority="751" text="While connected, your real location is masked with a private and secure location in the selected region" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="X0P-N8-lda">
+ <rect key="frame" x="28" y="61" width="335" height="74"/>
+ <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="17"/>
+ <color key="textColor" white="1" alpha="0.60217786815068497" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <constraints>
+ <constraint firstItem="X0P-N8-lda" firstAttribute="top" secondItem="sfD-OR-Col" secondAttribute="bottom" constant="4" id="7AH-h4-POM"/>
+ <constraint firstAttribute="bottomMargin" secondItem="X0P-N8-lda" secondAttribute="bottom" id="Ghh-mK-nAy"/>
+ <constraint firstItem="sfD-OR-Col" firstAttribute="top" secondItem="YMi-O0-jT1" secondAttribute="topMargin" id="NVJ-TA-Aqw"/>
+ <constraint firstAttribute="trailingMargin" secondItem="X0P-N8-lda" secondAttribute="trailing" id="gRy-Wb-s8K"/>
+ <constraint firstItem="sfD-OR-Col" firstAttribute="leading" secondItem="YMi-O0-jT1" secondAttribute="leadingMargin" id="r7f-jn-HXz"/>
+ <constraint firstItem="X0P-N8-lda" firstAttribute="leading" secondItem="YMi-O0-jT1" secondAttribute="leadingMargin" id="s3I-Rw-1Jg"/>
+ <constraint firstAttribute="trailingMargin" secondItem="sfD-OR-Col" secondAttribute="trailing" id="up1-GL-fpb"/>
+ </constraints>
+ <edgeInsets key="layoutMargins" top="8" left="28" bottom="24" right="12"/>
+ </view>
+ <prototypes>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="aFz-H5-sPu" customClass="SelectLocationCell" customModule="MullvadVPN" customModuleProvider="target">
+ <rect key="frame" x="0.0" y="187" width="375" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="aFz-H5-sPu" id="6nQ-gT-vzf">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5ag-N4-pUg" customClass="RelayStatusIndicatorView" customModule="MullvadVPN" customModuleProvider="target">
+ <rect key="frame" x="16" y="14" width="16" height="16"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="16" id="QWj-hh-I3P"/>
+ <constraint firstAttribute="width" constant="16" id="TFV-yi-LXG"/>
+ </constraints>
+ </view>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="y7o-0b-MUV">
+ <rect key="frame" x="44" y="11" width="42" height="22"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="IconTick" translatesAutoresizingMaskIntoConstraints="NO" id="e1o-Bl-zd5">
+ <rect key="frame" x="12" y="10" width="24" height="24"/>
+ <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ </imageView>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KaW-bN-I51">
+ <rect key="frame" x="311" y="0.0" width="64" height="43.5"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="64" id="UU3-Di-65E"/>
+ </constraints>
+ <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <state key="normal" image="IconChevronDown"/>
+ </button>
+ </subviews>
+ <color key="backgroundColor" red="0.16078431369999999" green="0.30196078430000001" blue="0.45098039220000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="KaW-bN-I51" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="y7o-0b-MUV" secondAttribute="trailing" id="3uQ-T4-POk"/>
+ <constraint firstAttribute="bottomMargin" secondItem="y7o-0b-MUV" secondAttribute="bottom" id="7ly-PI-8H3"/>
+ <constraint firstAttribute="bottom" secondItem="KaW-bN-I51" secondAttribute="bottom" id="9I6-k7-21c"/>
+ <constraint firstItem="e1o-Bl-zd5" firstAttribute="centerX" secondItem="5ag-N4-pUg" secondAttribute="centerX" id="Bk5-41-u3r"/>
+ <constraint firstItem="e1o-Bl-zd5" firstAttribute="centerY" secondItem="5ag-N4-pUg" secondAttribute="centerY" id="Pfw-mx-SZ3"/>
+ <constraint firstAttribute="trailing" secondItem="KaW-bN-I51" secondAttribute="trailing" id="Z2F-pa-wEE"/>
+ <constraint firstItem="y7o-0b-MUV" firstAttribute="top" secondItem="6nQ-gT-vzf" secondAttribute="topMargin" id="dw6-el-6DC"/>
+ <constraint firstItem="5ag-N4-pUg" firstAttribute="leading" secondItem="6nQ-gT-vzf" secondAttribute="leadingMargin" id="h2b-0z-IjZ"/>
+ <constraint firstItem="KaW-bN-I51" firstAttribute="top" secondItem="6nQ-gT-vzf" secondAttribute="top" id="ong-F1-a4V"/>
+ <constraint firstItem="5ag-N4-pUg" firstAttribute="centerY" secondItem="6nQ-gT-vzf" secondAttribute="centerY" id="upC-Vc-y0Y"/>
+ <constraint firstItem="y7o-0b-MUV" firstAttribute="leading" secondItem="5ag-N4-pUg" secondAttribute="trailing" constant="12" id="ylV-VK-pUm"/>
+ </constraints>
+ </tableViewCellContentView>
+ <connections>
+ <outlet property="collapseButton" destination="KaW-bN-I51" id="n5C-yZ-39f"/>
+ <outlet property="locationLabel" destination="y7o-0b-MUV" id="Pw4-Kb-uCu"/>
+ <outlet property="statusIndicator" destination="5ag-N4-pUg" id="wXj-KL-JdB"/>
+ <outlet property="tickImageView" destination="e1o-Bl-zd5" id="UjZ-ct-oPZ"/>
+ </connections>
+ </tableViewCell>
+ </prototypes>
+ <connections>
+ <outlet property="dataSource" destination="FxZ-7F-3yi" id="M4F-Hz-EiT"/>
+ <outlet property="delegate" destination="FxZ-7F-3yi" id="yWE-Dc-Wl5"/>
+ </connections>
+ </tableView>
+ <navigationItem key="navigationItem" title="Select location" largeTitleDisplayMode="always" id="PZM-r8-1Sb">
+ <barButtonItem key="leftBarButtonItem" title="Item" image="IconClose" id="4T0-a3-Ce4">
+ <color key="tintColor" white="1" alpha="0.39956121575342468" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <connections>
+ <segue destination="6Lc-ZQ-E4P" kind="unwind" identifier="" unwindAction="unwindFromSelectLocationWithSegue:" id="gAz-uu-Whd"/>
+ </connections>
+ </barButtonItem>
+ </navigationItem>
+ <connections>
+ <segue destination="6Lc-ZQ-E4P" kind="unwind" identifier="ReturnToConnectWithNewRelay" unwindAction="unwindFromSelectLocationWithSegue:" id="SPT-Ay-Cy3"/>
+ </connections>
+ </tableViewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="EvX-LH-gOg" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ <exit id="6Lc-ZQ-E4P" userLabel="Exit" sceneMemberID="exit"/>
+ </objects>
+ <point key="canvasLocation" x="2117.5999999999999" y="840.62968515742136"/>
+ </scene>
</scenes>
<resources>
<image name="DefaultButton" width="9" height="9"/>
+ <image name="IconChevronDown" width="24" height="24"/>
+ <image name="IconClose" width="24" height="24"/>
<image name="IconSettings" width="24" height="24"/>
+ <image name="IconTick" width="24" height="24"/>
<image name="LogoIcon" width="49" height="50"/>
- <image name="MapBackground" width="318.5" height="491"/>
+ <image name="MapBackground" width="637" height="982"/>
<image name="SuccessButton" width="9" height="9"/>
<image name="TranslucentNeutralButton" width="9" height="9"/>
</resources>
<inferredMetricsTieBreakers>
- <segue reference="tVd-Lw-FVU"/>
- <segue reference="RjC-Wk-Enk"/>
+ <segue reference="qKR-6L-kfz"/>
+ <segue reference="fxZ-Uq-nxv"/>
</inferredMetricsTieBreakers>
</document>
diff --git a/ios/MullvadVPN/BasicTableViewCell.swift b/ios/MullvadVPN/BasicTableViewCell.swift
new file mode 100644
index 0000000000..288b59c4b5
--- /dev/null
+++ b/ios/MullvadVPN/BasicTableViewCell.swift
@@ -0,0 +1,27 @@
+//
+// BasicTableViewCell.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import UIKit
+
+class BasicTableViewCell: UITableViewCell {
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ let backgroundView = UIView()
+ backgroundView.backgroundColor = UIColor.cellBackgroundColor
+
+ let selectedBackgroundView = UIView()
+ selectedBackgroundView.backgroundColor = UIColor.cellSelectedBackgroundColor
+
+ self.backgroundView = backgroundView
+ self.selectedBackgroundView = selectedBackgroundView
+ backgroundColor = UIColor.clear
+ contentView.backgroundColor = UIColor.clear
+ }
+}
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index df24ef65f5..ca0fbe7415 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -9,27 +9,33 @@
import UIKit
class ConnectViewController: UIViewController, HeaderBarViewControllerDelegate {
-
+
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
-
+
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if case .embedHeader? = SegueIdentifier.Connect.from(segue: segue) {
let headerBarController = segue.destination as? HeaderBarViewController
headerBarController?.delegate = self
}
}
-
+
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
-
+
// MARK: - HeaderBarViewControllerDelegate
-
+
func headerBarViewControllerShouldOpenSettings(_ controller: HeaderBarViewController) {
performSegue(withIdentifier: SegueIdentifier.Connect.showSettings.rawValue, sender: self)
}
-
+
+ // MARK: - Actions
+
+ @IBAction func unwindFromSelectLocation(segue: UIStoryboardSegue) {
+
+ }
+
}
diff --git a/ios/MullvadVPN/CustomNavigationBar.swift b/ios/MullvadVPN/CustomNavigationBar.swift
new file mode 100644
index 0000000000..5ffd4bb4f9
--- /dev/null
+++ b/ios/MullvadVPN/CustomNavigationBar.swift
@@ -0,0 +1,85 @@
+//
+// CustomNavigationBar.swift
+// MullvadVPN
+//
+// Created by pronebird on 03/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import UIKit
+
+class CustomNavigationBar: UINavigationBar {
+ private(set) var isBarVisible = false
+
+ private let emptyShadow = UIImage()
+
+ /// The blur view used internally by UINavigationBar
+ private var effectView: UIVisualEffectView? {
+ // Find the background view in the navigation bar view hierarchy
+ let backgroundView = subviews.first(where: { $0.description.starts(with: "<_UIBarBackground") })
+
+ // Find the blur view in the background view's view hierarchy
+ let backgroundEffectView = backgroundView?.subviews.first(where: { $0 is UIVisualEffectView })
+
+ return backgroundEffectView as? UIVisualEffectView
+ }
+
+ /// The custom title view or the standard title label used internally by UINavigationBar
+ private var titleView: UIView? {
+ // Return the custom title view when it's set
+ if let customTitleView = topItem?.titleView {
+ return customTitleView
+ }
+
+ // Find the content view inside of the navigation bar hierarchy
+ let contentView = subviews.first(where: { $0.description.starts(with: "<_UINavigationBarContentView") })
+
+ // Find the UILabel in the content view's subviews
+ return contentView?.subviews.first(where: { $0 is UILabel })
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ // UINavigationBar creates subviews dynamically, so make sure to reset the navigation bar state
+ setBarBackgroundVisibility(isBarVisible)
+
+ // UINavigationBar tends to reset the title view opacity in response to layout changes
+ setTitleVisibility(isBarVisible)
+ }
+
+ func setBarVisible(_ visible: Bool, animated: Bool) {
+ guard isBarVisible != visible else { return }
+
+ isBarVisible = visible
+
+ let action = {
+ self.setBarBackgroundVisibility(visible)
+ self.setTitleVisibility(visible)
+ }
+
+ if animated {
+ UIView.animate(withDuration: 0.25, delay: 0,
+ options: [.beginFromCurrentState],
+ animations: action)
+ } else {
+ action()
+ }
+ }
+
+ private func setBarBackgroundVisibility(_ visible: Bool) {
+ let backgroundEffectView = effectView
+
+ if visible {
+ backgroundEffectView?.alpha = 1
+ shadowImage = nil
+ } else {
+ backgroundEffectView?.alpha = 0
+ shadowImage = emptyShadow
+ }
+ }
+
+ private func setTitleVisibility(_ visible: Bool) {
+ titleView?.alpha = visible ? 1 : 0
+ }
+}
diff --git a/ios/MullvadVPN/HeaderBarViewController.swift b/ios/MullvadVPN/HeaderBarViewController.swift
index 08ed7560d1..f449217961 100644
--- a/ios/MullvadVPN/HeaderBarViewController.swift
+++ b/ios/MullvadVPN/HeaderBarViewController.swift
@@ -14,7 +14,7 @@ protocol HeaderBarViewControllerDelegate: class {
class HeaderBarViewController: UIViewController {
weak var delegate: HeaderBarViewControllerDelegate?
-
+
@IBAction func handleSettingsButton() {
delegate?.headerBarViewControllerShouldOpenSettings(self)
}
diff --git a/ios/MullvadVPN/JsonRpc.swift b/ios/MullvadVPN/JsonRpc.swift
new file mode 100644
index 0000000000..a2e29fea19
--- /dev/null
+++ b/ios/MullvadVPN/JsonRpc.swift
@@ -0,0 +1,70 @@
+//
+// JsonRpc.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import Foundation
+
+typealias JsonRpcRequestId = String
+struct JsonRpcRequest<T: Encodable>: Encodable {
+ let version = "2.0"
+ let id: JsonRpcRequestId = UUID().uuidString
+ let method: String
+ let params: [T]
+
+ fileprivate enum CodingKeys: String, CodingKey {
+ case version = "jsonrpc", id, method, params
+ }
+}
+
+extension JsonRpcRequest where T == NoData {
+ init(method: String) {
+ self.init(method: method, params: [])
+ }
+}
+
+struct NoData: Encodable {}
+
+class JsonRpcResponseError: Error, Decodable {
+ let serverErrorMessage: String
+
+ init(serverErrorMessage: String) {
+ self.serverErrorMessage = serverErrorMessage
+ }
+
+ var localizedDescription: String? {
+ return serverErrorMessage
+ }
+
+ required init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+
+ serverErrorMessage = try container.decode(String.self)
+ }
+}
+
+struct JsonRpcResponse<T: Decodable>: Decodable {
+ let version: String
+ let id: JsonRpcRequestId
+ let result: Result<T, JsonRpcResponseError>
+
+ private enum CodingKeys: String, CodingKey {
+ case version = "jsonrpc", id, result, error
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.version = try container.decode(String.self, forKey: .version)
+ self.id = try container.decode(String.self, forKey: .id)
+
+ if container.contains(.result) {
+ self.result = .success(try container.decode(T.self, forKey: .result))
+ } else {
+ self.result = .failure(try container.decode(JsonRpcResponseError.self, forKey: .error))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift
index fa02257513..4a7e364b09 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/LoginViewController.swift
@@ -9,16 +9,16 @@
import UIKit
class LoginViewController: UIViewController, HeaderBarViewControllerDelegate {
-
+
@IBOutlet var keyboardToolbar: UIToolbar!
@IBOutlet var accountTextField: UITextField!
@IBOutlet var loginForm: UIView!
@IBOutlet var loginFormWrapperBottomConstraint: NSLayoutConstraint!
-
+
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
-
+
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if case .embedHeader? = SegueIdentifier.Login.from(segue: segue) {
let headerBarController = segue.destination as? HeaderBarViewController
@@ -28,58 +28,58 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
-
+
accountTextField.inputAccessoryView = keyboardToolbar
-
+
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIWindow.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIWindow.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIWindow.keyboardWillHideNotification, object: nil)
}
-
+
// MARK: - HeaderBarViewControllerDelegate
-
+
func headerBarViewControllerShouldOpenSettings(_ controller: HeaderBarViewController) {
performSegue(withIdentifier: SegueIdentifier.Login.showSettings.rawValue, sender: self)
}
-
+
// MARK: - Keyboard notifications
-
+
@objc private func keyboardWillShow(_ notification: Notification) {
guard let keyboardFrameValue = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return }
-
+
makeLoginFormVisible(keyboardFrame: keyboardFrameValue.cgRectValue)
}
-
+
@objc private func keyboardWillChangeFrame(_ notification: Notification) {
guard let keyboardFrameValue = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue else { return }
-
+
makeLoginFormVisible(keyboardFrame: keyboardFrameValue.cgRectValue)
}
-
+
@objc private func keyboardWillHide(_ notification: Notification) {
loginFormWrapperBottomConstraint.constant = 0
view.layoutIfNeeded()
}
-
+
// MARK: - IBActions
-
+
@IBAction func cancelLogin() {
view.endEditing(true)
}
-
+
@IBAction func doLogin() {
view.endEditing(true)
-
+
// TODO: Add the code to initiate the log in
performSegue(withIdentifier: "ShowConnect", sender: self)
}
-
+
// MARK: - Private
-
+
private func makeLoginFormVisible(keyboardFrame: CGRect) {
let convertedKeyboardFrame = view.convert(keyboardFrame, from: nil)
let (_, remainder) = view.frame.divided(atDistance: convertedKeyboardFrame.minY, from: CGRectEdge.minYEdge)
-
+
loginFormWrapperBottomConstraint.constant = remainder.height
view.layoutIfNeeded()
}
diff --git a/ios/MullvadVPN/MullvadAPI.swift b/ios/MullvadVPN/MullvadAPI.swift
new file mode 100644
index 0000000000..b5b49be39c
--- /dev/null
+++ b/ios/MullvadVPN/MullvadAPI.swift
@@ -0,0 +1,59 @@
+//
+// MullvadAPI.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import Foundation
+
+private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")!
+
+class MullvadAPI {
+
+ class func getRelayList(completion: @escaping (_ result: Result<JsonRpcResponse<RelayList>, Error>) -> Void) -> URLSessionDataTask {
+ let urlRequest = try! makeURLRequest(method: "POST", rpcRequest: JsonRpcRequest(method: "relay_list_v2"))
+
+ return URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
+ DispatchQueue.main.async {
+ completion(error.flatMap({ Result.failure($0) }) ?? Result(catching: {
+ try self.decodeResponse(data: try data.unwrap())
+ }))
+ }
+ }
+ }
+
+ private class func decodeResponse<T: Decodable>(data: Data) throws -> JsonRpcResponse<T> {
+ let decoder = defaultJsonDecoder()
+
+ return try decoder.decode(JsonRpcResponse<T>.self, from: data)
+ }
+
+ private class func makeURLRequest<T: Encodable>(method: String, rpcRequest: JsonRpcRequest<T>) throws -> URLRequest {
+ let encoder = defaultJsonEncoder()
+
+ var urlRequest = URLRequest(url: kMullvadAPIURL)
+ urlRequest.httpMethod = method
+ urlRequest.httpBody = try encoder.encode(rpcRequest)
+ urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ return urlRequest
+ }
+
+}
+
+private func defaultJsonEncoder() -> JSONEncoder {
+ let encoder = JSONEncoder()
+ encoder.keyEncodingStrategy = .convertToSnakeCase
+ encoder.dateEncodingStrategy = .iso8601
+ return encoder
+}
+
+private func defaultJsonDecoder() -> JSONDecoder {
+ let decoder = JSONDecoder()
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+ decoder.dateDecodingStrategy = .iso8601
+ return decoder
+}
+
diff --git a/ios/MullvadVPN/Optional+Unwrap.swift b/ios/MullvadVPN/Optional+Unwrap.swift
new file mode 100644
index 0000000000..80698c5e94
--- /dev/null
+++ b/ios/MullvadVPN/Optional+Unwrap.swift
@@ -0,0 +1,23 @@
+
+//
+// Optional+Unwrap.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import Foundation
+
+class NilUnwrapError: Error {}
+
+extension Optional {
+ func unwrap() throws -> Wrapped {
+ switch self {
+ case .some(let value):
+ return value
+ case .none:
+ throw NilUnwrapError()
+ }
+ }
+}
diff --git a/ios/MullvadVPN/RelayList.swift b/ios/MullvadVPN/RelayList.swift
new file mode 100644
index 0000000000..415b549942
--- /dev/null
+++ b/ios/MullvadVPN/RelayList.swift
@@ -0,0 +1,35 @@
+
+//
+// RelayList.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import Foundation
+
+struct RelayList: Decodable {
+ struct Country: Decodable {
+ let name: String
+ let code: String
+ let cities: [City]
+ }
+
+ struct City: Decodable {
+ let name: String
+ let code: String
+ let latitude: Double
+ let longitude: Double
+ let relays: [Hostname]
+ }
+
+ struct Hostname: Decodable {
+ let hostname: String
+ let ipv4AddrIn: String
+ let includeInCountry: Bool
+ let weight: Int32
+ }
+
+ let countries: [Country]
+}
diff --git a/ios/MullvadVPN/RelayStatusIndicatorView.swift b/ios/MullvadVPN/RelayStatusIndicatorView.swift
new file mode 100644
index 0000000000..436f4b1aac
--- /dev/null
+++ b/ios/MullvadVPN/RelayStatusIndicatorView.swift
@@ -0,0 +1,79 @@
+//
+// RelayStatusIndicatorView.swift
+// MullvadVPN
+//
+// Created by pronebird on 01/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import UIKit
+
+@IBDesignable class RelayStatusIndicatorView: UIControl {
+
+ private let circleLayer: CAShapeLayer = {
+ let layer = CAShapeLayer()
+ layer.needsDisplayOnBoundsChange = true
+ return layer
+ }()
+
+ @IBInspectable var isActive: Bool = false {
+ didSet {
+ updateCircleLayerColor()
+ }
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ setup()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+
+ setup()
+ }
+
+ private func setup() {
+ backgroundColor = UIColor.clear
+
+ layer.addSublayer(circleLayer)
+ updateCircleLayerColor()
+ }
+
+ override var isHighlighted: Bool {
+ didSet {
+ updateCircleLayerColor()
+ }
+ }
+
+ private func updateCircleLayerColor() {
+ let baseColor = isActive
+ ? UIColor.relayStatusIndicatorActiveColor
+ : UIColor.relayStatusIndicatorInactiveColor
+
+ let circleColor = isHighlighted
+ ? baseColor.darkened(by: 0.2) ?? baseColor
+ : baseColor
+
+ circleLayer.fillColor = circleColor.cgColor
+ }
+
+ override func layoutSublayers(of layer: CALayer) {
+ super.layoutSublayers(of: layer)
+
+ guard layer == self.layer else { return }
+
+ // keep the circular layer square
+ let shortSide = min(layer.bounds.width, layer.bounds.height)
+ let circleOrigin = CGPoint(
+ x: (layer.bounds.width - shortSide) * 0.5,
+ y: (layer.bounds.height - shortSide) * 0.5
+ )
+ let circleSize = CGSize(width: shortSide, height: shortSide)
+ let bezierPath = UIBezierPath(ovalIn: CGRect(origin: .zero, size: circleSize))
+
+ circleLayer.frame = CGRect(origin: circleOrigin, size: circleSize)
+ circleLayer.path = bezierPath.cgPath
+ }
+}
diff --git a/ios/MullvadVPN/SegueIdentifier.swift b/ios/MullvadVPN/SegueIdentifier.swift
index 1279d8a2bb..9558ef68fd 100644
--- a/ios/MullvadVPN/SegueIdentifier.swift
+++ b/ios/MullvadVPN/SegueIdentifier.swift
@@ -10,17 +10,21 @@ import UIKit
// A phantom struct holding the storyboard segue identifiers for each view controller
struct SegueIdentifier {
-
+
enum Connect: String, SegueConvertible {
case embedHeader = "EmbedHeaderBar"
case showSettings = "ShowSettings"
}
-
+
enum Login: String, SegueConvertible {
case embedHeader = "EmbedHeaderBar"
case showSettings = "ShowSettings"
}
+ enum SelectLocation: String, SegueConvertible {
+ case returnToConnectWithNewRelay = "ReturnToConnectWithNewRelay"
+ }
+
private init() {}
}
diff --git a/ios/MullvadVPN/SelectLocationCell.swift b/ios/MullvadVPN/SelectLocationCell.swift
new file mode 100644
index 0000000000..e7e4c82318
--- /dev/null
+++ b/ios/MullvadVPN/SelectLocationCell.swift
@@ -0,0 +1,111 @@
+//
+// SelectLocationCell.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import UIKit
+
+class SelectLocationCell: BasicTableViewCell {
+ typealias CollapseHandler = (SelectLocationCell) -> Void
+
+ @IBOutlet var locationLabel: UILabel!
+ @IBOutlet var statusIndicator: RelayStatusIndicatorView!
+ @IBOutlet var tickImageView: UIImageView!
+ @IBOutlet var collapseButton: UIButton!
+
+ private let chevronDown = UIImage(imageLiteralResourceName: "IconChevronDown")
+ private let chevronUp = UIImage(imageLiteralResourceName: "IconChevronUp")
+
+ var isExpanded = false {
+ didSet {
+ updateCollapseImage()
+ }
+ }
+
+ var showsCollapseControl = false {
+ didSet {
+ collapseButton.isHidden = !showsCollapseControl
+ }
+ }
+
+ var didCollapseHandler: CollapseHandler?
+
+ private let preferredMargins = UIEdgeInsets(top: 16, left: 28, bottom: 16, right: 12)
+
+ override var indentationLevel: Int {
+ didSet {
+ updateBackgroundColor()
+ }
+ }
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ indentationWidth = 16
+
+ collapseButton.addTarget(self, action: #selector(handleCollapseButton(_ :)), for: .touchUpInside)
+
+ updateCollapseImage()
+ }
+
+ override func layoutMarginsDidChange() {
+ super.layoutMarginsDidChange()
+
+ // enforce the preferred layout margins
+ if contentView.layoutMargins != preferredMargins {
+ contentView.layoutMargins = preferredMargins
+ }
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ let indentPoints = CGFloat(indentationLevel) * indentationWidth
+
+ contentView.frame = CGRect(
+ x: indentPoints,
+ y: contentView.frame.origin.y,
+ width: contentView.frame.size.width - indentPoints,
+ height: contentView.frame.size.height
+ )
+ }
+
+ override func setSelected(_ selected: Bool, animated: Bool) {
+ super.setSelected(selected, animated: animated)
+
+ updateTickImage()
+ }
+
+ private func updateTickImage() {
+ statusIndicator.isHidden = isSelected
+ tickImageView?.isHidden = !isSelected
+ }
+
+ private func updateBackgroundColor() {
+ backgroundView?.backgroundColor = colorForIdentationLevel()
+ }
+
+ private func colorForIdentationLevel() -> UIColor {
+ switch indentationLevel {
+ case 1:
+ return UIColor.subCellBackgroundColor
+ case 2:
+ return UIColor.subSubCellBackgroundColor
+ default:
+ return UIColor.cellBackgroundColor
+ }
+ }
+
+ @objc private func handleCollapseButton(_ sender: UIControl) {
+ didCollapseHandler?(self)
+ }
+
+ private func updateCollapseImage() {
+ let image = isExpanded ? chevronUp : chevronDown
+
+ collapseButton.setImage(image, for: .normal)
+ }
+}
diff --git a/ios/MullvadVPN/SelectLocationController.swift b/ios/MullvadVPN/SelectLocationController.swift
new file mode 100644
index 0000000000..46ab41a23a
--- /dev/null
+++ b/ios/MullvadVPN/SelectLocationController.swift
@@ -0,0 +1,285 @@
+//
+// SelectLocationController.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import UIKit
+import os.log
+
+private let cellIdentifier = "Cell"
+
+class SelectLocationController: UITableViewController {
+
+ private var relayList: RelayList?
+ private var expandedItems = [RelayListDataSourceItem]()
+ private var displayedItems = [RelayListDataSourceItem]()
+
+ var selectedItem: RelayListDataSourceItem?
+
+ // MARK: - View lifecycle
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ loadRelayList()
+ updateTableHeaderViewSize(tableViewSize: tableView.frame.size)
+ }
+
+ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+ super.viewWillTransition(to: size, with: coordinator)
+
+ updateTableHeaderViewSize(tableViewSize: size)
+ }
+
+ // MARK: - UITableViewDataSource
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return displayedItems.count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! SelectLocationCell
+ let item = displayedItems[indexPath.row]
+
+ cell.locationLabel.text = item.displayName()
+ cell.statusIndicator.isActive = item.hasActiveRelays()
+ cell.showsCollapseControl = item.isCollapsibleLevel()
+ cell.isExpanded = expandedItems.contains(item)
+ cell.didCollapseHandler = { [weak self] (cell) in
+ self?.collapseCell(cell)
+ }
+
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
+ let item = displayedItems[indexPath.row]
+
+ return item.indentationLevel()
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ selectedItem = displayedItems[indexPath.row]
+
+ // Return back to the main view after selecting the relay
+ tableView.isUserInteractionEnabled = false
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
+ self.performSegue(withIdentifier:
+ SegueIdentifier.SelectLocation.returnToConnectWithNewRelay.rawValue, sender: self)
+ }
+ }
+
+ // MARK: - UIScrollViewDelegate
+
+ override func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ updateBarVisibility(threshold: 12)
+ }
+
+ // MARK: - Relay list handling
+
+ private func loadRelayList() {
+ let task = MullvadAPI.getRelayList { [weak self] (result) in
+ do {
+ self?.didReceiveRelayList(try result.get())
+ } catch {
+ os_log(.error, "Relay list network error: %{public}s", error.localizedDescription)
+ }
+ }
+
+ task.resume()
+ }
+
+ private func didReceiveRelayList(_ response: JsonRpcResponse<RelayList>) {
+ do {
+ relayList = try response.result.get()
+ } catch {
+ os_log(.error, "Relay list server error: %{public}s", error.localizedDescription)
+ }
+
+ updateDisplayedItems()
+ tableView.reloadData()
+ }
+
+ // MARK: - Collapsible cells
+
+ private func updateDisplayedItems() {
+ displayedItems = relayList?.intoRelayDataSourceItemList(filter: { expandedItems.contains($0) }) ?? []
+ }
+
+ private func collapseCell(_ cell: SelectLocationCell) {
+ guard let cellIndexPath = tableView.indexPath(for: cell) else {
+ return
+ }
+
+ let item = displayedItems[cellIndexPath.row]
+
+ let numberOfItemsBefore = displayedItems.count
+
+ if let index = expandedItems.firstIndex(of: item) {
+ expandedItems.remove(at: index)
+ cell.isExpanded = false
+ } else {
+ expandedItems.append(item)
+ cell.isExpanded = true
+ }
+
+ updateDisplayedItems()
+
+ let numberOfItemsAfter = displayedItems.count - numberOfItemsBefore
+ let indexPathsOfAffectedItems = cellIndexPath.subsequentIndexPaths(count: abs(numberOfItemsAfter))
+
+ if numberOfItemsAfter > 0 {
+ tableView.insertRows(at: indexPathsOfAffectedItems, with: .automatic)
+ } else {
+ tableView.deleteRows(at: indexPathsOfAffectedItems, with: .automatic)
+ }
+ }
+
+ // MARK: - Bar visibility
+
+ private func updateBarVisibility(threshold: CGFloat) {
+ guard let navigationBar = navigationController?.navigationBar as? CustomNavigationBar else {
+ return
+ }
+
+ let shouldShowBar = tableView.contentOffset.y > (-tableView.adjustedContentInset.top + threshold)
+
+ navigationBar.setBarVisible(shouldShowBar, animated: true)
+ }
+
+ // MARK: - UITableView header
+
+ private func updateTableHeaderViewSize(tableViewSize: CGSize) {
+ guard let header = tableView.tableHeaderView else { return }
+
+ // layout the header view
+ header.setNeedsLayout()
+ header.layoutIfNeeded()
+
+ // measure the view size
+ let sizeConstraint = CGSize(
+ width: tableViewSize.width,
+ height: UIView.layoutFittingCompressedSize.height
+ )
+ header.frame.size = header.systemLayoutSizeFitting(sizeConstraint)
+
+ // reset the header view to force UITableView layout pass
+ tableView.tableHeaderView = header
+ }
+}
+
+/// Private extension to convert a RelayList into a flat list of RelayListDataSourceItems
+private extension RelayList {
+
+ typealias FilterFunc = (RelayListDataSourceItem) -> Bool
+
+ func intoRelayDataSourceItemList(filter: FilterFunc) -> [RelayListDataSourceItem] {
+ var items = [RelayListDataSourceItem]()
+
+ for country in countries {
+ let countryItem = RelayListDataSourceItem.country(country)
+
+ items.append(countryItem)
+
+ guard filter(countryItem) else { continue }
+
+ for city in country.cities {
+ let cityItem = RelayListDataSourceItem.city(city)
+
+ items.append(cityItem)
+
+ guard filter(cityItem) else { continue }
+
+ for host in city.relays {
+ items.append(.hostname(host))
+ }
+ }
+ }
+
+ return items
+ }
+
+}
+
+/// A wrapper type for RelayList to be able to represent it as a flat list
+enum RelayListDataSourceItem: Equatable {
+
+ case country(RelayList.Country)
+ case city(RelayList.City)
+ case hostname(RelayList.Hostname)
+
+ static func == (lhs: RelayListDataSourceItem, rhs: RelayListDataSourceItem) -> Bool {
+ switch (lhs, rhs) {
+ case (.country(let a), .country(let b)):
+ return a.code == b.code
+
+ case (.city(let a), .city(let b)):
+ return a.code == b.code
+
+ case (.hostname(let a), .hostname(let b)):
+ return a.hostname == b.hostname
+
+ default:
+ return false
+ }
+ }
+}
+
+private extension RelayListDataSourceItem {
+
+ func indentationLevel() -> Int {
+ switch self {
+ case .country:
+ return 0
+ case .city:
+ return 1
+ case .hostname:
+ return 2
+ }
+ }
+
+ func displayName() -> String {
+ switch self {
+ case .country(let country):
+ return country.name
+ case .city(let city):
+ return city.name
+ case .hostname(let relay):
+ return relay.hostname
+ }
+ }
+
+ func hasActiveRelays() -> Bool {
+ switch self {
+ case .country(let country):
+ return country.cities.count > 0
+ case .city(let city):
+ return city.relays.count > 0
+ case .hostname:
+ return true
+ }
+ }
+
+ func isCollapsibleLevel() -> Bool {
+ switch self {
+ case .country, .city:
+ return true
+ case .hostname:
+ return false
+ }
+ }
+
+}
+
+private extension IndexPath {
+ func subsequentIndexPaths(count: Int) -> [IndexPath] {
+ return (1...count).map({ IndexPath(row: self.row + $0, section: self.section) })
+ }
+}
diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift
index d620b14f37..af20b8dd11 100644
--- a/ios/MullvadVPN/SettingsViewController.swift
+++ b/ios/MullvadVPN/SettingsViewController.swift
@@ -11,49 +11,49 @@ import UIKit
private let kAccountCellIdentifier = "Account"
class SettingsViewController: UITableViewController {
-
+
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
-
+
// MARK: - IBActions
-
+
@IBAction func handleDismiss() {
dismiss(animated: true)
}
// MARK: - UITableViewDataSource
-
+
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// TODO: implement
}
// MARK: - UITableViewDelegate
-
+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
+
if indexPath.section == 0 {
switch indexPath.row {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: kAccountCellIdentifier, for: indexPath)
-
+
return cell
-
+
default:
break
}
}
-
+
fatalError("Index path \(indexPath) is not handled.")
}
-
+
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
-
+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
-
+
}
diff --git a/ios/MullvadVPN/TranslucentButtonBlurView.swift b/ios/MullvadVPN/TranslucentButtonBlurView.swift
index 5b86073ed9..ea835497c0 100644
--- a/ios/MullvadVPN/TranslucentButtonBlurView.swift
+++ b/ios/MullvadVPN/TranslucentButtonBlurView.swift
@@ -11,22 +11,22 @@ import UIKit
private let kButtonCornerRadius = CGFloat(4)
@IBDesignable class TranslucentButtonBlurView: UIVisualEffectView {
-
+
override init(effect: UIVisualEffect?) {
super.init(effect: effect)
-
+
setup()
}
-
+
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
-
+
setup()
}
-
+
private func setup() {
layer.cornerRadius = kButtonCornerRadius
layer.masksToBounds = true
}
-
+
}
diff --git a/ios/MullvadVPN/UIColor+Helpers.swift b/ios/MullvadVPN/UIColor+Helpers.swift
new file mode 100644
index 0000000000..20adddd5a7
--- /dev/null
+++ b/ios/MullvadVPN/UIColor+Helpers.swift
@@ -0,0 +1,37 @@
+//
+// UIColor+Helpers.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/05/2019.
+// Copyright © 2019 Amagicom AB. All rights reserved.
+//
+
+import UIKit
+
+extension UIColor {
+
+ /// Returns the color lighter by the given percent (in range from 0..1)
+ func lightened(by percent: CGFloat) -> UIColor? {
+ return darkened(by: -percent)
+ }
+
+ /// Returns the color darker by the given percent (in range from 0..1)
+ func darkened(by percent: CGFloat) -> UIColor? {
+ var r = CGFloat.zero, g = CGFloat.zero, b = CGFloat.zero, a = CGFloat.zero
+ let factor = 1.0 - percent
+
+ if getRed(&r, green: &g, blue: &b, alpha: &a) {
+ return UIColor(red: clampColorComponent(r * factor),
+ green: clampColorComponent(g * factor),
+ blue: clampColorComponent(b * factor),
+ alpha: a)
+ }
+
+ return nil
+ }
+
+}
+
+private func clampColorComponent(_ value: CGFloat) -> CGFloat {
+ return min(1.0, max(value, 0.0))
+}
diff --git a/ios/MullvadVPN/UIColor+Palette.swift b/ios/MullvadVPN/UIColor+Palette.swift
index c6fa9d4d38..bfe80e5bd2 100644
--- a/ios/MullvadVPN/UIColor+Palette.swift
+++ b/ios/MullvadVPN/UIColor+Palette.swift
@@ -11,4 +11,15 @@ import UIKit
extension UIColor {
// Account text field
static let accountTextFieldBorderColor = UIColor(red: 0.10, green: 0.18, blue: 0.27, alpha: 1.0)
+
+ // Relay availability indicator view
+ static let relayStatusIndicatorActiveColor = UIColor(red: 0.27, green: 0.68, blue: 0.30, alpha: 0.9)
+ static let relayStatusIndicatorInactiveColor = UIColor(red: 0.82, green: 0.01, blue: 0.11, alpha: 0.95)
+
+ // Cells
+ static let cellBackgroundColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 1.0)
+ static let subCellBackgroundColor = UIColor(red:0.15, green:0.23, blue:0.33, alpha:1.0)
+ static let subSubCellBackgroundColor = UIColor(red:0.13, green:0.20, blue:0.30, alpha:1.0)
+
+ static let cellSelectedBackgroundColor = UIColor(red: 0.27, green: 0.68, blue: 0.30, alpha: 1.0)
}