Campo de texto opcional de SwiftUI

Resuelto Brandon Bradley asked hace 5 años • 8 respuestas

¿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.

Brandon Bradley avatar Jul 14 '19 01:07 Brandon Bradley
Aceptado

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")
Jonathan. avatar Apr 02 '2020 23:04 Jonathan.

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)
Brandon Bradley avatar Jul 15 '2019 13:07 Brandon Bradley

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: ""))
andrewbuilder avatar May 18 '2022 14:05 andrewbuilder

No querrás crear una nueva Bindingen cada bodycarga. Utilice una costumbre ParseableFormatStyleen 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)
Justin Oroz avatar Nov 19 '2023 09:11 Justin Oroz