Skip to content

Commit

Permalink
Implement SwiftUI Shadowsocks settings view with custom field editing
Browse files Browse the repository at this point in the history
  • Loading branch information
acb-mv authored and buggmagnet committed Nov 28, 2024
1 parent a004064 commit 6007b2b
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 38 deletions.
8 changes: 8 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
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 */; };
447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */; };
447F3D8B2CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.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 @@ -1397,6 +1399,8 @@
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>"; };
447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsView.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 @@ -2602,6 +2606,8 @@
4422C06F2CCFF6520001A385 /* Obfuscation */ = {
isa = PBXGroup;
children = (
447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift */,
447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */,
440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */,
4422C0702CCFF6790001A385 /* UDPTCPObfuscationSettingsView.swift */,
44075DFA2CDA4F7400F61139 /* UDPTCPObfuscationSettingsViewModel.swift */,
Expand Down Expand Up @@ -5747,6 +5753,7 @@
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
7A9CCCB82A96302800DD6A34 /* SetupAccountCompletedCoordinator.swift in Sources */,
447F3D8B2CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
7A0B311E2B303A0D004B12E0 /* AccessbilityIdentifier.swift in Sources */,
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */,
Expand Down Expand Up @@ -5889,6 +5896,7 @@
F02F41A22B9723AF00625A4F /* AddLocationsCoordinator.swift in Sources */,
7A27E3CD2CB814EF0088BCFF /* DAITAInfoView.swift in Sources */,
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */,
447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */,
7A5869C52B5A899C00640D27 /* MethodSettingsCellConfiguration.swift in Sources */,
58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */,
58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */,
Expand Down
9 changes: 6 additions & 3 deletions ios/MullvadVPN/UI appearance/UIColor+Palette.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@ extension UIColor {

enum TextField {
static let placeholderTextColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 0.40)
static let inactivePlaceholderTextColor = UIColor(white: 1.0, alpha: 0.4)
static let textColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 1.0)
static let inactiveTextColor = UIColor.white
static let backgroundColor = UIColor.white
static let inactiveBackgroundColor = UIColor(white: 1.0, alpha: 0.1)
static let invalidInputTextColor = UIColor.dangerColor
}

enum SearchTextField {
static let placeholderTextColor = TextField.placeholderTextColor
static let inactivePlaceholderTextColor = UIColor(white: 1.0, alpha: 0.4)
static let inactivePlaceholderTextColor = TextField.inactivePlaceholderTextColor
static let textColor = TextField.textColor
static let inactiveTextColor = UIColor.white
static let inactiveTextColor = TextField.inactiveTextColor
static let backgroundColor = TextField.backgroundColor
static let inactiveBackgroundColor = UIColor(white: 1.0, alpha: 0.1)
static let inactiveBackgroundColor = TextField.inactiveBackgroundColor
static let leftViewTintColor = UIColor.primaryColor
static let inactiveLeftViewTintColor = UIColor.white
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// ShadowsocksObfuscationSettingsView.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-11-07.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import MullvadSettings
import SwiftUI

struct ShadowsocksObfuscationSettingsView<VM>: View where VM: ShadowsocksObfuscationSettingsViewModel {
@StateObject var viewModel: VM

var body: some View {
let portString = NSLocalizedString(
"SHADOWSOCKS_PORT_LABEL",
tableName: "Shadowsocks",
value: "Port",
comment: ""
)

SingleChoiceList(
title: portString,
options: [WireGuardObfuscationShadowsockPort.automatic],
value: $viewModel.value,
itemDescription: { item in NSLocalizedString(
"SHADOWSOCKS_PORT_VALUE_\(item)",
tableName: "Shadowsocks",
value: "\(item)",
comment: ""
) },
parseCustomValue: { UInt16($0).flatMap { $0 > 0 ? WireGuardObfuscationShadowsockPort.custom($0) : nil }
},
formatCustomValue: {
if case let .custom(port) = $0 {
"\(port)"
} else {
nil
}
},
customLabel: NSLocalizedString(
"SHADOWSOCKS_PORT_VALUE_CUSTOM",
tableName: "Shadowsocks",
value: "Custom",
comment: ""
),
customPrompt: NSLocalizedString(
"SHADOWSOCKS_PORT_VALUE_PORT_PROMPT",
tableName: "Shadowsocks",
value: "Port",
comment: ""
),
customLegend: NSLocalizedString(
"SHADOWSOCKS_PORT_VALUE_PORT_LEGEND",
tableName: "Shadowsocks",
value: "Valid range: 1 - 65535",
comment: ""
),
customInputMinWidth: 100,
customInputMaxLength: 5,
customFieldMode: .numericText
).onDisappear {
viewModel.commit()
}
}
}

#Preview {
var model = MockShadowsocksObfuscationSettingsViewModel(shadowsocksPort: .automatic)
return ShadowsocksObfuscationSettingsView(viewModel: model)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// ShadowsocksObfuscationSettingsViewModel.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-11-07.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadSettings

protocol ShadowsocksObfuscationSettingsViewModel: ObservableObject {
var value: WireGuardObfuscationShadowsockPort { get set }

func commit()
}

/** A simple mock view model for use in Previews and similar */
class MockShadowsocksObfuscationSettingsViewModel: ShadowsocksObfuscationSettingsViewModel {
@Published var value: WireGuardObfuscationShadowsockPort

init(shadowsocksPort: WireGuardObfuscationShadowsockPort = .automatic) {
self.value = shadowsocksPort
}

func commit() {}
}

/// ** The live view model which interfaces with the TunnelManager */
class TunnelShadowsocksObfuscationSettingsViewModel: TunnelObfuscationSettingsWatchingObservableObject<
WireGuardObfuscationShadowsockPort
>,
ShadowsocksObfuscationSettingsViewModel {
init(tunnelManager: TunnelManager) {
super.init(
tunnelManager: tunnelManager,
keyPath: \.shadowsocksPort
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,12 @@ class TunnelObfuscationSettingsWatchingObservableObject<T: Equatable>: Observabl
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
tunnelManager.updateSettings([.obfuscation(obfuscationSettings)])
}
}
@Published var value: T

init(tunnelManager: TunnelManager, keyPath: WritableKeyPath<WireGuardObfuscationSettings, T>, _ initialValue: T) {
init(tunnelManager: TunnelManager, keyPath: WritableKeyPath<WireGuardObfuscationSettings, T>) {
self.tunnelManager = tunnelManager
self.keyPath = keyPath
self.value = initialValue
self.value = tunnelManager.settings.wireGuardObfuscation[keyPath: keyPath]
tunnelObserver =
TunnelBlockObserver(didUpdateTunnelSettings: { [weak self] _, newSettings in
guard let self else { return }
Expand All @@ -45,4 +36,11 @@ class TunnelObfuscationSettingsWatchingObservableObject<T: Equatable>: Observabl
value = newValue
}
}

// Commit the temporarily stored value upstream
func commit() {
var obfuscationSettings = tunnelManager.settings.wireGuardObfuscation
obfuscationSettings[keyPath: keyPath] = value
tunnelManager.updateSettings([.obfuscation(obfuscationSettings)])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ struct UDPTCPObfuscationSettingsView<VM>: View where VM: UDPTCPObfuscationSettin
value: "\(item)",
comment: ""
) }
)
).onDisappear {
viewModel.commit()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import MullvadSettings

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

func commit()
}

/** A simple mock view model for use in Previews and similar */
Expand All @@ -20,6 +22,8 @@ class MockUDPTCPObfuscationSettingsViewModel: UDPTCPObfuscationSettingsViewModel
init(udpTcpPort: WireGuardObfuscationUdpOverTcpPort = .automatic) {
self.value = udpTcpPort
}

func commit() {}
}

/** The live view model which interfaces with the TunnelManager */
Expand All @@ -30,8 +34,7 @@ class TunnelUDPTCPObfuscationSettingsViewModel: TunnelObfuscationSettingsWatchin
init(tunnelManager: TunnelManager) {
super.init(
tunnelManager: tunnelManager,
keyPath: \.udpOverTcpPort,
tunnelManager.settings.wireGuardObfuscation.udpOverTcpPort
keyPath: \.udpOverTcpPort
)
}
}
Loading

0 comments on commit 6007b2b

Please sign in to comment.