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 Feb 28, 2024
1 parent b74b165 commit f73eadb
Show file tree
Hide file tree
Showing 21 changed files with 682 additions and 471 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
46 changes: 21 additions & 25 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 3 additions & 3 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 @@ -703,11 +703,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}

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

let selectLocationCoordinator = SelectLocationCoordinator(
let selectLocationCoordinator = LocationCoordinator(
navigationController: navigationController,
tunnelManager: tunnelManager,
relayCacheTracker: relayCacheTracker
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// CustomListsDataSource.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-02-22.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadREST
import MullvadSettings
import MullvadTypes

class CustomListsDataSource: LocationDataSourceProtocol {
private(set) var nodes = [LocationNode]()
private(set) var repository: CustomListRepositoryProtocol

init(repository: CustomListRepositoryProtocol) {
self.repository = repository
}

var searchableNodes: [LocationNode] {
nodes.flatMap { $0.children }
}

func reload(allLocationNodes: [LocationNode]) {
nodes = repository.fetchAll().map { list in
let listNode = LocationListNode(
nodeName: list.name,
nodeCode: list.name.lowercased(),
locations: list.locations,
customList: list
)

listNode.children = list.locations.compactMap { location in
copy(location, from: allLocationNodes, withParent: listNode)
}

listNode.forEachDescendant { _, node in
node.nodeCode = "\(listNode.nodeCode)-\(node.nodeCode)"
}

return listNode
}
}

func node(by locations: [RelayLocation], for customList: CustomList) -> LocationNode? {
guard let customListNode = nodes.first(where: { $0.nodeName == customList.name })
else { return nil }

if locations.count > 1 {
return customListNode
} else {
return switch locations.first {
case let .country(countryCode):
customListNode.nodeFor(nodeCode: "\(customListNode.nodeCode)-\(countryCode)")
case let .city(_, cityCode):
customListNode.nodeFor(nodeCode: "\(customListNode.nodeCode)-\(cityCode)")
case let .hostname(_, _, hostCode):
customListNode.nodeFor(nodeCode: "\(customListNode.nodeCode)-\(hostCode)")
case .none:
nil
}
}
}

func customList(by id: UUID) -> CustomList? {
repository.fetch(by: id)
}

private func copy(
_ location: RelayLocation,
from allLocationNodes: [LocationNode],
withParent parentNode: LocationNode
) -> LocationNode? {
let rootNode = RootNode(children: allLocationNodes)

return switch location {
case let .country(countryCode):
rootNode
.countryFor(countryCode: countryCode)?.copy(withParent: parentNode)

case let .city(countryCode, cityCode):
rootNode
.countryFor(countryCode: countryCode)?.copy(withParent: parentNode)
.cityFor(cityCode: cityCode)

case let .hostname(countryCode, cityCode, hostCode):
rootNode
.countryFor(countryCode: countryCode)?.copy(withParent: parentNode)
.cityFor(cityCode: cityCode)?
.hostFor(hostCode: hostCode)
}
}
}
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 @@ -13,7 +13,7 @@ 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 +24,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 +39,7 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
}
}

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

init(
navigationController: UINavigationController,
Expand All @@ -52,22 +52,22 @@ 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, customListId in
guard let self else { return }

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

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

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

selectLocationViewController.navigateToFilter = { [weak self] in
Expand All @@ -91,7 +91,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 +101,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
}
}
4 changes: 4 additions & 0 deletions ios/MullvadVPN/UI appearance/UIColor+Palette.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ extension UIColor {
static let backgroundColor = UIColor(red: 0.13, green: 0.20, blue: 0.30, alpha: 1.0)
}

enum SubSubSubCell {
static let backgroundColor = UIColor(red: 0.11, green: 0.17, blue: 0.27, alpha: 1.0)
}

enum HeaderBar {
static let defaultBackgroundColor = primaryColor
static let unsecuredBackgroundColor = dangerColor
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 f73eadb

Please sign in to comment.