Skip to content

Commit

Permalink
Add feature chip view
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii committed Dec 6, 2024
1 parent 3ee5ce0 commit 723d71a
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 0 deletions.
4 changes: 4 additions & 0 deletions ios/MullvadSettings/WireGuardObfuscationSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public enum WireGuardObfuscationState: Codable {
self = .off
}
}

public var isEnabled: Bool {
self != .off
}
}

public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomStringConvertible {
Expand Down
44 changes: 44 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,14 @@
F0ADC3722CD3AD1600A1AD97 /* ChipCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADC3712CD3AD1600A1AD97 /* ChipCollectionView.swift */; };
F0ADC3742CD3C47400A1AD97 /* ChipFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADC3732CD3C47400A1AD97 /* ChipFlowLayout.swift */; };
F0ADF1CD2CFDFF3100299F09 /* StringConversionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */; };
F0ADF1D12D01B55C00299F09 /* ChipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1D02D01B55C00299F09 /* ChipModel.swift */; };
F0ADF1D32D01B6B400299F09 /* FeatureChipViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1D22D01B6B400299F09 /* FeatureChipViewModel.swift */; };
F0ADF1D52D01DCFD00299F09 /* ChipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1D42D01DCFD00299F09 /* ChipView.swift */; };
F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */; };
F0B495762D02025200CFEC2A /* ChipContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495752D02025200CFEC2A /* ChipContainerView.swift */; };
F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */; };
F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */; };
F0B4957C2D03154200CFEC2A /* FeaturesIndicatoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B4957B2D03154200CFEC2A /* FeaturesIndicatoresView.swift */; };
F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */; };
F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F02BF751E300817A42 /* RelayWithDistance.swift */; };
F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */; };
Expand Down Expand Up @@ -2212,7 +2219,14 @@
F0ADC3712CD3AD1600A1AD97 /* ChipCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipCollectionView.swift; sourceTree = "<group>"; };
F0ADC3732CD3C47400A1AD97 /* ChipFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFlowLayout.swift; sourceTree = "<group>"; };
F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringConversionError.swift; sourceTree = "<group>"; };
F0ADF1D02D01B55C00299F09 /* ChipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipModel.swift; sourceTree = "<group>"; };
F0ADF1D22D01B6B400299F09 /* FeatureChipViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureChipViewModel.swift; sourceTree = "<group>"; };
F0ADF1D42D01DCFD00299F09 /* ChipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipView.swift; sourceTree = "<group>"; };
F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = "<group>"; };
F0B495752D02025200CFEC2A /* ChipContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipContainerView.swift; sourceTree = "<group>"; };
F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewModelProtocol.swift; sourceTree = "<group>"; };
F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeatures.swift; sourceTree = "<group>"; };
F0B4957B2D03154200CFEC2A /* FeaturesIndicatoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturesIndicatoresView.swift; sourceTree = "<group>"; };
F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = "<group>"; };
F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = "<group>"; };
F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Wireguard.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2982,6 +2996,7 @@
583FE01E29C197D5006E85F9 /* Tunnel */ = {
isa = PBXGroup;
children = (
F0ADF1CE2D01B4F300299F09 /* FeaturesIndicator */,
58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */,
5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */,
58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */,
Expand Down Expand Up @@ -4331,6 +4346,28 @@
path = MullvadTypes;
sourceTree = "<group>";
};
F0ADF1CE2D01B4F300299F09 /* FeaturesIndicator */ = {
isa = PBXGroup;
children = (
F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */,
F0ADF1CF2D01B50B00299F09 /* ChipsView */,
F0ADF1D22D01B6B400299F09 /* FeatureChipViewModel.swift */,
F0B4957B2D03154200CFEC2A /* FeaturesIndicatoresView.swift */,
);
path = FeaturesIndicator;
sourceTree = "<group>";
};
F0ADF1CF2D01B50B00299F09 /* ChipsView */ = {
isa = PBXGroup;
children = (
F0B495752D02025200CFEC2A /* ChipContainerView.swift */,
F0ADF1D02D01B55C00299F09 /* ChipModel.swift */,
F0ADF1D42D01DCFD00299F09 /* ChipView.swift */,
F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */,
);
path = ChipsView;
sourceTree = "<group>";
};
F0DC779F2B2222D20087F09D /* Relay */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5850,6 +5887,7 @@
5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */,
7AF9BE882A30C62100DBFEDB /* SelectableSettingsCell.swift in Sources */,
58CCA010224249A1004F3011 /* TunnelViewController.swift in Sources */,
F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */,
58CEB30A2AFD584700E6E088 /* CustomCellDisclosureHandling.swift in Sources */,
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
Expand Down Expand Up @@ -5923,6 +5961,7 @@
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */,
7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */,
F0B495762D02025200CFEC2A /* ChipContainerView.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
58CEB3022AFD365600E6E088 /* SwitchCellContentConfiguration.swift in Sources */,
7A9CCCB52A96302800DD6A34 /* AddCreditSucceededCoordinator.swift in Sources */,
Expand Down Expand Up @@ -6027,6 +6066,7 @@
588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */,
5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */,
588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */,
F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */,
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */,
7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */,
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
Expand Down Expand Up @@ -6062,8 +6102,10 @@
586C0D782B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift in Sources */,
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
7A9CCCC02A96302800DD6A34 /* ProfileVoucherCoordinator.swift in Sources */,
F0B4957C2D03154200CFEC2A /* FeaturesIndicatoresView.swift in Sources */,
7A9CCCBC2A96302800DD6A34 /* ChangeLogCoordinator.swift in Sources */,
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
F0ADF1D52D01DCFD00299F09 /* ChipView.swift in Sources */,
586C0D932B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift in Sources */,
58EFC7712AFB45E500E9F4CB /* SettingsChildCoordinator.swift in Sources */,
7A8A19102CEE391B000BCB5B /* RowSeparator.swift in Sources */,
Expand Down Expand Up @@ -6106,6 +6148,7 @@
7A9CCCC22A96302800DD6A34 /* SafariCoordinator.swift in Sources */,
58CEB3082AFD484100E6E088 /* BasicCell.swift in Sources */,
7A5869C12B57D21A00640D27 /* IPOverrideStatusView.swift in Sources */,
F0ADF1D32D01B6B400299F09 /* FeatureChipViewModel.swift in Sources */,
58CEB2F52AFD0BB500E6E088 /* TextCellContentConfiguration.swift in Sources */,
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */,
Expand All @@ -6125,6 +6168,7 @@
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */,
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
F0ADF1D12D01B55C00299F09 /* ChipModel.swift in Sources */,
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
58CEB2FB2AFD13E600E6E088 /* UIListContentConfiguration+Extensions.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// ChipFeatures.swift
// MullvadVPN
//
// Created by Mojgan on 2024-12-06.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadSettings
import SwiftUICore

protocol ChipFeature {
var isEnabled: Bool { get }
func chipName() -> LocalizedStringKey
}

struct DaitaFeature: ChipFeature {
let settings: LatestTunnelSettings

var isEnabled: Bool {
settings.daita.daitaState.isEnabled
}

func chipName() -> LocalizedStringKey {
LocalizedStringKey("DAITA")
}
}

struct QuantumResistanceFeature: ChipFeature {
let settings: LatestTunnelSettings
var isEnabled: Bool {
settings.tunnelQuantumResistance.isEnabled
}

func chipName() -> LocalizedStringKey {
LocalizedStringKey("Quantum resistance")
}
}

struct MultihopFeature: ChipFeature {
let settings: LatestTunnelSettings
var isEnabled: Bool {
settings.tunnelMultihopState.isEnabled
}

func chipName() -> LocalizedStringKey {
LocalizedStringKey("Multihop")
}
}

struct ObfuscationFeature: ChipFeature {
let settings: LatestTunnelSettings

var isEnabled: Bool {
settings.wireGuardObfuscation.state.isEnabled
}

func chipName() -> LocalizedStringKey {
LocalizedStringKey("Obfuscation")
}
}

struct DNSFeature: ChipFeature {
let settings: LatestTunnelSettings

var isEnabled: Bool {
settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty
}

func chipName() -> LocalizedStringKey {
if !settings.dnsSettings.blockingOptions.isEmpty {
return LocalizedStringKey("DNS content blockers")
}
return LocalizedStringKey("Custom DNS")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// ChipContainerView.swift
// MullvadVPN
//
// Created by Mojgan on 2024-12-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import SwiftUI

struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
@ObservedObject var viewModel: ViewModel

init(viewModel: ViewModel) {
self.viewModel = viewModel
}

var body: some View {
GeometryReader { geo in
let containerWidth = geo.size.width
ZStack(alignment: .topLeading) {
var width = CGFloat.zero
var height = CGFloat.zero

ForEach(viewModel.chips) { data in
ChipView(item: data)
.padding(5)
.alignmentGuide(.leading) { dimension in
if abs(width - dimension.width) > containerWidth {
width = 0
height -= dimension.height
}
let result = width
if data.id == viewModel.chips.last!.id {
width = 0
} else {
width -= dimension.width
}
return result
}
.alignmentGuide(.top) { _ in
let result = height
if data.id == viewModel.chips.last!.id {
height = 0
}
return result
}
}
}
}
}
}

#Preview("ChipContainerView") {
ChipContainerView(viewModel: MockChipViewModel())
}

private class MockChipViewModel: ChipViewModelProtocol {
@Published var chips: [ChipModel] = (5 ..< 20).map { index in
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return ChipModel(name: LocalizedStringKey(String((0 ..< index).map { _ in letters.randomElement()! })))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// FeatureChipModel.swift
// MullvadVPN
//
// Created by Mojgan on 2024-12-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import SwiftUICore

struct ChipModel: Identifiable {
let id = UUID()
let name: LocalizedStringKey
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// FeatureChipView.swift
// MullvadVPN
//
// Created by Mojgan on 2024-12-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import SwiftUI

struct ChipView: View {
let item: ChipModel
var body: some View {
HStack(spacing: UIMetrics.padding4) {
Text(item.name)
.font(.body)
.lineLimit(1)
.foregroundStyle(Color(uiColor: .primaryTextColor))
}
.padding(.horizontal, UIMetrics.padding8)
.padding(.vertical, UIMetrics.padding4)
.background(Color(uiColor: .secondaryColor))
.foregroundStyle(Color(uiColor: .primaryColor))
.overlay {
RoundedRectangle(cornerRadius: UIMetrics.controlCornerRadius)
.stroke(Color(uiColor: .primaryColor), style: StrokeStyle(lineWidth: 1.0))
}
}
}

#Preview {
ChipView(item: ChipModel(name: LocalizedStringKey("Example")))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// ChipViewModelProtocol.swift
// MullvadVPN
//
// Created by Mojgan on 2024-12-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
protocol ChipViewModelProtocol: ObservableObject {
var chips: [ChipModel] { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// FeatureChipViewModel.swift
// MullvadVPN
//
// Created by Mojgan on 2024-12-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadSettings
import SwiftUICore
class FeatureChipViewModel: ChipViewModelProtocol {
@Published var chips: [ChipModel] = []

let tunnelManager: TunnelManager
var observer: TunnelObserver?

init(tunnelManager: TunnelManager) {
self.tunnelManager = tunnelManager
let observer = TunnelBlockObserver(
didLoadConfiguration: { [weak self] tunnelManager in
guard let self else { return }
chips = createChips(tunnelManager.settings)
},
didUpdateTunnelSettings: { [weak self] _, latestTunnelSettings in
guard let self else { return }
chips = createChips(latestTunnelSettings)
}
)
self.observer = observer
tunnelManager.addObserver(observer)
}

private func createChips(_ latestTunnelSettings: LatestTunnelSettings) -> [ChipModel] {
let features: [ChipFeature] = [
DaitaFeature(settings: latestTunnelSettings),
QuantumResistanceFeature(settings: latestTunnelSettings),
MultihopFeature(settings: latestTunnelSettings),
ObfuscationFeature(settings: latestTunnelSettings),
DNSFeature(settings: latestTunnelSettings),
]

return features
.filter { $0.isEnabled }
.map { ChipModel(name: $0.chipName()) }
}
}
Loading

0 comments on commit 723d71a

Please sign in to comment.