Skip to content

Commit

Permalink
Add UI for creating and editing a custom list
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Feb 16, 2024
1 parent 1a6cc86 commit 13dbec5
Show file tree
Hide file tree
Showing 16 changed files with 652 additions and 21 deletions.
6 changes: 4 additions & 2 deletions ios/MullvadSettings/CustomList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import MullvadTypes
public struct CustomList: Codable, Equatable {
public let id: UUID
public var name: String
public var list: [RelayLocation] = []
public init(id: UUID, name: String) {
public var locations: [RelayLocation]

public init(id: UUID = UUID(), name: String, locations: [RelayLocation]) {
self.id = id
self.name = name
self.locations = locations
}
}
4 changes: 2 additions & 2 deletions ios/MullvadSettings/CustomListRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ public struct CustomListRepository: CustomListRepositoryProtocol {

public init() {}

public func create(_ name: String) throws -> CustomList {
public func create(_ name: String, locations: [RelayLocation]) throws -> CustomList {
var lists = fetchAll()
if lists.contains(where: { $0.name == name }) {
throw CustomRelayListError.duplicateName
} else {
let item = CustomList(id: UUID(), name: name)
let item = CustomList(id: UUID(), name: name, locations: locations)
lists.append(item)
try write(lists)
return item
Expand Down
3 changes: 2 additions & 1 deletion ios/MullvadSettings/CustomListRepositoryProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ public protocol CustomListRepositoryProtocol {

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

/// Fetch all custom list.
/// - Returns: all custom list model .
Expand Down
46 changes: 45 additions & 1 deletion ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,15 @@
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D7B2B03BDD100E7CDD7 /* AccessMethodViewModel.swift */; };
7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */; };
7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */; };
7A6389DB2B7E3BD6008E77E1 /* CustomListCellConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */; };
7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */; };
7A6389DD2B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */; };
7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D52B7E3BD6008E77E1 /* CustomListItemIdentifier.swift */; };
7A6389DF2B7E3BD6008E77E1 /* AddCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */; };
7A6389E12B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D92B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift */; };
7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */; };
7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */; };
7A6389E72B7E42BE008E77E1 /* CustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */; };
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
Expand Down Expand Up @@ -1727,6 +1736,15 @@
7A6000F52B60092F001CF0D9 /* AccessMethodViewModelEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodViewModelEditing.swift; sourceTree = "<group>"; };
7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentConfiguration.swift; sourceTree = "<group>"; };
7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentView.swift; sourceTree = "<group>"; };
7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListCellConfiguration.swift; sourceTree = "<group>"; };
7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListViewModel.swift; sourceTree = "<group>"; };
7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListDataSourceConfiguration.swift; sourceTree = "<group>"; };
7A6389D52B7E3BD6008E77E1 /* CustomListItemIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListItemIdentifier.swift; sourceTree = "<group>"; };
7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCustomListCoordinator.swift; sourceTree = "<group>"; };
7A6389D92B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListSectionIdentifier.swift; sourceTree = "<group>"; };
7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListInteractor.swift; sourceTree = "<group>"; };
7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomListCoordinator.swift; sourceTree = "<group>"; };
7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListViewController.swift; sourceTree = "<group>"; };
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3003,6 +3021,8 @@
58CAF9F22983D32200BE19F7 /* Coordinators */ = {
isa = PBXGroup;
children = (
7A6389D12B7E3BD6008E77E1 /* CustomLists */,
58EFC76F2AFB3FA800E9F4CB /* Settings */,
7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */,
7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */,
7A9CCCA32A96302700DD6A34 /* AddCreditSucceededCoordinator.swift */,
Expand All @@ -3018,7 +3038,6 @@
7A9CCCA52A96302700DD6A34 /* RevokedCoordinator.swift */,
7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */,
7A9CCCA72A96302700DD6A34 /* SelectLocationCoordinator.swift */,
58EFC76F2AFB3FA800E9F4CB /* Settings */,
7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */,
7A9CCCA22A96302700DD6A34 /* TermsOfServiceCoordinator.swift */,
7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */,
Expand Down Expand Up @@ -3381,6 +3400,22 @@
path = IPOverride;
sourceTree = "<group>";
};
7A6389D12B7E3BD6008E77E1 /* CustomLists */ = {
isa = PBXGroup;
children = (
7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */,
7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */,
7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */,
7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */,
7A6389D52B7E3BD6008E77E1 /* CustomListItemIdentifier.swift */,
7A6389D92B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift */,
7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */,
7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */,
7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */,
);
path = CustomLists;
sourceTree = "<group>";
};
7A83C3FC2A55B39500DFB83A /* TestPlans */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4908,6 +4943,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */,
7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */,
5827B0A42B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift in Sources */,
586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */,
Expand Down Expand Up @@ -5071,6 +5107,7 @@
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */,
5867771629097C5B006F721F /* ProductState.swift in Sources */,
7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */,
58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */,
58CEB3042AFD36CE00E6E088 /* SwitchCellContentView.swift in Sources */,
F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */,
Expand All @@ -5086,8 +5123,11 @@
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */,
7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */,
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
7A6389E12B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift in Sources */,
58CEB2F32AFD0BA100E6E088 /* TextCellContentView.swift in Sources */,
7A6389E72B7E42BE008E77E1 /* CustomListViewController.swift in Sources */,
586C0D7C2B03BDD100E7CDD7 /* AccessMethodViewModel.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */,
Expand All @@ -5107,6 +5147,7 @@
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */,
5877F94E2A0A59AA0052D9E9 /* NotificationResponse.swift in Sources */,
7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */,
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
Expand Down Expand Up @@ -5167,6 +5208,7 @@
581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */,
5827B0BB2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift in Sources */,
7A83C4022A57FAA800DFB83A /* SettingsDNSInfoCell.swift in Sources */,
7A6389DF2B7E3BD6008E77E1 /* AddCustomListCoordinator.swift in Sources */,
586C0D952B03D92100E7CDD7 /* SocksItemIdentifier.swift in Sources */,
F0C6A8432AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift in Sources */,
7AF10EB42ADE85BC00C090B9 /* RelayFilterCoordinator.swift in Sources */,
Expand Down Expand Up @@ -5203,6 +5245,7 @@
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */,
F0D8825B2B04F53600D3EF9A /* OutgoingConnectionData.swift in Sources */,
7A6F2FAF2AFE36E7006D0856 /* PreferencesInfoButtonItem.swift in Sources */,
7A6389DD2B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift in Sources */,
5827B0BF2B14B37D00CCBBA1 /* Publisher+PreviousValue.swift in Sources */,
7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */,
5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */,
Expand All @@ -5221,6 +5264,7 @@
7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */,
585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
7A6389DB2B7E3BD6008E77E1 /* CustomListCellConfiguration.swift in Sources */,
F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */,
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// AddCustomListCoordinator.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-02-14.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Combine
import MullvadSettings
import Routing
import UIKit

class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
let navigationController: UINavigationController
let customListInteractor: CustomListInteractorProtocol

var presentedViewController: UIViewController {
navigationController
}

var didFinish: (() -> Void)?

init(
navigationController: UINavigationController,
customListInteractor: CustomListInteractorProtocol
) {
self.navigationController = navigationController
self.customListInteractor = customListInteractor
}

func start() {
let subject = CurrentValueSubject<CustomListViewModel, Never>(
CustomListViewModel(id: UUID(), name: "", locations: [], tableSections: [.name, .addLocations])
)

let controller = CustomListViewController(
interactor: customListInteractor,
subject: subject,
alertPresenter: AlertPresenter(context: self)
)
controller.delegate = self

controller.navigationItem.title = NSLocalizedString(
"CUSTOM_LIST_NAVIGATION_EDIT_TITLE",
tableName: "CustomLists",
value: "New custom list",
comment: ""
)

controller.saveBarButton.title = NSLocalizedString(
"CUSTOM_LIST_NAVIGATION_CREATE_BUTTON",
tableName: "CustomLists",
value: "Create",
comment: ""
)

navigationController.pushViewController(controller, animated: false)
}
}

extension AddCustomListCoordinator: CustomListViewControllerDelegate {
func customListDidSave() {
didFinish?()
}

func customListDidDelete() {
// No op.
}

func showLocations() {
// TODO: Show view controller for locations.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// CustomListCellConfiguration.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-02-14.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Combine
import UIKit

struct CustomListCellConfiguration {
let tableView: UITableView
let subject: CurrentValueSubject<CustomListViewModel, Never>

var onDelete: (() -> Void)?

func dequeueCell(at indexPath: IndexPath, for itemIdentifier: CustomListItemIdentifier) -> UITableViewCell {
let cell = tableView.dequeueReusableView(withIdentifier: itemIdentifier.cellIdentifier, for: indexPath)

configureBackground(cell: cell, itemIdentifier: itemIdentifier)

switch itemIdentifier {
case .name:
configureName(cell, itemIdentifier: itemIdentifier)
case .addLocations, .editLocations:
configureLocations(cell, itemIdentifier: itemIdentifier)
case .deleteList:
configureDelete(cell, itemIdentifier: itemIdentifier)
}

return cell
}

private func configureBackground(cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
guard let cell = cell as? DynamicBackgroundConfiguration else { return }
cell.setAutoAdaptingBackgroundConfiguration(.mullvadListGroupedCell(), selectionType: .dimmed)
}

private func configureName(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
var contentConfiguration = TextCellContentConfiguration()

contentConfiguration.text = itemIdentifier.text
contentConfiguration.setPlaceholder(type: .required)
contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled()
contentConfiguration.inputText = subject.value.name
contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name)

cell.contentConfiguration = contentConfiguration
}

private func configureLocations(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: tableView.style)

contentConfiguration.text = itemIdentifier.text
cell.contentConfiguration = contentConfiguration

if let cell = cell as? CustomCellDisclosureHandling {
cell.disclosureType = .chevron
}
}

private func configureDelete(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
var contentConfiguration = ButtonCellContentConfiguration()

contentConfiguration.style = .tableInsetGroupedDanger
contentConfiguration.text = itemIdentifier.text
contentConfiguration.primaryAction = UIAction { _ in
onDelete?()
}

cell.contentConfiguration = contentConfiguration
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// CustomListDataSourceConfigurationv.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-02-14.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import UIKit

class CustomListDataSourceConfiguration: NSObject {
let dataSource: UITableViewDiffableDataSource<CustomListSectionIdentifier, CustomListItemIdentifier>

var didSelectItem: ((CustomListItemIdentifier) -> Void)?

init(dataSource: UITableViewDiffableDataSource<CustomListSectionIdentifier, CustomListItemIdentifier>) {
self.dataSource = dataSource
}

func updateDataSource(
sections: [CustomListSectionIdentifier],
animated: Bool,
completion: (() -> Void)? = nil
) {
var snapshot = NSDiffableDataSourceSnapshot<CustomListSectionIdentifier, CustomListItemIdentifier>()

sections.forEach { section in
switch section {
case .name:
snapshot.appendSections([.name])
snapshot.appendItems([.name], toSection: .name)
case .addLocations:
snapshot.appendSections([.addLocations])
snapshot.appendItems([.addLocations], toSection: .addLocations)
case .editLocations:
snapshot.appendSections([.editLocations])
snapshot.appendItems([.editLocations], toSection: .editLocations)
case .deleteList:
snapshot.appendSections([.deleteList])
snapshot.appendItems([.deleteList], toSection: .deleteList)
}
}

dataSource.apply(snapshot, animatingDifferences: animated)
}
}

extension CustomListDataSourceConfiguration: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
UIMetrics.SettingsCell.customListsCellHeight
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)

dataSource.itemIdentifier(for: indexPath).flatMap { item in
didSelectItem?(item)
}
}
}
Loading

0 comments on commit 13dbec5

Please sign in to comment.