diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-05-14 15:24:36 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-05-14 15:24:36 +0200 |
| commit | 35fff9db7e386ff575bd683e8844bbebb0229ab0 (patch) | |
| tree | c8c5f3020549b452e09ef5bfcbd4647eff76318d | |
| parent | c6a8e6ddd7405a5560fee9c9651cc4a48660d460 (diff) | |
| parent | 3d1cdd8c18dd5d98296ff31ea0354357b296a8bf (diff) | |
| download | mullvadvpn-35fff9db7e386ff575bd683e8844bbebb0229ab0.tar.xz mullvadvpn-35fff9db7e386ff575bd683e8844bbebb0229ab0.zip | |
Merge branch 'select-location-ios'
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) } |
