Skip to content

Commit

Permalink
Implement SwiftUI UI for UDP TCP Obfuscation port selector view
Browse files Browse the repository at this point in the history
  • Loading branch information
acb-mv committed Nov 8, 2024
1 parent e914beb commit e5a8213
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 37 deletions.
6 changes: 3 additions & 3 deletions ios/MullvadSettings/WireGuardObfuscationSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ public struct WireGuardObfuscationSettings: Codable, Equatable {
@available(*, deprecated, message: "Use `udpOverTcpPort` instead")
private var port: WireGuardObfuscationPort = .automatic

public let state: WireGuardObfuscationState
public let udpOverTcpPort: WireGuardObfuscationUdpOverTcpPort
public let shadowsocksPort: WireGuardObfuscationShadowsockPort
public var state: WireGuardObfuscationState
public var udpOverTcpPort: WireGuardObfuscationUdpOverTcpPort
public var shadowsocksPort: WireGuardObfuscationShadowsockPort

public init(
state: WireGuardObfuscationState = .automatic,
Expand Down
36 changes: 36 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114128F8413A0037AF9A /* AddressCache.swift */; };
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; };
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
44075DFB2CDA4F7400F61139 /* UDPTCPObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44075DFA2CDA4F7400F61139 /* UDPTCPObfuscationSettingsViewModel.swift */; };
440E5AB02CDBD67D00B09614 /* StatefulPreviewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */; };
440E5AB42CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */; };
4422C0712CCFF6790001A385 /* UDPTCPObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4422C0702CCFF6790001A385 /* UDPTCPObfuscationSettingsView.swift */; };
4424CDD32CDBD4A6009D8C9F /* SingleChoiceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */; };
449275422C3570CA000526DE /* ICMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449275412C3570CA000526DE /* ICMP.swift */; };
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
Expand Down Expand Up @@ -1385,6 +1390,11 @@
06FAE67A28F83CA50033DD93 /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; };
06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; };
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
44075DFA2CDA4F7400F61139 /* UDPTCPObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPTCPObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPreviewWrapper.swift; sourceTree = "<group>"; };
440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationSettingsWatchingObservableObject.swift; sourceTree = "<group>"; };
4422C0702CCFF6790001A385 /* UDPTCPObfuscationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPTCPObfuscationSettingsView.swift; sourceTree = "<group>"; };
4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleChoiceList.swift; sourceTree = "<group>"; };
449275412C3570CA000526DE /* ICMP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMP.swift; sourceTree = "<group>"; };
449275432C3C3029000526DE /* TunnelPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelPinger.swift; sourceTree = "<group>"; };
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2581,6 +2591,25 @@
path = Protocols;
sourceTree = "<group>";
};
4422C06F2CCFF6520001A385 /* Obfuscation */ = {
isa = PBXGroup;
children = (
4422C0702CCFF6790001A385 /* UDPTCPObfuscationSettingsView.swift */,
44075DFA2CDA4F7400F61139 /* UDPTCPObfuscationSettingsViewModel.swift */,
440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */,
);
path = Obfuscation;
sourceTree = "<group>";
};
4424CDD12CDBD457009D8C9F /* SwiftUI components */ = {
isa = PBXGroup;
children = (
440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */,
4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */,
);
path = "SwiftUI components";
sourceTree = "<group>";
};
449872E22B7CB91B00094DDC /* MullvadSettings */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2809,6 +2838,8 @@
583FE01829C19709006E85F9 /* Settings */ = {
isa = PBXGroup;
children = (
4424CDD12CDBD457009D8C9F /* SwiftUI components */,
4422C06F2CCFF6520001A385 /* Obfuscation */,
7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */,
F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */,
7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */,
Expand Down Expand Up @@ -5607,7 +5638,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
44075DFB2CDA4F7400F61139 /* UDPTCPObfuscationSettingsViewModel.swift in Sources */,
7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */,
4422C0712CCFF6790001A385 /* UDPTCPObfuscationSettingsView.swift in Sources */,
7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */,
5827B0A42B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift in Sources */,
586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */,
Expand Down Expand Up @@ -5679,6 +5712,7 @@
58DFF7D22B0256A300F864E0 /* MarkdownStylingOptions.swift in Sources */,
5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */,
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */,
4424CDD32CDBD4A6009D8C9F /* SingleChoiceList.swift in Sources */,
5827B0BD2B14AC9200CCBBA1 /* AccessViewModel+TestingStatus.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
58EFC7752AFB4CEF00E9F4CB /* AboutViewController.swift in Sources */,
Expand Down Expand Up @@ -5722,6 +5756,7 @@
7A5869C72B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift in Sources */,
58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
440E5AB02CDBD67D00B09614 /* StatefulPreviewWrapper.swift in Sources */,
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */,
7A9CCCC12A96302800DD6A34 /* AccountCoordinator.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
Expand Down Expand Up @@ -5852,6 +5887,7 @@
58CEB2FD2AFD19D300E6E088 /* UITableView+ReuseIdentifier.swift in Sources */,
F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
440E5AB42CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift in Sources */,
588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */,
5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */,
588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ public enum AccessibilityIdentifier: String {
case wireGuardObfuscationUdpOverTcp
case wireGuardObfuscationShadowsocks
case wireGuardPort
case udpTcpObfuscationSettings

// Custom DNS
case blockAll
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// TunnelObfuscationSettingsWatchingObservableObject.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-11-07.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadSettings

/// a generic ObservableObject that binds to obfuscation settings in TunnelManager.
/// Used as the basis for ViewModels for SwiftUI interfaces for these settings.

class TunnelObfuscationSettingsWatchingObservableObject<T: Equatable>: ObservableObject {
let tunnelManager: TunnelManager
let keyPath: WritableKeyPath<WireGuardObfuscationSettings, T>
private var tunnelObserver: TunnelObserver?

// this is essentially @Published from scratch
var value: T {
willSet(newValue) {
guard newValue != self.value else { return }
objectWillChange.send()
var obfuscationSettings = tunnelManager.settings.wireGuardObfuscation
obfuscationSettings[keyPath: keyPath] = newValue
}
}

init(tunnelManager: TunnelManager, keyPath: WritableKeyPath<WireGuardObfuscationSettings, T>, _ initialValue: T) {
self.tunnelManager = tunnelManager
self.keyPath = keyPath
self.value = initialValue
tunnelObserver =
TunnelBlockObserver(didUpdateTunnelSettings: { [weak self] _, newSettings in
guard let self else { return }
updateValueFromSettings(newSettings.wireGuardObfuscation)
})
}

private func updateValueFromSettings(_ settings: WireGuardObfuscationSettings) {
let newValue = settings
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// UDPTCPObfuscationSettingsView.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-10-28.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import MullvadSettings
import SwiftUI

struct UDPTCPObfuscationSettingsView<VM>: View where VM: UDPTCPObfuscationSettingsViewModel {
@StateObject var viewModel: VM

var body: some View {
SingleChoiceList(
title: "Port",
options: [WireGuardObfuscationUdpOverTcpPort.automatic, .port80, .port5001],
value: $viewModel.value
)
}
}

#Preview {
var model = MockUDPTCPObfuscationSettingsViewModel(udpTcpPort: .port5001)
return UDPTCPObfuscationSettingsView(viewModel: model)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// UDPTCPObfuscationSettingsViewModel.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-11-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadSettings

protocol UDPTCPObfuscationSettingsViewModel: ObservableObject {
var value: WireGuardObfuscationUdpOverTcpPort { get set }
}

/** A simple mock view model for use in Previews and similar */
class MockUDPTCPObfuscationSettingsViewModel: UDPTCPObfuscationSettingsViewModel {
@Published var value: WireGuardObfuscationUdpOverTcpPort

init(udpTcpPort: WireGuardObfuscationUdpOverTcpPort = .automatic) {
self.value = udpTcpPort
}
}

/** The live view model which interfaces with the TunnelManager */
class TunnelUDPTCPObfuscationSettingsViewModel: TunnelObfuscationSettingsWatchingObservableObject<
WireGuardObfuscationUdpOverTcpPort
>,
UDPTCPObfuscationSettingsViewModel {
init(tunnelManager: TunnelManager) {
super.init(
tunnelManager: tunnelManager,
keyPath: \.udpOverTcpPort,
.automatic
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// SingleChoiceList.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-11-06.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import SwiftUI

/**
A component presenting a vertical list in the Mullvad style for selecting a single item from a list.
The items can be any Hashable type.
*/

struct SingleChoiceList<T>: View where T: Hashable {
let title: String
let options: [T]
var value: Binding<T>

func row(_ v: T) -> some View {
let isSelected = value.wrappedValue == v
return HStack {
Image("IconTick").opacity(isSelected ? 1.0 : 0.0)
Text(verbatim: "\(v)")
Spacer()
}
.padding(16)
.background(isSelected ? Color(UIColor.Cell.Background.selected) : Color(UIColor.Cell.Background.normal))
.foregroundColor(Color(UIColor.Cell.titleTextColor))
.onTapGesture {
value.wrappedValue = v
}
}

var body: some View {
VStack(spacing: 0) {
HStack {
Text(title)
Spacer()
}
.padding(16)
ForEach(options, id: \.self) { opt in
row(opt)
}
Spacer()
}
.background(Color(.secondaryColor))
.foregroundColor(Color(.primaryTextColor))
}
}

#Preview {
StatefulPreviewWrapper(1) { SingleChoiceList(title: "Test", options: [1, 2, 3], value: $0) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// StatefulPreviewWrapper.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-11-06.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

// This should probably live somewhere more central than `View controllers/Settings/SwiftUI components`. Where exactly is to be determined.

import SwiftUI

/** A wrapper for providing a state binding for SwiftUI Views in #Preview. This takes as arguments an initial value for the binding and a block which accepts the binding and returns a View to be previewed
The usage looks like:

```
#Preview {
StatefulPreviewWrapper(initvalue) { ComponentToBePreviewed(binding: $0) }
}
```
*/

struct StatefulPreviewWrapper<Value, Content: View>: View {
@State var value: Value
var content: (Binding<Value>) -> Content

var body: some View {
content($value)
}

init(_ value: Value, content: @escaping (Binding<Value>) -> Content) {
self._value = State(wrappedValue: value)
self.content = content
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ final class VPNSettingsCellFactory: CellFactoryProtocol {
cell.disclosureType = .chevron
cell.accessibilityIdentifier = item.accessibilityIdentifier

case .udpTcpObfuscationSettings:
guard let cell = cell as? SettingsCell else { return }

cell.titleLabel.text = NSLocalizedString(
"UDP_TCP_OBFUSCATION_CELL_LABEL",
tableName: "VPNSettings",
value: "UDP/TCP Obfuscation",
comment: ""
)

cell.disclosureType = .chevron
cell.accessibilityIdentifier = item.accessibilityIdentifier

case let .wireGuardPort(port):
guard let cell = cell as? SelectableSettingsCell else { return }

Expand Down
Loading

0 comments on commit e5a8213

Please sign in to comment.