summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-08-08 15:10:58 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-08-08 15:10:58 +0200
commitb2804eb48c5ad586cd5840798282245882834142 (patch)
tree483ecba253f7731412919e4257f40a557964b6ce
parent7e332f5a1a9b67b7fc5f1dfce6a7e0f804a541f5 (diff)
parent73a575b2411ddc9f3ff85360c576b28cf46687f4 (diff)
downloadmullvadvpn-b2804eb48c5ad586cd5840798282245882834142.tar.xz
mullvadvpn-b2804eb48c5ad586cd5840798282245882834142.zip
Merge branch 'implement-first-slice-of-standardized-text-fields-ios-1121'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj8
-rw-r--r--ios/MullvadVPN/Extensions/Image+Assets.swift2
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/Contents.json12
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/IconCross.svg8
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/Contents.json12
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/IconSearch.svg8
-rw-r--r--ios/MullvadVPN/UI appearance/Color+Mullvad.swift73
-rw-r--r--ios/MullvadVPN/Views/MullvadPrimaryTextField.swift295
-rw-r--r--ios/MullvadVPN/Views/MullvadSecondaryTextField.swift86
9 files changed, 502 insertions, 2 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 35ebd1315f..dcc97b34e1 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -1109,6 +1109,8 @@
F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; };
F0FADDEC2BE90AB0000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; };
F90A988A2E042D040020F64F /* ClearBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90A98892E042D040020F64F /* ClearBackgroundView.swift */; };
+ F90A988C2E1268570020F64F /* MullvadPrimaryTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90A988B2E1268510020F64F /* MullvadPrimaryTextField.swift */; };
+ F90A988E2E13C5490020F64F /* MullvadSecondaryTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90A988D2E13C5490020F64F /* MullvadSecondaryTextField.swift */; };
F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4002D3FF22E002FF3BB /* View+Modifier.swift */; };
F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */; };
F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */; };
@@ -2542,6 +2544,8 @@
F0FA160F2D7F2FC0007E2546 /* RelayFilterViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewModelTests.swift; sourceTree = "<group>"; };
F0FBD98E2C4A60CC00EE5323 /* KeyExchangingResultStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyExchangingResultStub.swift; sourceTree = "<group>"; };
F90A98892E042D040020F64F /* ClearBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearBackgroundView.swift; sourceTree = "<group>"; };
+ F90A988B2E1268510020F64F /* MullvadPrimaryTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadPrimaryTextField.swift; sourceTree = "<group>"; };
+ F90A988D2E13C5490020F64F /* MullvadSecondaryTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadSecondaryTextField.swift; sourceTree = "<group>"; };
F910A4002D3FF22E002FF3BB /* View+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Modifier.swift"; sourceTree = "<group>"; };
F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; };
F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseViewController.swift; sourceTree = "<group>"; };
@@ -3339,6 +3343,8 @@
583FE01F29C197ED006E85F9 /* Views */ = {
isa = PBXGroup;
children = (
+ F90A988B2E1268510020F64F /* MullvadPrimaryTextField.swift */,
+ F90A988D2E13C5490020F64F /* MullvadSecondaryTextField.swift */,
7A5869962B32EA4500640D27 /* AppButton.swift */,
7A0EAEA12D033D5A00D3EB8B /* BlurView.swift */,
7A9FA1412A2E3306000B728D /* CheckboxView.swift */,
@@ -6413,6 +6419,7 @@
5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */,
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */,
4424CDD32CDBD4A6009D8C9F /* SingleChoiceList.swift in Sources */,
+ F90A988E2E13C5490020F64F /* MullvadSecondaryTextField.swift in Sources */,
5827B0BD2B14AC9200CCBBA1 /* AccessViewModel+TestingStatus.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
58EFC7752AFB4CEF00E9F4CB /* AboutViewController.swift in Sources */,
@@ -6583,6 +6590,7 @@
58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */,
5877F94E2A0A59AA0052D9E9 /* NotificationResponse.swift in Sources */,
7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */,
+ F90A988C2E1268570020F64F /* MullvadPrimaryTextField.swift in Sources */,
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
diff --git a/ios/MullvadVPN/Extensions/Image+Assets.swift b/ios/MullvadVPN/Extensions/Image+Assets.swift
index 0f61db0ed4..3bac2e8adf 100644
--- a/ios/MullvadVPN/Extensions/Image+Assets.swift
+++ b/ios/MullvadVPN/Extensions/Image+Assets.swift
@@ -10,4 +10,6 @@ extension Image {
static let mullvadIconSpinner = Image("IconSpinner")
static let mullvadIconSuccess = Image("IconSuccess")
static let mullvadIconFail = Image("IconFail")
+ static let mullvadIconSearch = Image("IconSearch")
+ static let mullvadIconCross = Image("IconCross")
}
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/Contents.json
new file mode 100644
index 0000000000..32c3266417
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "IconCross.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/IconCross.svg b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/IconCross.svg
new file mode 100644
index 0000000000..2368864472
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCross.imageset/IconCross.svg
@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<mask id="mask0_8042_9375" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
+<rect width="24" height="24" fill="#D9D9D9"/>
+</mask>
+<g mask="url(#mask0_8042_9375)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.70711 6.29289C7.31658 5.90237 6.68342 5.90237 6.29289 6.29289C5.90237 6.68342 5.90237 7.31658 6.29289 7.70711L10.5858 12L6.29289 16.2929C5.90237 16.6834 5.90237 17.3166 6.29289 17.7071C6.68342 18.0976 7.31658 18.0976 7.70711 17.7071L12 13.4142L16.2929 17.7071C16.6834 18.0976 17.3166 18.0976 17.7071 17.7071C18.0976 17.3166 18.0976 16.6834 17.7071 16.2929L13.4142 12L17.7071 7.70711C18.0976 7.31658 18.0976 6.68342 17.7071 6.29289C17.3166 5.90237 16.6834 5.90237 16.2929 6.29289L12 10.5858L7.70711 6.29289Z" fill="white" fill-opacity="0.6"/>
+</g>
+</svg>
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/Contents.json
new file mode 100644
index 0000000000..1a03ba590b
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "IconSearch.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/IconSearch.svg b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/IconSearch.svg
new file mode 100644
index 0000000000..7600bf0944
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSearch.imageset/IconSearch.svg
@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<mask id="mask0_8042_4889" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
+<rect width="24" height="24" fill="#D9D9D9"/>
+</mask>
+<g mask="url(#mask0_8042_4889)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7 10C7 7.79086 8.79086 6 11 6C13.2091 6 15 7.79086 15 10C15 11.2998 14.38 12.4548 13.4196 13.1855C13.4066 13.1947 13.3938 13.2044 13.3812 13.2143C12.7159 13.708 11.8921 14 11 14C8.79086 14 7 12.2091 7 10ZM13.861 15.2752C13.0106 15.7374 12.036 16 11 16C7.68629 16 5 13.3137 5 10C5 6.68629 7.68629 4 11 4C14.3137 4 17 6.68629 17 10C17 11.5515 16.4111 12.9654 15.4447 14.0305L19.7071 18.2929C20.0976 18.6834 20.0976 19.3166 19.7071 19.7071C19.3166 20.0976 18.6834 20.0976 18.2929 19.7071L13.861 15.2752Z" fill="white" fill-opacity="0.6"/>
+</g>
+</svg>
diff --git a/ios/MullvadVPN/UI appearance/Color+Mullvad.swift b/ios/MullvadVPN/UI appearance/Color+Mullvad.swift
index f4c85dd81f..a267cb4cb7 100644
--- a/ios/MullvadVPN/UI appearance/Color+Mullvad.swift
+++ b/ios/MullvadVPN/UI appearance/Color+Mullvad.swift
@@ -1,8 +1,8 @@
import SwiftUI
extension Color {
- private static let mullvadPrimaryColor = UIColor.primaryColor.color
- private static let mullvadSecondaryColor = UIColor.secondaryColor.color
+ private static let mullvadPrimaryColor = MullvadBlue.base
+ private static let mullvadSecondaryColor = MullvadDarkBlue.base
private static let mullvadWarningColor = UIColor.warningColor.color
private static let mullvadDangerColor = UIColor.dangerColor.color
private static let mullvadSuccessColor = UIColor.successColor.color
@@ -14,6 +14,50 @@ extension Color {
)
static let secondaryTextColor: Color = UIColor.secondaryTextColor.color
+ private enum MullvadBlue {
+ static let base: Color = .init(red: 0.16, green: 0.3, blue: 0.45)
+ static let _10: Color = .init(red: 0.11, green: 0.19, blue: 0.29)
+ static let _20: Color = .init(red: 0.11, green: 0.2, blue: 0.31)
+ static let _40: Color = .init(red: 0.12, green: 0.23, blue: 0.34)
+ static let _50: Color = .init(red: 0.11, green: 0.2, blue: 0.31)
+ static let _60: Color = .init(red: 0.14, green: 0.25, blue: 0.38)
+ static let _80: Color = .init(red: 0.15, green: 0.28, blue: 0.42)
+ }
+
+ private enum MullvadDarkBlue {
+ static let base: Color = .init(red: 0.10, green: 0.18, blue: 0.27)
+ }
+
+ private enum MullvadRed {
+ static let base: Color = .init(red: 0.89, green: 0.25, blue: 0.22)
+ }
+
+ private enum MullvadGreen {
+ static let base: Color = .init(red: 0.27, green: 0.68, blue: 0.3)
+ }
+
+ private enum MullvadYellow {
+ static let base: Color = .init(red: 1, green: 0.84, blue: 0.14)
+ }
+
+ private enum MullvadWhiteOnDarkBlue {
+ static let _5: Color = .init(red: 0.15, green: 0.22, blue: 0.31)
+ }
+
+ private enum MullvadWhite {
+ static let _100: Color = .white
+ static let _80: Color = _100.opacity(0.8)
+ static let _60: Color = _100.opacity(0.6)
+ static let _40: Color = _100.opacity(0.4)
+ static let _20: Color = _100.opacity(0.2)
+ }
+
+ enum MullvadText {
+ static let inputPlaceholder: Color = MullvadWhite._60
+ static let disabled: Color = MullvadWhite._20
+ static let onBackgroundEmphasis100: Color = MullvadWhite._100
+ }
+
enum MullvadButton {
static let primary: Color = .mullvadPrimaryColor
static let primaryPressed = Color(red: 0.12, green: 0.23, blue: 0.34)
@@ -30,4 +74,29 @@ extension Color {
static let separator: Color = .mullvadSecondaryColor
static let background: Color = .mullvadPrimaryColor
}
+
+ enum MullvadTextField {
+ static let background: Color = .MullvadBlue._40
+ static let backgroundDisabled: Color = .MullvadWhiteOnDarkBlue._5
+ static let backgroundSuggestion: Color = .MullvadBlue._80
+ static let inputPlaceholder: Color = MullvadText.inputPlaceholder
+ static let textDisabled: Color = MullvadText.disabled
+ static let textInput: Color = MullvadText.onBackgroundEmphasis100
+ static let label: Color = MullvadText.onBackgroundEmphasis100
+ static let border: Color = .MullvadOpacities.chalk40
+ static let borderFocused: Color = .MullvadNewGraphicalProfile.chalk
+ static let borderError: Color = .MullvadNewGraphicalProfile.red
+ }
+
+ private enum MullvadOpacities {
+ static let chalk40: Color = .MullvadNewGraphicalProfile.chalk.opacity(
+ 0.4
+ )
+ }
+
+ private enum MullvadNewGraphicalProfile {
+ static let red: Color = .init(red: 0.92, green: 0.36, blue: 0.25)
+ static let chalk: Color = .init(red: 0.97, green: 0.97, blue: 0.95)
+ static let dark: Color = .init(red: 0.31, green: 0.29, blue: 0.29)
+ }
}
diff --git a/ios/MullvadVPN/Views/MullvadPrimaryTextField.swift b/ios/MullvadVPN/Views/MullvadPrimaryTextField.swift
new file mode 100644
index 0000000000..0ee481afab
--- /dev/null
+++ b/ios/MullvadVPN/Views/MullvadPrimaryTextField.swift
@@ -0,0 +1,295 @@
+import SwiftUI
+
+struct MullvadPrimaryTextField: View {
+ private let label: String
+ private let placeholder: String
+ @Binding private var text: String
+ @Binding private var suggestion: String?
+ private let validate: ((String) -> Bool)?
+
+ init(
+ label: String,
+ placeholder: String,
+ text: Binding<String>,
+ suggestion: Binding<String?>? = nil,
+ validate: ((String) -> Bool)? = nil
+ ) {
+ self.label = label
+ self.placeholder = placeholder
+ self._text = text
+ self._suggestion = suggestion ?? .constant(nil)
+ self.validate = validate
+ }
+
+ var isValid: Bool {
+ validate?(text) ?? true
+ }
+
+ @FocusState private var isFocused: Bool
+ @Environment(\.isEnabled) private var isEnabled
+
+ private var showSuggestion: Bool {
+ if let suggestion,
+ !suggestion.isEmpty,
+ suggestion != text,
+ isEnabled {
+ return true
+ }
+ return false
+ }
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ Text(label)
+ .foregroundColor(.MullvadTextField.label)
+ VStack(spacing: 0) {
+ HStack(spacing: 4) {
+ TextField(
+ placeholder,
+ text: $text,
+ prompt: Text(placeholder)
+ .foregroundColor(
+ isEnabled ? .MullvadTextField.inputPlaceholder : .MullvadTextField.textDisabled
+ )
+ )
+ .focused($isFocused)
+ .padding(.vertical, 12)
+ if !text.isEmpty && isEnabled {
+ Button {
+ withAnimation {
+ text = ""
+ }
+ } label: {
+ Image.mullvadIconCross
+ }
+ .padding(0)
+ }
+ }
+ .zIndex(1)
+ .padding(.horizontal, 8)
+ .background(
+ isEnabled
+ ? Color.MullvadTextField.background
+ : Color.MullvadTextField
+ .backgroundDisabled
+ )
+ .foregroundColor(isEnabled ? .MullvadTextField.textInput : .MullvadTextField.textDisabled)
+ .overlay {
+ if isFocused {
+ RoundedCorner(
+ cornerRadius: 4,
+ corners: !showSuggestion
+ ? [.allCorners]
+ : [
+ .topLeft,
+ .topRight,
+ ]
+ )
+ .stroke(
+ isValid
+ ? Color.MullvadTextField.borderFocused
+ : Color.MullvadTextField.borderError,
+ lineWidth: 4
+ )
+ } else if isEnabled {
+ RoundedCorner(
+ cornerRadius: 4,
+ corners: !showSuggestion
+ ? [.allCorners]
+ : [
+ .topLeft,
+ .topRight,
+ ]
+ )
+ .stroke(
+ isValid
+ ? Color.MullvadTextField.border
+ : Color.MullvadTextField.borderError,
+ lineWidth: 2
+ )
+ }
+ }
+ .clipShape(RoundedCorner(
+ cornerRadius: 4,
+ corners: !showSuggestion
+ ? [.allCorners]
+ : [
+ .topLeft,
+ .topRight,
+ ]
+ ))
+
+ if showSuggestion,
+ let suggestion {
+ HStack {
+ Button {
+ withAnimation {
+ text = suggestion
+ }
+ } label: {
+ Text(suggestion)
+ .foregroundColor(.MullvadTextField.textInput)
+ Spacer()
+ }
+ Button {
+ withAnimation {
+ self.suggestion = nil
+ }
+ } label: {
+ Image.mullvadIconCross
+ }
+ }
+ .transition(.move(edge: .top))
+ .padding(.horizontal, 8)
+ .padding(.vertical, 12)
+ .background(Color.MullvadTextField.backgroundSuggestion)
+ }
+ }
+ .clipShape(
+ RoundedCorner(cornerRadius: 4)
+ )
+ }
+ .transformEffect(.identity)
+ .animation(.default, value: showSuggestion)
+ }
+}
+
+private struct RoundedCorner: Shape {
+ var cornerRadius: CGFloat = .infinity
+ var corners: UIRectCorner = .allCorners
+ var insertBy: CGFloat = 0
+
+ func path(in rect: CGRect) -> Path {
+ let insetRect = rect.insetBy(dx: insertBy, dy: insertBy)
+ let path = UIBezierPath(
+ roundedRect: insetRect,
+ byRoundingCorners: corners,
+ cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)
+ )
+ return Path(path.cgPath)
+ }
+}
+
+@available(iOS 17.0, *)
+#Preview {
+ @Previewable @State var suggestion: String? = "1234"
+ @Previewable @State var text = ""
+ VStack {
+ MullvadPrimaryTextField(
+ label: "Label",
+ placeholder: "Placeholder text",
+ text: $text,
+ suggestion: $suggestion
+ )
+
+ MullvadPrimaryTextField(
+ label: "Label",
+ placeholder: "Placeholder text",
+ text: $text,
+ suggestion: $suggestion,
+ validate: { _ in
+ false
+ }
+ )
+
+ MullvadPrimaryTextField(
+ label: "Label",
+ placeholder: "Placeholder text",
+ text: $text,
+ suggestion: $suggestion
+ )
+ .disabled(true)
+ }
+ .padding()
+ .background(Color.yellow)
+}
+
+class UIMullvadPrimaryTextField: UIHostingController<UIMullvadPrimaryTextField.Wrapper> {
+ var text: String {
+ get {
+ rootView.text
+ }
+ set {
+ rootView.text = newValue
+ }
+ }
+
+ struct Wrapper: View {
+ let label: String
+ let placeholder: String
+ @State var text = ""
+ @State var suggestion: String?
+ let validate: ((String) -> Bool)?
+ var contentType: UITextContentType?
+ var keyboardType: UIKeyboardType = .default
+ var submitLabel: SubmitLabel?
+ var body: some View {
+ MullvadPrimaryTextField(
+ label: label,
+ placeholder: placeholder,
+ text: $text,
+ suggestion: $suggestion,
+ validate: validate
+ )
+ .textContentType(contentType)
+ .keyboardType(keyboardType)
+ .apply {
+ if let submitLabel {
+ $0.submitLabel(submitLabel)
+ } else {
+ $0
+ }
+ }
+ }
+ }
+
+ init(
+ label: String,
+ placeholder: String,
+ validate: ((String) -> Bool)? = nil,
+ contentType: UITextContentType? = nil,
+ keyboardType: UIKeyboardType = .default
+ ) {
+ let rootView = Wrapper(
+ label: label,
+ placeholder: placeholder,
+ validate: validate,
+ contentType: contentType,
+ keyboardType: keyboardType
+ )
+
+ super.init(rootView: rootView)
+ }
+
+ override func viewDidLoad() {
+ view.backgroundColor = .clear
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("Not implemented")
+ }
+}
+
+struct UIMullvadPrimaryTextFieldRepresentable: UIViewRepresentable {
+ func makeCoordinator() -> Coordinator {
+ Coordinator()
+ }
+
+ func makeUIView(context: Context) -> UIView {
+ let controller = UIMullvadPrimaryTextField(label: "Label", placeholder: "Placeholder")
+ context.coordinator.controller = controller
+ return controller.view
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {}
+
+ class Coordinator {
+ var controller: UIMullvadPrimaryTextField?
+ }
+}
+
+#Preview {
+ UIMullvadPrimaryTextFieldRepresentable()
+ .padding()
+ .background(Color.yellow)
+}
diff --git a/ios/MullvadVPN/Views/MullvadSecondaryTextField.swift b/ios/MullvadVPN/Views/MullvadSecondaryTextField.swift
new file mode 100644
index 0000000000..cf98d98caa
--- /dev/null
+++ b/ios/MullvadVPN/Views/MullvadSecondaryTextField.swift
@@ -0,0 +1,86 @@
+import SwiftUI
+
+struct MullvadSecondaryTextField: View {
+ let placeholder: String
+ @Binding var text: String
+ var isValid = true
+
+ @FocusState private var isFocused: Bool
+ @Environment(\.isEnabled) private var isEnabled
+
+ var body: some View {
+ HStack(spacing: 4) {
+ Image.mullvadIconSearch
+ TextField(
+ placeholder,
+ text: $text,
+ prompt: Text(placeholder)
+ .foregroundColor(
+ isEnabled ? .MullvadTextField.inputPlaceholder : .MullvadTextField.textDisabled
+ )
+ )
+ .focused($isFocused)
+ if !text.isEmpty && isEnabled {
+ Button {
+ withAnimation {
+ text = ""
+ }
+ } label: {
+ Image.mullvadIconCross
+ }
+ }
+ }
+ .padding(8)
+ .background(
+ isEnabled ? Color.MullvadTextField.background : Color.MullvadTextField
+ .backgroundDisabled
+ )
+ .foregroundColor(isEnabled ? .MullvadTextField.textInput : .MullvadTextField.textDisabled)
+ .overlay {
+ if isFocused {
+ RoundedRectangle(cornerRadius: 12)
+ .inset(by: 1)
+ .stroke(
+ isValid ? Color.MullvadTextField.borderFocused : Color.MullvadTextField.borderError,
+ lineWidth: 2
+ )
+ } else if isEnabled,
+ !isValid {
+ RoundedRectangle(cornerRadius: 12)
+ .inset(by: 0.5)
+ .stroke(
+ Color.MullvadTextField.borderError,
+ lineWidth: 1
+ )
+ }
+ }
+ .clipShape(
+ RoundedRectangle(cornerRadius: 12)
+ )
+ }
+}
+
+#Preview {
+ StatefulPreviewWrapper("") { text in
+ VStack {
+ MullvadSecondaryTextField(
+ placeholder: "Placeholder text",
+ text: text
+ )
+
+ MullvadSecondaryTextField(
+ placeholder: "Placeholder text",
+ text: text,
+ isValid: false
+ )
+
+ MullvadSecondaryTextField(
+ placeholder: "Placeholder text",
+ text: text
+ )
+ .disabled(true)
+ }
+ .padding()
+ .background(Color.mullvadBackground)
+ }
+}