Skip to content

Commit

Permalink
Split select location view into two sections
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Mar 4, 2024
1 parent 75c0537 commit 4107302
Show file tree
Hide file tree
Showing 31 changed files with 1,090 additions and 515 deletions.
1 change: 1 addition & 0 deletions ios/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ disabled_rules:
- type_body_length
- opening_brace # Differs from Google swift guidelines enforced by swiftformat
- trailing_comma
- switch_case_alignment # Enables expressions such as [return switch location {}]

Check warning on line 10 in ios/.swiftlint.yml

View workflow job for this annotation

GitHub Actions / check-formatting

10:27 [comments] too few spaces before comment
opt_in_rules:
- empty_count

Expand Down
78 changes: 53 additions & 25 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}()

private var splitTunnelCoordinator: TunnelCoordinator?
private var splitLocationCoordinator: SelectLocationCoordinator?
private var splitLocationCoordinator: LocationCoordinator?

private let tunnelManager: TunnelManager
private let storePaymentManager: StorePaymentManager
Expand Down Expand Up @@ -488,17 +488,17 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo

private func setupSplitView() {
let tunnelCoordinator = makeTunnelCoordinator()
let selectLocationCoordinator = makeSelectLocationCoordinator(forModalPresentation: false)
let locationCoordinator = makeLocationCoordinator(forModalPresentation: false)

addChild(tunnelCoordinator)
addChild(selectLocationCoordinator)
addChild(locationCoordinator)

splitTunnelCoordinator = tunnelCoordinator
splitLocationCoordinator = selectLocationCoordinator
splitLocationCoordinator = locationCoordinator

splitViewController.delegate = self
splitViewController.viewControllers = [
selectLocationCoordinator.navigationController,
locationCoordinator.navigationController,
tunnelCoordinator.rootViewController,
]

Expand All @@ -508,7 +508,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
.safeAreaLayoutGuide

tunnelCoordinator.start()
selectLocationCoordinator.start()
locationCoordinator.start()
}

private func presentTOS(animated: Bool, completion: @escaping (Coordinator) -> Void) {
Expand Down Expand Up @@ -634,7 +634,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}

private func presentSelectLocation(animated: Bool, completion: @escaping (Coordinator) -> Void) {
let coordinator = makeSelectLocationCoordinator(forModalPresentation: true)
let coordinator = makeLocationCoordinator(forModalPresentation: true)
coordinator.start()

presentChild(coordinator, animated: animated) {
Expand Down Expand Up @@ -702,24 +702,24 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
return tunnelCoordinator
}

private func makeSelectLocationCoordinator(forModalPresentation isModalPresentation: Bool)
-> SelectLocationCoordinator {
private func makeLocationCoordinator(forModalPresentation isModalPresentation: Bool)
-> LocationCoordinator {
let navigationController = CustomNavigationController()
navigationController.isNavigationBarHidden = !isModalPresentation

let selectLocationCoordinator = SelectLocationCoordinator(
let locationCoordinator = LocationCoordinator(
navigationController: navigationController,
tunnelManager: tunnelManager,
relayCacheTracker: relayCacheTracker
)

selectLocationCoordinator.didFinish = { [weak self] _, _ in
locationCoordinator.didFinish = { [weak self] _ in
if isModalPresentation {
self?.router.dismiss(.selectLocation, animated: true)
}
}

return selectLocationCoordinator
return locationCoordinator
}

private func presentAccount(animated: Bool, completion: @escaping (Coordinator) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// SelectLocationCoordinator.swift
// LocationCoordinator.swift
// MullvadVPN
//
// Created by pronebird on 29/01/2023.
Expand All @@ -11,9 +11,7 @@ import MullvadTypes
import Routing
import UIKit

import MullvadSettings

class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrackerObserver {
class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrackerObserver {
private let tunnelManager: TunnelManager
private let relayCacheTracker: RelayCacheTracker
private var cachedRelays: CachedRelays?
Expand All @@ -24,10 +22,10 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
navigationController
}

var selectLocationViewController: SelectLocationViewController? {
var selectLocationViewController: LocationViewController? {
return navigationController.viewControllers.first {
$0 is SelectLocationViewController
} as? SelectLocationViewController
$0 is LocationViewController
} as? LocationViewController
}

var relayFilter: RelayFilter {
Expand All @@ -39,7 +37,7 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
}
}

var didFinish: ((SelectLocationCoordinator, RelayLocation?) -> Void)?
var didFinish: ((LocationCoordinator) -> Void)?

init(
navigationController: UINavigationController,
Expand All @@ -52,22 +50,19 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
}

func start() {
let selectLocationViewController = SelectLocationViewController()
let selectLocationViewController = LocationViewController()

selectLocationViewController.didSelectRelay = { [weak self] relay in
selectLocationViewController.didSelectRelays = { [weak self] locations in
guard let self else { return }

var relayConstraints = tunnelManager.settings.relayConstraints
relayConstraints.locations = .only(RelayLocations(
locations: [relay],
customListId: nil
))
relayConstraints.locations = .only(locations)

tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) {
self.tunnelManager.startTunnel()
}

didFinish?(self, relay)
didFinish?(self)
}

selectLocationViewController.navigateToFilter = { [weak self] in
Expand All @@ -91,7 +86,7 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
selectLocationViewController.didFinish = { [weak self] in
guard let self else { return }

didFinish?(self, nil)
didFinish?(self)
}

relayCacheTracker.addObserver(self)
Expand All @@ -101,8 +96,7 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
selectLocationViewController.setCachedRelays(cachedRelays, filter: relayFilter)
}

selectLocationViewController.relayLocation =
tunnelManager.settings.relayConstraints.locations.value?.locations.first
selectLocationViewController.relayLocations = tunnelManager.settings.relayConstraints.locations.value

navigationController.pushViewController(selectLocationViewController, animated: false)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// SettingsValidationErrorConfiguration.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-02-16.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import UIKit

struct SettingsValidationErrorConfiguration: UIContentConfiguration, Equatable {
var errors: [CustomListFieldValidationError] = []
var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.settingsValidationErrorLayoutMargins

func makeContentView() -> UIView & UIContentView {
return SettingsValidationErrorContentView(configuration: self)
}

func updated(for state: UIConfigurationState) -> Self {
return self
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// SettingsValidationErrorContentView.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-02-16.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import UIKit

class SettingsValidationErrorContentView: UIView, UIContentView {
let contentView = UIStackView()

var icon: UIImageView {
let view = UIImageView(image: UIImage(resource: .iconAlert).withTintColor(.dangerColor))
view.heightAnchor.constraint(equalToConstant: 14).isActive = true
view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1).isActive = true
return view
}

var configuration: UIContentConfiguration {
get {
actualConfiguration
}
set {
guard let newConfiguration = newValue as? SettingsValidationErrorConfiguration else { return }

let previousConfiguration = actualConfiguration
actualConfiguration = newConfiguration

configureSubviews(previousConfiguration: previousConfiguration)
}
}

private var actualConfiguration: SettingsValidationErrorConfiguration

func supports(_ configuration: UIContentConfiguration) -> Bool {
configuration is SettingsValidationErrorConfiguration
}

init(configuration: SettingsValidationErrorConfiguration) {
actualConfiguration = configuration

super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 44))

addSubviews()
configureSubviews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func addSubviews() {
contentView.axis = .vertical
contentView.spacing = 6

addConstrainedSubviews([contentView]) {
contentView.pinEdgesToSuperviewMargins()
}
}

private func configureSubviews(previousConfiguration: SettingsValidationErrorConfiguration? = nil) {
guard actualConfiguration != previousConfiguration else { return }

configureLayoutMargins()

contentView.arrangedSubviews.forEach { view in
view.removeFromSuperview()
}

actualConfiguration.errors.forEach { error in
let label = UILabel()
label.text = error.errorDescription
label.numberOfLines = 0
label.font = .systemFont(ofSize: 13)
label.textColor = .white.withAlphaComponent(0.6)

let stackView = UIStackView(arrangedSubviews: [icon, label])
stackView.alignment = .top
stackView.spacing = 6

contentView.addArrangedSubview(stackView)
}
}

private func configureLayoutMargins() {
directionalLayoutMargins = actualConfiguration.directionalLayoutMargins
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ extension UIBackgroundConfiguration {
/// - Returns: a background configuration
static func mullvadListPlainCell() -> UIBackgroundConfiguration {
var config = listPlainCell()
config.backgroundColor = UIColor.Cell.backgroundColor
config.backgroundColor = UIColor.Cell.Background.normal
return config
}

/// Returns the corresponding grouped cell background configuration adapted for Mullvad UI.
/// - Returns: a background configuration
static func mullvadListGroupedCell() -> UIBackgroundConfiguration {
var config = listGroupedCell()
config.backgroundColor = UIColor.Cell.backgroundColor
config.backgroundColor = UIColor.Cell.Background.normal
return config
}

Expand Down Expand Up @@ -58,20 +58,20 @@ extension UICellConfigurationState {
switch selectionType {
case .dimmed:
if isSelected || isHighlighted {
UIColor.Cell.selectedAltBackgroundColor
UIColor.Cell.Background.selectedAlt
} else if isDisabled {
UIColor.Cell.disabledBackgroundColor
UIColor.Cell.Background.disabled
} else {
UIColor.Cell.backgroundColor
UIColor.Cell.Background.normal
}

case .green:
if isSelected || isHighlighted {
UIColor.Cell.selectedBackgroundColor
UIColor.Cell.Background.selected
} else if isDisabled {
UIColor.Cell.disabledBackgroundColor
UIColor.Cell.Background.disabled
} else {
UIColor.Cell.backgroundColor
UIColor.Cell.Background.normal
}
}
}
Expand Down
27 changes: 13 additions & 14 deletions ios/MullvadVPN/UI appearance/UIColor+Palette.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,18 @@ extension UIColor {

// Cells
enum Cell {
static let backgroundColor = primaryColor
static let disabledBackgroundColor = backgroundColor.darkened(by: 0.3)!

static let selectedBackgroundColor = successColor
static let disabledSelectedBackgroundColor = selectedBackgroundColor.darkened(by: 0.3)!

static let selectedAltBackgroundColor = backgroundColor.darkened(by: 0.2)!
enum Background {
static let indentationLevelZero = primaryColor
static let indentationLevelOne = UIColor(red: 0.15, green: 0.23, blue: 0.33, alpha: 1.0)
static let indentationLevelTwo = UIColor(red: 0.13, green: 0.20, blue: 0.30, alpha: 1.0)
static let indentationLevelThree = UIColor(red: 0.11, green: 0.17, blue: 0.27, alpha: 1.0)

static let normal = indentationLevelZero
static let disabled = normal.darkened(by: 0.3)!
static let selected = successColor
static let disabledSelected = selected.darkened(by: 0.3)!
static let selectedAlt = normal.darkened(by: 0.2)!
}

static let titleTextColor = UIColor.white
static let detailTextColor = UIColor(white: 1.0, alpha: 0.8)
Expand All @@ -109,13 +114,7 @@ extension UIColor {
static let footerTextColor = UIColor(white: 1.0, alpha: 0.6)
}

enum SubCell {
static let backgroundColor = UIColor(red: 0.15, green: 0.23, blue: 0.33, alpha: 1.0)
}

enum SubSubCell {
static let backgroundColor = UIColor(red: 0.13, green: 0.20, blue: 0.30, alpha: 1.0)
}
enum SettingsCellBackground {}

enum HeaderBar {
static let defaultBackgroundColor = primaryColor
Expand Down
2 changes: 1 addition & 1 deletion ios/MullvadVPN/UI appearance/UIMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ extension UIMetrics {
static let contentInsets = UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)

/// Common layout margins for location cell presentation
static let selectLocationCellLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 28, bottom: 16, trailing: 12)
static let locationCellLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 28, bottom: 16, trailing: 12)

/// Layout margins used by content heading displayed below the large navigation title.
static let contentHeadingLayoutMargins = NSDirectionalEdgeInsets(top: 8, leading: 24, bottom: 24, trailing: 24)
Expand Down
Loading

0 comments on commit 4107302

Please sign in to comment.