Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding section header view for select location sections ios 549 #5930

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@
584023292A407F5F007B27AC /* libtunnel_obfuscator_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */; };
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; };
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; };
58435AC229CB2A350099C71B /* LocationCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58435AC129CB2A350099C71B /* LocationCellFactory.swift */; };
584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */; };
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; };
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */; };
Expand Down Expand Up @@ -848,7 +847,9 @@
F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; };
F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; };
F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; };
F0A92B3C2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A92B3B2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift */; };
F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */; };
F0BE65372B9F136A005CC385 /* LocationSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */; };
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; };
F0C3333C2B31A29C00D1A478 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
F0C6A8432AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */; };
Expand Down Expand Up @@ -1419,7 +1420,6 @@
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; };
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; };
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; };
58435AC129CB2A350099C71B /* LocationCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCellFactory.swift; sourceTree = "<group>"; };
584592602639B4A200EF967F /* TermsOfServiceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceContentView.swift; sourceTree = "<group>"; };
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; };
5846227026E229F20035F7C2 /* StoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSubscription.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1981,7 +1981,9 @@
F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionStub.swift; sourceTree = "<group>"; };
F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionService.swift; sourceTree = "<group>"; };
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = "<group>"; };
F0A92B3B2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryCustomListRepository.swift; sourceTree = "<group>"; };
F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = "<group>"; };
F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationSectionHeaderView.swift; sourceTree = "<group>"; };
F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; };
F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = "<group>"; };
F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2403,13 +2405,14 @@
children = (
F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */,
F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */,
F0A92B3B2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift */,
5888AD82227B11080051EB06 /* LocationCell.swift */,
58435AC129CB2A350099C71B /* LocationCellFactory.swift */,
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */,
583DA21325FA4B5C00318683 /* LocationDataSource.swift */,
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */,
7A6389F72B864CDF008E77E1 /* LocationNode.swift */,
F050AE512B70DFC0003F4EDB /* LocationSection.swift */,
F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */,
5888AD86227B17950051EB06 /* LocationViewController.swift */,
);
path = SelectLocation;
Expand Down Expand Up @@ -5115,7 +5118,6 @@
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
7A9CCCB82A96302800DD6A34 /* SetupAccountCompletedCoordinator.swift in Sources */,
58435AC229CB2A350099C71B /* LocationCellFactory.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
7A0B311E2B303A0D004B12E0 /* AccessbilityIdentifier.swift in Sources */,
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */,
Expand All @@ -5129,6 +5131,8 @@
7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */,
58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */,
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */,
F0BE65372B9F136A005CC385 /* LocationSectionHeaderView.swift in Sources */,
F0A92B3C2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift in Sources */,
7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */,
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */,
58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */,
Expand Down
11 changes: 10 additions & 1 deletion ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
*/
private let secondaryNavigationContainer = RootContainerViewController()

private var customListRepository: CustomListRepositoryProtocol {
#if DEBUG
InMemoryCustomListRepository()
#else
CustomListRepository()
#endif
}

/// Posts `preferredAccountNumber` notification when user inputs the account number instead of voucher code
private let preferredAccountNumberSubject = PassthroughSubject<String, Never>()

Expand Down Expand Up @@ -710,7 +718,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
let locationCoordinator = LocationCoordinator(
navigationController: navigationController,
tunnelManager: tunnelManager,
relayCacheTracker: relayCacheTracker
relayCacheTracker: relayCacheTracker,
customListRepository: customListRepository
)

locationCoordinator.didFinish = { [weak self] _ in
Expand Down
30 changes: 21 additions & 9 deletions ios/MullvadVPN/Coordinators/LocationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import MullvadREST
import MullvadSettings
import MullvadTypes
import Routing
import UIKit
Expand All @@ -15,6 +16,7 @@
private let tunnelManager: TunnelManager
private let relayCacheTracker: RelayCacheTracker
private var cachedRelays: CachedRelays?
private var customListRepository: CustomListRepositoryProtocol

let navigationController: UINavigationController

Expand Down Expand Up @@ -42,17 +44,21 @@
init(
navigationController: UINavigationController,
tunnelManager: TunnelManager,
relayCacheTracker: RelayCacheTracker
relayCacheTracker: RelayCacheTracker,
customListRepository: CustomListRepositoryProtocol
) {
self.navigationController = navigationController
self.tunnelManager = tunnelManager
self.relayCacheTracker = relayCacheTracker
self.customListRepository = customListRepository
}

func start() {
let selectLocationViewController = LocationViewController()
let locationViewController = LocationViewController(customListRepository: customListRepository)
locationViewController.delegate = self

locationViewController.didSelectRelays = { [weak self] locations in

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

var relayConstraints = tunnelManager.settings.relayConstraints
Expand All @@ -65,7 +71,7 @@
didFinish?(self)
}

selectLocationViewController.navigateToFilter = { [weak self] in
locationViewController.navigateToFilter = { [weak self] in
guard let self else { return }

let coordinator = makeRelayFilterCoordinator(forModalPresentation: true)
Expand All @@ -74,7 +80,7 @@
presentChild(coordinator, animated: true)
}

selectLocationViewController.didUpdateFilter = { [weak self] filter in
locationViewController.didUpdateFilter = { [weak self] filter in
guard let self else { return }

var relayConstraints = tunnelManager.settings.relayConstraints
Expand All @@ -83,7 +89,7 @@
tunnelManager.updateSettings([.relayConstraints(relayConstraints)])
}

selectLocationViewController.didFinish = { [weak self] in
locationViewController.didFinish = { [weak self] in
guard let self else { return }

didFinish?(self)
Expand All @@ -93,12 +99,12 @@

if let cachedRelays = try? relayCacheTracker.getCachedRelays() {
self.cachedRelays = cachedRelays
selectLocationViewController.setCachedRelays(cachedRelays, filter: relayFilter)
locationViewController.setCachedRelays(cachedRelays, filter: relayFilter)
}

selectLocationViewController.relayLocations = tunnelManager.settings.relayConstraints.locations.value
locationViewController.relayLocations = tunnelManager.settings.relayConstraints.locations.value

navigationController.pushViewController(selectLocationViewController, animated: false)
navigationController.pushViewController(locationViewController, animated: false)
}

private func makeRelayFilterCoordinator(forModalPresentation isModalPresentation: Bool)
Expand Down Expand Up @@ -131,3 +137,9 @@
selectLocationViewController?.setCachedRelays(cachedRelays, filter: relayFilter)
}
}

extension LocationCoordinator: LocationViewControllerDelegate {
func didRequestRouteToCustomLists(_ controller: LocationViewController) {
// TODO: Show add/Edit bottom sheet.

Check warning on line 143 in ios/MullvadVPN/Coordinators/LocationCoordinator.swift

View workflow job for this annotation

GitHub Actions / End to end tests

Todo Violation: TODOs should be resolved (Show add/Edit bottom sheet.) (todo)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// InMemoryCustomListRepository.swift
// MullvadVPN
//
// Created by Mojgan on 2024-01-31.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Combine
import Foundation
import MullvadSettings
import MullvadTypes

class InMemoryCustomListRepository: CustomListRepositoryProtocol {
var publisher: AnyPublisher<[CustomList], Never> {
passthroughSubject.eraseToAnyPublisher()
}

private var customRelayLists: [CustomList] = [
CustomList(
id: UUID(uuidString: "F17948CB-18E2-4F84-82CD-5780F94216DB")!,
name: "Netflix",
locations: [.city("al", "tia")]
),
CustomList(
id: UUID(uuidString: "4104C603-B35D-4A64-8865-96C0BF33D57F")!,
name: "Streaming",
locations: [
.city("us", "dal"),
.country("se"),
.city("de", "ber"),
]
),
]

private let passthroughSubject = PassthroughSubject<[CustomList], Never>()

func update(_ list: CustomList) {
if let index = customRelayLists.firstIndex(where: { $0.id == list.id }) {
customRelayLists[index] = list
}
}

func delete(id: UUID) {
if let index = customRelayLists.firstIndex(where: { $0.id == id }) {
customRelayLists.remove(at: index)
}
}

func fetch(by id: UUID) -> CustomList? {
return customRelayLists.first(where: { $0.id == id })
}

func create(_ name: String, locations: [RelayLocation]) throws -> CustomList {
let item = CustomList(id: UUID(), name: name, locations: locations)
customRelayLists.append(item)
return item
}

func fetchAll() -> [CustomList] {
customRelayLists
}
}
71 changes: 44 additions & 27 deletions ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,46 @@

import UIKit

private let kCollapseButtonWidth: CGFloat = 24
private let kRelayIndicatorSize: CGFloat = 16
protocol LocationCellDelegate: AnyObject {
func toggle(cell: LocationCell)
}

class LocationCell: UITableViewCell {
typealias CollapseHandler = (LocationCell) -> Void
weak var delegate: LocationCellDelegate?

private let locationLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16)
label.textColor = .white
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.lineBreakStrategy = []
return label
}()

let locationLabel = UILabel()
let statusIndicator: UIView = {
private let statusIndicator: UIView = {
let view = UIView()
view.layer.cornerRadius = kRelayIndicatorSize * 0.5
view.layer.cornerRadius = 8
view.layer.cornerCurve = .circular
return view
}()

let tickImageView = UIImageView(image: UIImage(named: "IconTick"))
let collapseButton = UIButton(type: .custom)
private let tickImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(resource: .iconTick))
imageView.tintColor = .white
return imageView
}()

private let collapseButton: UIButton = {
let button = UIButton(type: .custom)
button.accessibilityIdentifier = .collapseButton
button.isAccessibilityElement = false
button.tintColor = .white
return button
}()

private let chevronDown = UIImage(named: "IconChevronDown")
private let chevronUp = UIImage(named: "IconChevronUp")
private let chevronDown = UIImage(resource: .iconChevronDown)
private let chevronUp = UIImage(resource: .iconChevronUp)

var isDisabled = false {
didSet {
Expand All @@ -50,8 +71,6 @@ class LocationCell: UITableViewCell {
}
}

var didCollapseHandler: CollapseHandler?

override var indentationLevel: Int {
didSet {
updateBackgroundColor()
Expand Down Expand Up @@ -103,17 +122,6 @@ class LocationCell: UITableViewCell {
selectedBackgroundView = UIView()
selectedBackgroundView?.backgroundColor = UIColor.Cell.Background.selected

locationLabel.font = UIFont.systemFont(ofSize: 17)
locationLabel.textColor = .white
locationLabel.lineBreakMode = .byWordWrapping
locationLabel.numberOfLines = 0
locationLabel.lineBreakStrategy = []

tickImageView.tintColor = .white

collapseButton.accessibilityIdentifier = .collapseButton
collapseButton.isAccessibilityElement = false
collapseButton.tintColor = .white
collapseButton.addTarget(self, action: #selector(handleCollapseButton(_:)), for: .touchUpInside)

[locationLabel, tickImageView, statusIndicator, collapseButton].forEach { subview in
Expand All @@ -131,7 +139,7 @@ class LocationCell: UITableViewCell {
tickImageView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
tickImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),

statusIndicator.widthAnchor.constraint(equalToConstant: kRelayIndicatorSize),
statusIndicator.widthAnchor.constraint(equalToConstant: 16),
statusIndicator.heightAnchor.constraint(equalTo: statusIndicator.widthAnchor),
statusIndicator.centerXAnchor.constraint(equalTo: tickImageView.centerXAnchor),
statusIndicator.centerYAnchor.constraint(equalTo: tickImageView.centerYAnchor),
Expand All @@ -148,7 +156,7 @@ class LocationCell: UITableViewCell {
collapseButton.widthAnchor
.constraint(
equalToConstant: UIMetrics.contentLayoutMargins.leading + UIMetrics
.contentLayoutMargins.trailing + kCollapseButtonWidth
.contentLayoutMargins.trailing + 24
),
collapseButton.topAnchor.constraint(equalTo: contentView.topAnchor),
collapseButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
Expand Down Expand Up @@ -213,11 +221,11 @@ class LocationCell: UITableViewCell {
}

@objc private func handleCollapseButton(_ sender: UIControl) {
didCollapseHandler?(self)
delegate?.toggle(cell: self)
}

@objc private func toggleCollapseAccessibilityAction() -> Bool {
didCollapseHandler?(self)
delegate?.toggle(cell: self)
return true
}

Expand Down Expand Up @@ -255,3 +263,12 @@ class LocationCell: UITableViewCell {
}
}
}

extension LocationCell {
func configureCell(item: LocationCellViewModel) {
accessibilityIdentifier = item.node.code
locationLabel.text = item.node.name
showsCollapseControl = !item.node.children.isEmpty
isExpanded = item.node.showsChildren
}
}
Loading
Loading