Skip to content

Instantly share code, notes, and snippets.

@DandyLyons
Created August 27, 2024 17:25
Show Gist options
  • Select an option

  • Save DandyLyons/80312b225934b79fc895cd0b924566a3 to your computer and use it in GitHub Desktop.

Select an option

Save DandyLyons/80312b225934b79fc895cd0b924566a3 to your computer and use it in GitHub Desktop.
OptionalTextField.swift
//
// OptionalTextField.swift
//
//
// Created by Daniel Lyons on 2024-08-12.
//
import Foundation
import SwiftUI
/// A `TextField` with a `Toggle` which allows the user to include or remove the `TextField`.
///
/// If the user sets the `Toggle` to `false` then the `TextField` will be replaced with the `noTextFieldView`
/// and the `optionalString` will be set to `nil`.
@available(iOS 15.0, macOS 12.0, *)
public struct OptionalTextField<AddTextFieldLabel: View, RemoveTextFieldLabel: View>: View {
@Binding var optionalString: String?
@State private var hasString: Bool
/// a string to assign to ``optionalString`` if ever the user toggles the string off and then on again
///
/// This cache will only live as long as the View
@State private var cachedString: String
let titleKey: LocalizedStringKey
// the label of the add text field button
let addTextFieldLabel: AddTextFieldLabel
// the label of the remove text field button
let removeTextFieldLabel: RemoveTextFieldLabel
let prompt: Text?
/// Creates an OptionalTextField
///
/// >Note: `prompt` will be ignored pre-iOS 15 and macOS 12
public init(
_ titleKey: LocalizedStringKey,
text optionalString: Binding<String?>,
prompt: Text? = nil,
@ViewBuilder addTextFieldLabel: () -> AddTextFieldLabel,
@ViewBuilder removeTextFieldLabel: () -> RemoveTextFieldLabel
) {
self.titleKey = titleKey
self._optionalString = optionalString
self.prompt = prompt
self.cachedString = optionalString.wrappedValue ?? ""
self.hasString = optionalString.wrappedValue != nil
self.addTextFieldLabel = addTextFieldLabel()
self.removeTextFieldLabel = removeTextFieldLabel()
}
@FocusState var isFocusedOnTextField: Bool
public var body: some View {
Group {
if let string = optionalString {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
HStack {
Button {
withAnimation {
isFocusedOnTextField = false // we must remove the focus first, otherwise the focus will be on the TextField, which will write to the String which will cause optionalString to not be nil
hasString = false
}
} label: {
removeTextFieldLabel
}
TextField(
titleKey,
text: $optionalString.toNonOptional(defaultValue: string), // it should not be possible for this default value to ever be used
prompt: prompt
)
.focused($isFocusedOnTextField)
}
.buttonStyle(.plain)
} else {
TextField(
titleKey,
text: $optionalString.toNonOptional(defaultValue: string) // it should not be possible for this default value to ever be used
)
}
} else {
Button {
withAnimation {
isFocusedOnTextField = true
hasString = true
}
} label: {
addTextFieldLabel
}
}
}
.onChange(of: hasString) { newValue in
optionalString = if newValue { cachedString } else { nil }
}
.onChange(of: optionalString) { newValue in
guard let newValue else {
return
}
cachedString = newValue
}
}
}
@available(iOS 15.0, macOS 12.0, *)
struct OptionalTextFieldPreview: View {
@State var string: String? = "DandyLyons"
var body: some View {
OptionalTextField(
"Nickname",
text: $string.animation(),
addTextFieldLabel: {
Label("Add Nickname", systemImage: "plus.circle.fill")
},
removeTextFieldLabel: {
Label("Remove Nickname", systemImage: "minus.circle.fill")
.labelStyle(.iconOnly)
}
)
}
}
#Preview {
Form {
if #available(iOS 15.0, macOS 12.0, *) {
OptionalTextFieldPreview()
} else {
// Fallback on earlier versions
}
}
}
@available(iOS 15.0, macOS 12.0, *)
extension OptionalTextField where AddTextFieldLabel == Text, RemoveTextFieldLabel == Text {
public init(
_ titleKey: LocalizedStringKey,
text optionalString: Binding<String?>,
prompt: LocalizedStringKey,
addTextField: LocalizedStringKey,
removeTextField: LocalizedStringKey
) {
self.titleKey = titleKey
self._optionalString = optionalString
self.cachedString = optionalString.wrappedValue ?? ""
self.hasString = optionalString.wrappedValue != nil
self.prompt = Text(prompt)
self.addTextFieldLabel = Text(addTextField)
self.removeTextFieldLabel = Text(removeTextField)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment