Campo de texto opcional de SwiftUI
¿Pueden los campos de texto SwiftUI funcionar con enlaces opcionales? Actualmente este código:
struct SOTestView : View {
@State var test: String? = "Test"
var body: some View {
TextField($test)
}
}
produce el siguiente error:
No se puede convertir el valor del tipo 'Binding< String?>' al tipo de argumento esperado 'Binding< String>'
¿Hay alguna manera de evitar esto? El uso de opciones opcionales en modelos de datos es un patrón muy común; de hecho, es el valor predeterminado en Core Data, por lo que parece extraño que SwiftUI no los admita.
Puede agregar esta sobrecarga de operador y luego funciona con tanta naturalidad como si no fuera un enlace.
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
Esto crea un enlace que devuelve el lado izquierdo del valor del operador si no es nulo; de lo contrario, devuelve el valor predeterminado del lado derecho.
Al configurarlo, solo establece el valor lhs e ignora todo lo que tenga que ver con el lado derecho.
Se puede utilizar así:
TextField("", text: $test ?? "default value")
En última instancia, la API no permite esto, pero existe una solución alternativa muy simple y versátil:
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}
Esto le permite mantener el opcional y al mismo tiempo hacerlo compatible con los enlaces:
TextField($test.bound)
Prefiero la respuesta proporcionada por @Jonathon. ya que es simple y elegante y proporciona al codificador un caso base in situ cuando es Optional
( .none
= nil
) y no .some
.
Sin embargo, creo que vale la pena aportar mi granito de arena. Aprendí esta técnica leyendo el blog de Jim Dovey sobre SwiftUI Bindings with Core Data . Es esencialmente la misma respuesta proporcionada por @Jonathon. pero incluye un patrón agradable que se puede replicar para varios tipos de datos diferentes.
Primero crea una extensión enBinding
public extension Binding where Value: Equatable {
init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
self.init(
get: { source.wrappedValue ?? nilProxy },
set: { newValue in
if newValue == nilProxy { source.wrappedValue = nil }
else { source.wrappedValue = newValue }
}
)
}
}
Luego úsalo en tu código como este...
TextField("", text: Binding($test, replacingNilWith: String()))
o
TextField("", text: Binding($test, replacingNilWith: ""))
No querrás crear una nueva Binding
en cada body
carga. Utilice una costumbre ParseableFormatStyle
en su lugar.
struct OptionalStringParseableFormatStyle: ParseableFormatStyle {
var parseStrategy: Strategy = .init()
func format(_ value: String?) -> String {
value ?? ""
}
struct Strategy: ParseStrategy {
func parse(_ value: String) throws -> String? {
value
}
}
}
Entonces úsalo
TextField("My Label", value: $myValue, format: OptionalStringParseableFormatStyle())
Hazlo genérico
Prefiero hacer versiones genéricas de estos estilos con comodidades estáticas que puedan funcionar con cualquier tipo.
extension Optional {
struct FormatStyle<Format: ParseableFormatStyle>: ParseableFormatStyle
where Format.FormatOutput == String, Format.FormatInput == Wrapped {
let formatter: Format
let parseStrategy: Strategy<Format.Strategy>
init(format: Format) {
self.formatter = format
self.parseStrategy = .init(strategy: format.parseStrategy)
}
func format(_ value: Format.FormatInput?) -> Format.FormatOutput {
guard let value else { return "" }
return formatter.format(value)
}
struct Strategy<OutputStrategy: ParseStrategy>: ParseStrategy where OutputStrategy.ParseInput == String {
let strategy: OutputStrategy
func parse(_ value: String) throws -> OutputStrategy.ParseOutput? {
guard !value.isEmpty else { return nil }
return try strategy.parse(value)
}
}
}
}
extension ParseableFormatStyle where FormatOutput == String {
var optional: Optional<FormatInput>.FormatStyle<Self> { .init(format: self) }
}
Dado que la cadena no tiene un estilo de formato, ya que es redundante en la mayoría de los casos, hago una identidadParseableFormatStyle
extension String {
struct FormatStyle: ParseableFormatStyle {
var parseStrategy: Strategy = .init()
func format(_ value: String) -> String {
value
}
struct Strategy: ParseStrategy {
func parse(_ value: String) throws -> String {
value
}
}
}
}
extension ParseableFormatStyle where Self == String.FormatStyle {
static var string: Self { .init() }
}
extension ParseableFormatStyle where Self == Optional<String>.FormatStyle<String.FormatStyle> {
static var optional: Self { .init(format: .string) }
}
Ahora puedes usar esto para cualquier valor. Ejemplos:
TextField("My Label", value: $myStringValue, format: .optional)
TextField("My Label", value: $myStringValue, format: .string.optional)
TextField("My Label", value: $myNumberValue, format: .number.optional)
TextField("My Label", value: $myDateValue, format: .dateTime.optional)