Skip to content

Commit 941d596

Browse files
author
Jon Petersson
committed
Add UI for creating a custom list
1 parent aa50f7c commit 941d596

17 files changed

+553
-19
lines changed

ios/MullvadSettings/CustomList.swift

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import MullvadTypes
1212
public struct CustomList: Codable, Equatable {
1313
public let id: UUID
1414
public var name: String
15-
public var list: [RelayLocation] = []
16-
public init(id: UUID, name: String) {
15+
public var locations: [RelayLocation]
16+
17+
public init(id: UUID = UUID(), name: String, locations: [RelayLocation]) {
1718
self.id = id
1819
self.name = name
20+
self.locations = locations
1921
}
2022
}

ios/MullvadSettings/CustomListRepository.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ public struct CustomListRepository: CustomListRepositoryProtocol {
4141

4242
public init() {}
4343

44-
public func create(_ name: String) throws -> CustomList {
45-
var lists = fetchAll()
44+
public func create(_ name: String, locations: [RelayLocation]) throws -> CustomList {
45+
let lists = fetchAll()
4646
if lists.contains(where: { $0.name == name }) {
4747
throw CustomRelayListError.duplicateName
4848
} else {
49-
let item = CustomList(id: UUID(), name: name)
50-
lists.append(item)
51-
try write(lists)
49+
let item = CustomList(id: UUID(), name: name, locations: locations)
5250
return item
5351
}
5452
}

ios/MullvadSettings/CustomListRepositoryProtocol.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ public protocol CustomListRepositoryProtocol {
2828

2929
/// Create a custom list by unique name.
3030
/// - Parameter name: a custom list name.
31+
/// - Parameter locations: locations in a custom list.
3132
/// - Returns: a persistent custom list model upon success, otherwise throws `Error`.
32-
func create(_ name: String) throws -> CustomList
33+
func create(_ name: String, locations: [RelayLocation]) throws -> CustomList
3334

3435
/// Fetch all custom list.
3536
/// - Returns: all custom list model .

ios/MullvadVPN.xcodeproj/project.pbxproj

+60-8
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"pins" : [
3+
{
4+
"identity" : "swift-log",
5+
"kind" : "remoteSourceControl",
6+
"location" : "https://github.com/apple/swift-log.git",
7+
"state" : {
8+
"revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc",
9+
"version" : "1.4.0"
10+
}
11+
},
12+
{
13+
"identity" : "wireguard-apple",
14+
"kind" : "remoteSourceControl",
15+
"location" : "https://github.com/mullvad/wireguard-apple.git",
16+
"state" : {
17+
"revision" : "11a00c20dc03f2751db47e94f585c0778c7bde82"
18+
}
19+
}
20+
],
21+
"version" : 2
22+
}

ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import MullvadREST
10+
import MullvadSettings
1011
import MullvadTypes
1112
import Routing
1213
import UIKit
@@ -68,7 +69,11 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
6869
selectLocationViewController.navigateToFilter = { [weak self] in
6970
guard let self else { return }
7071

71-
let coordinator = makeRelayFilterCoordinator(forModalPresentation: true)
72+
// let coordinator = makeRelayFilterCoordinator(forModalPresentation: true)
73+
let coordinator = AddCustomListViewCoordinator(
74+
navigationController: CustomNavigationController(),
75+
customListInteractor: CustomListInteractor(repository: CustomListRepository())
76+
)
7277
coordinator.start()
7378

7479
presentChild(coordinator, animated: true)

ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//
88

99
import Combine
10-
import struct MullvadTypes.Duration
10+
import MullvadTypes
1111
import UIKit
1212

1313
/// The view controller providing the interface for editing method settings
@@ -20,7 +20,7 @@ class MethodSettingsViewController: UITableViewController {
2020

2121
private let subject: CurrentValueSubject<AccessMethodViewModel, Never>
2222
private let interactor: EditAccessMethodInteractorProtocol
23-
private var cancellables = Set<AnyCancellable>()
23+
private var cancellables = Set<Combine.AnyCancellable>()
2424
private var alertPresenter: AlertPresenter
2525
private var inputValidationErrors: [AccessMethodFieldValidationError] = []
2626
private var contentValidationErrors: [AccessMethodFieldValidationError] = []

ios/MullvadVPN/UI appearance/UIMetrics.swift

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ enum UIMetrics {
8484
static let apiAccessLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 16, bottom: 20, trailing: 16)
8585
static let apiAccessInsetLayoutMargins = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
8686
static let apiAccessCellHeight: CGFloat = 44
87+
static let customListsCellHeight: CGFloat = 44
8788
static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4
8889
static let apiAccessPickerListContentInsetTop: CGFloat = 16
8990
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// AddCustomListViewCoordinator.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-02-14.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Combine
10+
import MullvadSettings
11+
import Routing
12+
import UIKit
13+
14+
class AddCustomListViewCoordinator: Coordinator, Presentable, Presenting {
15+
let navigationController: UINavigationController
16+
let customListInteractor: CustomListInteractorProtocol
17+
18+
var presentedViewController: UIViewController {
19+
navigationController
20+
}
21+
22+
init(
23+
navigationController: UINavigationController,
24+
customListInteractor: CustomListInteractorProtocol
25+
) {
26+
self.navigationController = navigationController
27+
self.customListInteractor = customListInteractor
28+
}
29+
30+
func start() {
31+
let subject = CurrentValueSubject<CustomListViewModel, Never>(
32+
CustomListViewModel(id: UUID(), name: "", locations: [])
33+
)
34+
let controller = AddCustomListViewController(interactor: customListInteractor, subject: subject)
35+
36+
controller.delegate = self
37+
38+
navigationController.pushViewController(controller, animated: false)
39+
}
40+
}
41+
42+
extension AddCustomListViewCoordinator: AddCustomListViewControllerDelegate {
43+
func customListDidSave() {
44+
dismiss(animated: true)
45+
}
46+
47+
func showLocations() {
48+
// Todo: Show view controller for locations.
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// AddCustomListViewController.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-02-14.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Combine
10+
import MullvadSettings
11+
import UIKit
12+
13+
protocol AddCustomListViewControllerDelegate: AnyObject {
14+
func customListDidSave()
15+
func showLocations()
16+
}
17+
18+
class AddCustomListViewController: UIViewController {
19+
typealias DataSource = UITableViewDiffableDataSource<CustomListSectionIdentifier, CustomListItemIdentifier>
20+
21+
private let interactor: CustomListInteractorProtocol
22+
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
23+
private let subject: CurrentValueSubject<CustomListViewModel, Never>
24+
private var cancellables = Set<AnyCancellable>()
25+
private var dataSource: DataSource?
26+
27+
private lazy var cellConfiguration: CustomListCellConfiguration = {
28+
CustomListCellConfiguration(tableView: tableView, subject: subject)
29+
}()
30+
31+
private lazy var dataSourceConfiguration: CustomListDataSourceConfiguration? = {
32+
dataSource.flatMap { dataSource in
33+
CustomListDataSourceConfiguration(dataSource: dataSource)
34+
}
35+
}()
36+
37+
lazy var saveBarButton: UIBarButtonItem = {
38+
let barButtonItem = UIBarButtonItem(
39+
title: NSLocalizedString("SAVE_NAVIGATION_BUTTON", tableName: "CustomLists", value: "Create", comment: ""),
40+
primaryAction: UIAction { [weak self] _ in
41+
self?.onSave()
42+
}
43+
)
44+
barButtonItem.style = .done
45+
return barButtonItem
46+
}()
47+
48+
weak var delegate: AddCustomListViewControllerDelegate?
49+
50+
init(
51+
interactor: CustomListInteractorProtocol,
52+
subject: CurrentValueSubject<CustomListViewModel, Never>
53+
) {
54+
self.subject = subject
55+
self.interactor = interactor
56+
57+
super.init(nibName: nil, bundle: nil)
58+
}
59+
60+
required init?(coder: NSCoder) {
61+
fatalError("init(coder:) has not been implemented")
62+
}
63+
64+
override func viewDidLoad() {
65+
super.viewDidLoad()
66+
67+
view.directionalLayoutMargins = UIMetrics.contentLayoutMargins
68+
view.backgroundColor = .secondaryColor
69+
isModalInPresentation = true
70+
71+
addSubviews()
72+
configureNavigationItem()
73+
configureDataSource()
74+
configureTableView()
75+
76+
subject.sink { [weak self] viewModel in
77+
self?.saveBarButton.isEnabled = !viewModel.name.isEmpty
78+
}
79+
.store(in: &cancellables)
80+
}
81+
82+
private func configureNavigationItem() {
83+
navigationItem.title = NSLocalizedString(
84+
"CUSTOM_LIST_NAVIGATION_ADD_TITLE",
85+
tableName: "CustomLists",
86+
value: "New custom list",
87+
comment: ""
88+
)
89+
90+
navigationItem.leftBarButtonItem = UIBarButtonItem(
91+
systemItem: .cancel,
92+
primaryAction: UIAction(handler: { _ in
93+
self.dismiss(animated: true)
94+
})
95+
)
96+
97+
navigationItem.rightBarButtonItem = saveBarButton
98+
}
99+
100+
private func configureTableView() {
101+
tableView.delegate = dataSourceConfiguration
102+
tableView.backgroundColor = .secondaryColor
103+
tableView.registerReusableViews(from: CustomListItemIdentifier.CellIdentifier.self)
104+
}
105+
106+
private func configureDataSource() {
107+
dataSource = DataSource(
108+
tableView: tableView,
109+
cellProvider: { [weak self] _, indexPath, itemIdentifier in
110+
self?.cellConfiguration.dequeueCell(at: indexPath, for: itemIdentifier)
111+
}
112+
)
113+
114+
dataSourceConfiguration?.didSelectItem = { item in
115+
switch item {
116+
case .name:
117+
break
118+
case .locations:
119+
self.showLocations()
120+
}
121+
}
122+
123+
dataSourceConfiguration?.updateDataSource(animated: false)
124+
}
125+
126+
private func addSubviews() {
127+
view.addConstrainedSubviews([tableView]) {
128+
tableView.pinEdgesToSuperview()
129+
}
130+
}
131+
132+
private func showLocations() {
133+
view.endEditing(false)
134+
delegate?.showLocations()
135+
}
136+
137+
private func onSave() {
138+
do {
139+
try interactor.createCustomList(viewModel: subject.value)
140+
delegate?.customListDidSave()
141+
} catch {
142+
// Todo: Show error dialog.
143+
}
144+
}
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// CustomListCellConfiguration.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-02-14.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Combine
10+
import UIKit
11+
12+
struct CustomListCellConfiguration {
13+
let tableView: UITableView
14+
let subject: CurrentValueSubject<CustomListViewModel, Never>
15+
16+
func dequeueCell(at indexPath: IndexPath, for itemIdentifier: CustomListItemIdentifier) -> UITableViewCell {
17+
let cell = tableView.dequeueReusableView(withIdentifier: itemIdentifier.cellIdentifier, for: indexPath)
18+
19+
configureBackground(cell: cell, itemIdentifier: itemIdentifier)
20+
21+
switch itemIdentifier {
22+
case .name:
23+
configureName(cell, itemIdentifier: itemIdentifier)
24+
case .locations:
25+
configureLocations(cell, itemIdentifier: itemIdentifier)
26+
}
27+
28+
return cell
29+
}
30+
31+
private func configureBackground(cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
32+
guard let cell = cell as? DynamicBackgroundConfiguration else { return }
33+
cell.setAutoAdaptingBackgroundConfiguration(.mullvadListGroupedCell(), selectionType: .dimmed)
34+
}
35+
36+
private func configureName(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
37+
var contentConfiguration = TextCellContentConfiguration()
38+
39+
contentConfiguration.text = itemIdentifier.text
40+
contentConfiguration.setPlaceholder(type: .required)
41+
contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled()
42+
contentConfiguration.inputText = subject.value.name
43+
contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name)
44+
45+
cell.contentConfiguration = contentConfiguration
46+
}
47+
48+
private func configureLocations(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
49+
var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: tableView.style)
50+
51+
contentConfiguration.text = itemIdentifier.text
52+
cell.contentConfiguration = contentConfiguration
53+
54+
if let cell = cell as? CustomCellDisclosureHandling {
55+
cell.disclosureType = .chevron
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// CustomListValidationError.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-02-14.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
enum CustomListFieldValidationError: String {
12+
case name
13+
14+
var description: String {
15+
switch self {
16+
case .name:
17+
"Error"
18+
default:
19+
""
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)