Mueva TextField hacia arriba cuando el teclado haya aparecido en SwiftUI

100

Tengo siete TextFielddentro de mi principal ContentView. Cuando el usuario abre el teclado, algunos de ellos TextFieldestán ocultos debajo del marco del teclado. Así que quiero mover todos TextFieldhacia arriba respectivamente cuando haya aparecido el teclado.

He usado el siguiente código para agregar TextFielden la pantalla.

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
            VStack {
                TextField($textfieldText, placeholder: Text("TextField1"))
                TextField($textfieldText, placeholder: Text("TextField2"))
                TextField($textfieldText, placeholder: Text("TextField3"))
                TextField($textfieldText, placeholder: Text("TextField4"))
                TextField($textfieldText, placeholder: Text("TextField5"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField7"))
            }
    }
}

Salida:

Salida

Hitesh Surani
fuente
Puede utilizar ScrollView. developer.apple.com/documentation/swiftui/scrollview
Prashant Tukadiya
1
@PrashantTukadiya Gracias por la rápida respuesta. Agregué TextField dentro de Scrollview pero sigo enfrentando el mismo problema.
Hitesh Surani
1
@DimaPaliychuk Esto no funcionará. es SwiftUI
Prashant Tukadiya
20
La visualización del teclado y su contenido oculto en la pantalla ha existido desde qué, ¿la primera aplicación de iPhone Objective C? Este es un problema que se resuelve constantemente . Por mi parte, estoy decepcionado de que Apple no haya abordado esto con SwiftUi. Sé que este comentario no es útil para nadie, pero quería plantear este problema de que realmente deberíamos presionar a Apple para que brinde una solución y no depender de la comunidad para que siempre brinde este problema más común.
P. Ent
1
Hay un muy buen artículo de Vadim vadimbulavin.com/…
Sudara

Respuestas:

64

Código actualizado para Xcode, beta 7.

No necesita relleno, ScrollViews o Lists para lograr esto. Aunque esta solución también funcionará bien con ellos. Incluyo dos ejemplos aquí.

El primero mueve todo el campo de texto hacia arriba, si el teclado aparece para alguno de ellos. Pero solo si es necesario. Si el teclado no oculta los campos de texto, no se moverán.

En el segundo ejemplo, la vista solo se mueve lo suficiente para evitar ocultar el campo de texto activo.

Ambos ejemplos usan el mismo código común que se encuentra al final: GeometryGetter y KeyboardGuardian

Primer ejemplo (mostrar todos los campos de texto)

Cuando se abre el teclado, los 3 campos de texto se mueven hacia arriba lo suficiente para mantenerlos todos visibles

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 1)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("enter text #1", text: $name[0])
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("enter text #2", text: $name[1])
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("enter text #3", text: $name[2])
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

        }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }

}

Segundo ejemplo (muestra solo el campo activo)

Cuando se hace clic en cada campo de texto, la vista solo se mueve hacia arriba lo suficiente para hacer visible el campo de texto en el que se hizo clic.

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 3)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("text #1", text: $name[0], onEditingChanged: { if $0 { self.kGuardian.showField = 0 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

            TextField("text #2", text: $name[1], onEditingChanged: { if $0 { self.kGuardian.showField = 1 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[1]))

            TextField("text #3", text: $name[2], onEditingChanged: { if $0 { self.kGuardian.showField = 2 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[2]))

            }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }.onAppear { self.kGuardian.addObserver() } 
.onDisappear { self.kGuardian.removeObserver() }

}

Geometry Getter

Esta es una vista que absorbe el tamaño y la posición de su vista principal. Para lograrlo, se llama dentro del modificador .background. Este es un modificador muy poderoso, no solo una forma de decorar el fondo de una vista. Al pasar una vista a .background (MyView ()), MyView obtiene la vista modificada como padre. El uso de GeometryReader es lo que hace posible que la vista conozca la geometría del padre.

Por ejemplo: Text("hello").background(GeometryGetter(rect: $bounds))llenará los límites de las variables, con el tamaño y la posición de la vista Texto, y utilizando el espacio de coordenadas global.

struct GeometryGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { geometry in
            Group { () -> AnyView in
                DispatchQueue.main.async {
                    self.rect = geometry.frame(in: .global)
                }

                return AnyView(Color.clear)
            }
        }
    }
}

Actualización Agregué DispatchQueue.main.async, para evitar la posibilidad de modificar el estado de la vista mientras se está renderizando. ***

KeyboardGuardian

El propósito de KeyboardGuardian es realizar un seguimiento de los eventos de mostrar / ocultar del teclado y calcular cuánto espacio debe desplazarse la vista.

Actualización: modifiqué KeyboardGuardian para actualizar la diapositiva, cuando el usuario pasa de un campo a otro

import SwiftUI
import Combine

final class KeyboardGuardian: ObservableObject {
    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    public var keyboardIsHidden = true

    @Published var slide: CGFloat = 0

    var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

    }

    func addObserver() {
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)
}

func removeObserver() {
 NotificationCenter.default.removeObserver(self)
}

    deinit {
        NotificationCenter.default.removeObserver(self)
    }



    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            let tfRect = self.rects[self.showField]
            let diff = keyboardRect.minY - tfRect.maxY

            if diff > 0 {
                slide += diff
            } else {
                slide += min(diff, 0)
            }

        }
    }
}
Kon Tiki
fuente
1
¿Es posible adjuntar GeometryGettercomo un modificador de vista que un fondo haciéndolo cumplir con el ViewModifierprotocolo?
Sudara
2
Es posible, pero ¿cuál es la ganancia? Lo adjuntaría así: en .modifier(GeometryGetter(rect: $kGuardian.rects[1]))lugar de .background(GeometryGetter(rect: $kGuardian.rects[1])). No hay mucha diferencia (solo 2 caracteres menos).
kontiki
3
En algunas situaciones, puede obtener un ABORTO DE SEÑAL del programa dentro de GeometryGetter al asignar el nuevo rectángulo si está navegando fuera de esta pantalla. Si eso le sucede, simplemente agregue un código para verificar que el tamaño de la geometría sea mayor que cero (geometry.size.width> 0 && geometry.size.height> 0) antes de asignar un valor a self.rect
Julio Bailon
1
@JulioBailon No sé por qué, pero mudarse geometry.framede DispatchQueue.main.asyncayudó con SIGNAL ABORT, ahora probará su solución. Actualización: if geometry.size.width > 0 && geometry.size.height > 0antes de asignar self.rectayudó.
Roman Vasilyev
2
esto también se rompe para mí en self.rect = geometry.frame (en: .global) obteniendo SIGNAL ABORT y probé todas las soluciones propuestas para abordar este error
Marwan Roushdy
55

Para construir a partir de la solución de @rraphael, la convertí para que fuera utilizable por el soporte de swiftUI de xcode11 de hoy.

import SwiftUI

final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published private(set) var currentHeight: CGFloat = 0

    init(center: NotificationCenter = .default) {
        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        currentHeight = 0
    }
}

Uso:

struct ContentView: View {
    @ObservedObject private var keyboard = KeyboardResponder()
    @State private var textFieldInput: String = ""

    var body: some View {
        VStack {
            HStack {
                TextField("uMessage", text: $textFieldInput)
            }
        }.padding()
        .padding(.bottom, keyboard.currentHeight)
        .edgesIgnoringSafeArea(.bottom)
        .animation(.easeOut(duration: 0.16))
    }
}

La publicación currentHeightactivará una nueva representación de la interfaz de usuario y moverá su TextField hacia arriba cuando se muestre el teclado y hacia abajo cuando se cierre. Sin embargo, no utilicé un ScrollView.

Michael Neas
fuente
6
Me gusta esta respuesta por su simplicidad. Agregué .animation(.easeOut(duration: 0.16))para tratar de igualar la velocidad del teclado deslizándose hacia arriba.
Mark Moeykens
¿Por qué ha establecido una altura máxima de 340 para el teclado?
Daniel Ryan
1
@DanielRyan A veces, la altura del teclado devolvía valores incorrectos en el simulador. Parece que no puedo encontrar una manera de precisar el problema actualmente
Michael Neas
1
Yo mismo no he visto ese problema. Quizás esté arreglado en las últimas versiones. No quería bloquear el tamaño en caso de que haya (o haya) teclados más grandes.
Daniel Ryan
1
Podrías probar con keyboardFrameEndUserInfoKey. Eso debería contener el fotograma final del teclado.
Mathias Claassen
50

Probé muchas de las soluciones propuestas y, aunque funcionan en la mayoría de los casos, tuve algunos problemas, principalmente con el área segura (tengo un formulario dentro de la pestaña de TabView).

Terminé combinando algunas soluciones diferentes y usando GeometryReader para obtener la inserción inferior del área segura de la vista específica y usarla en el cálculo del relleno:

import SwiftUI
import Combine

struct AdaptsToKeyboard: ViewModifier {
    @State var currentHeight: CGFloat = 0

    func body(content: Content) -> some View {
        GeometryReader { geometry in
            content
                .padding(.bottom, self.currentHeight)
                .animation(.easeOut(duration: 0.16))
                .onAppear(perform: {
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillShowNotification)
                        .merge(with: NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillChangeFrameNotification))
                        .compactMap { notification in
                            notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect
                    }
                    .map { rect in
                        rect.height - geometry.safeAreaInsets.bottom
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))

                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillHideNotification)
                        .compactMap { notification in
                            CGFloat.zero
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
                })
        }
    }
}

Uso:

struct MyView: View {
    var body: some View {
        Form {...}
        .modifier(AdaptsToKeyboard())
    }
}
Predrag Samardzic
fuente
7
Vaya, esta es la versión más SwiftUI de todas, con GeometryReader y ViewModifier. Quiéralo.
Mateusz
3
Esto es tan útil y elegante. Muchas gracias por escribir esto.
Danilo Campos
2
Veo una pequeña vista en blanco sobre mi teclado. Esta vista es GeometryReader View, lo confirmé cambiando el color de fondo. Alguna idea de por qué GeometryReader se muestra entre mi Vista real y el teclado.
user832
6
Recibo el error Thread 1: signal SIGABRTen línea rect.height - geometry.safeAreaInsets.bottomcuando voy a la vista con el teclado por segunda vez y hago clic en TextField. No importa si hago clic TextFieldla primera vez o no. La aplicación aún falla.
JLively
2
¡Finalmente algo que funciona! ¡Por qué Apple no puede hacer esto para nosotros es una locura!
Dave Kozikowski
35

Creé una Vista que puede envolver cualquier otra vista para reducirla cuando aparece el teclado.

Es bastante simple. Creamos editores para eventos de mostrar / ocultar teclado y luego suscribimos a ellos usando onReceive. Usamos el resultado de eso para crear un rectángulo del tamaño de un teclado detrás del teclado.

struct KeyboardHost<Content: View>: View {
    let view: Content

    @State private var keyboardHeight: CGFloat = 0

    private let showPublisher = NotificationCenter.Publisher.init(
        center: .default,
        name: UIResponder.keyboardWillShowNotification
    ).map { (notification) -> CGFloat in
        if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
            return rect.size.height
        } else {
            return 0
        }
    }

    private let hidePublisher = NotificationCenter.Publisher.init(
        center: .default,
        name: UIResponder.keyboardWillHideNotification
    ).map {_ -> CGFloat in 0}

    // Like HStack or VStack, the only parameter is the view that this view should layout.
    // (It takes one view rather than the multiple views that Stacks can take)
    init(@ViewBuilder content: () -> Content) {
        view = content()
    }

    var body: some View {
        VStack {
            view
            Rectangle()
                .frame(height: keyboardHeight)
                .animation(.default)
                .foregroundColor(.clear)
        }.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = height
        }
    }
}

Luego puede usar la vista así:

var body: some View {
    KeyboardHost {
        viewIncludingKeyboard()
    }
}

Para mover el contenido de la vista hacia arriba en lugar de reducirlo, se puede agregar relleno o desplazamiento en viewlugar de colocarlo en una VStack con un rectángulo.

Benjamin Kindle
fuente
6
Creo que esta es la respuesta correcta. Solo hice un pequeño ajuste: en lugar de un rectángulo, solo estoy modificando el relleno self.viewy funciona muy bien. No hay ningún problema con la animación
Tae
5
¡Gracias! Funciona perfectamente. Como dijo @Taed, es mejor usar un enfoque de relleno. El resultado final seríavar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente
1
A pesar de los votos menores, esta es la respuesta más rápida. Y el enfoque anterior con AnyView, rompe la ayuda de aceleración de Metal.
Nelson Cardaci
4
Es una gran solución, pero el problema principal aquí es que pierde la capacidad de subir la vista solo si el teclado oculta el campo de texto que está editando. Quiero decir: si tiene un formulario con varios campos de texto y comienza a editar el primero en la parte superior, probablemente no quiera que se mueva hacia arriba porque se movería fuera de la pantalla.
superpuccio
Realmente me gusta la respuesta, pero como todas las otras respuestas, no funciona si su vista está dentro de una TabBar o la Vista no está alineada con la parte inferior de la pantalla.
Ben Patch
25

He creado un modificador de vista realmente simple de usar.

Agregue un archivo Swift con el código a continuación y simplemente agregue este modificador a sus vistas:

.keyboardResponsive()
import SwiftUI

struct KeyboardResponsiveModifier: ViewModifier {
  @State private var offset: CGFloat = 0

  func body(content: Content) -> some View {
    content
      .padding(.bottom, offset)
      .onAppear {
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notif in
          let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
          let height = value.height
          let bottomInset = UIApplication.shared.windows.first?.safeAreaInsets.bottom
          self.offset = height - (bottomInset ?? 0)
        }

        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { notif in
          self.offset = 0
        }
    }
  }
}

extension View {
  func keyboardResponsive() -> ModifiedContent<Self, KeyboardResponsiveModifier> {
    return modifier(KeyboardResponsiveModifier())
  }
}

jberlana
fuente
Agradable. Gracias por compartir.
décadas
2
Sería genial, si solo se desplazara, si fuera necesario (es decir, no se desplaza, si el teclado no cubre el elemento de entrada). Es bueno tener ...
décadas
Esto funciona muy bien, gracias. Una implementación muy limpia también, y para mí, solo se desplaza si es necesario.
Joshua
¡Increíble! ¿Por qué no proporcionas esto en Github o en otro lugar? :) O puede sugerir esto a github.com/hackiftekhar/IQKeyboardManager ya que aún no tienen un soporte completo de SwiftUI
Schnodderbalken
No funcionará bien con los cambios de orientación y se compensará independientemente de si es necesario o no.
GrandSteph
16

O simplemente puede usar IQKeyBoardManagerSwift

y, opcionalmente, puede agregar esto al delegado de su aplicación para ocultar la barra de herramientas y habilitar la ocultación del teclado al hacer clic en cualquier vista que no sea el teclado.

        IQKeyboardManager.shared.enableAutoToolbar = false
        IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide
Amit Samant
fuente
De hecho, esta es la forma (inesperada) para mí también. Sólido.
HelloTimo
Este marco funcionó incluso mejor de lo esperado. ¡Gracias por compartir!
Richard Poutier
1
Funcionando bien para mí en SwiftUI - gracias @DominatorVbN - Yo en el modo horizontal de iPad necesitaba aumentar IQKeyboardManager.shared.keyboardDistanceFromTextFielda 40 para obtener un espacio cómodo.
Richard Groves
También tuve que configurar IQKeyboardManager.shared.enable = truepara evitar que el teclado ocultara mis campos de texto. En cualquier caso, esta es la mejor solución. Tengo 4 campos dispuestos verticalmente y las otras soluciones funcionarían para mi campo más bajo, pero empujarían al campo más alto fuera de la vista.
MisterEd
12

Debe agregar un ScrollViewy establecer un relleno inferior del tamaño del teclado para que el contenido pueda desplazarse cuando aparezca el teclado.

Para obtener el tamaño del teclado, deberá usar el NotificationCenterevento para registrarse para teclados. Puede usar una clase personalizada para hacerlo:

import SwiftUI
import Combine

final class KeyboardResponder: BindableObject {
    let didChange = PassthroughSubject<CGFloat, Never>()

    private var _center: NotificationCenter
    private(set) var currentHeight: CGFloat = 0 {
        didSet {
            didChange.send(currentHeight)
        }
    }

    init(center: NotificationCenter = .default) {
        _center = center
        _center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        _center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        _center.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        print("keyboard will show")
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        print("keyboard will hide")
        currentHeight = 0
    }
}

La BindableObjectconformidad le permitirá utilizar esta clase como Statey activar la actualización de la vista. Si es necesario, consulte el tutorial para BindableObject: tutorial de SwiftUI

Cuando lo obtenga, debe configurar a ScrollViewpara reducir su tamaño cuando aparezca el teclado. Por conveniencia, envolví esto ScrollViewen algún tipo de componente:

struct KeyboardScrollView<Content: View>: View {
    @State var keyboard = KeyboardResponder()
    private var content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        ScrollView {
            VStack {
                content
            }
        }
        .padding(.bottom, keyboard.currentHeight)
    }
}

Todo lo que tienes que hacer ahora es incrustar tu contenido dentro del archivo personalizado ScrollView.

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
        KeyboardScrollView {
            ForEach(0...10) { index in
                TextField(self.$textfieldText, placeholder: Text("TextField\(index)")) {
                    // Hide keyboard when uses tap return button on keyboard.
                    self.endEditing(true)
                }
            }
        }
    }

    private func endEditing(_ force: Bool) {
        UIApplication.shared.keyWindow?.endEditing(true)
    }
}

Editar: el comportamiento de desplazamiento es realmente extraño cuando el teclado se oculta. Tal vez usar una animación para actualizar el relleno solucionaría esto, o debería considerar usar algo más que paddingpara ajustar el tamaño de la vista de desplazamiento.

rraphael
fuente
oye, parece que tienes experiencia en bindableobject. No puedo hacer que funcione como quiero. Sería bueno si pudiera consultar: stackoverflow.com/questions/56500147/…
SwiftiSwift
¿Por qué no estás usando @ObjectBinding
SwiftiSwift
3
Con BindableObjectobsoleto, esto ya no funciona, desafortunadamente.
LinusGeffarth
2
@LinusGeffarth Por lo que vale, BindableObjectsimplemente se le cambió el nombre a ObservableObjecty didChangea objectWillChange. El objeto actualiza la vista muy bien (aunque probé usando en @ObservedObjectlugar de @State)
SeizeTheDay
Hola, esta solución desplaza el contenido, pero muestra un área blanca sobre el teclado que oculta la mitad del campo de texto. Hágame saber cómo podemos eliminar el área blanca.
Shahbaz Sajjad
12

Xcode 12 - Código de una línea

Agregue este modificador al TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Manifestación

Apple agregó el teclado como una región para el área segura, por lo que puede usarlo para mover cualquieraView con el teclado como otras regiones.

Mojtaba Hosseini
fuente
funciona para TextField, pero ¿qué pasa con TextEditor?
Андрей Первушин
Funciona en Any View , incluido el TextEditor.
Mojtaba Hosseini
3
recién probado, Xcode beta 6, TextEditor no funciona
Андрей Первушин
1
Puede hacer una pregunta y vincularla aquí. Entonces puedo echar un vistazo a su código reproducible y ver si puedo ayudar :) @kyrers
Mojtaba Hosseini
2
Lo descubrí yo mismo. Agregue .ignoresSafeArea(.keyboard)a su vista.
leonboe1
6

Revisé y refactoricé las soluciones existentes en un práctico paquete de SPM que proporciona un .keyboardAware()modificador:

KeyboardAwareSwiftUI

Ejemplo:

struct KeyboardAwareView: View {
    @State var text = "example"

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(0 ..< 20) { i in
                        Text("Text \(i):")
                        TextField("Text", text: self.$text)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .padding(.bottom, 10)
                    }
                }
                .padding()
            }
            .keyboardAware()  // <--- the view modifier
            .navigationBarTitle("Keyboard Example")
        }

    }
}

Fuente:

import UIKit
import SwiftUI

public class KeyboardInfo: ObservableObject {

    public static var shared = KeyboardInfo()

    @Published public var height: CGFloat = 0

    private init() {
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIApplication.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }

    @objc func keyboardChanged(notification: Notification) {
        if notification.name == UIApplication.keyboardWillHideNotification {
            self.height = 0
        } else {
            self.height = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
        }
    }

}

struct KeyboardAware: ViewModifier {
    @ObservedObject private var keyboard = KeyboardInfo.shared

    func body(content: Content) -> some View {
        content
            .padding(.bottom, self.keyboard.height)
            .edgesIgnoringSafeArea(self.keyboard.height > 0 ? .bottom : [])
            .animation(.easeOut)
    }
}

extension View {
    public func keyboardAware() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAware())
    }
}
Ralf Ebert
fuente
Solo veo la mitad de la altura de la vista de texto. ¿Sabes cómo resolver esto?
Matrosov Alexander
Bueno. esto me salvó el tiempo. Antes de usar esto, sabemos que este modificador maneja el relleno inferior de la vista.
Brownsoo Han
5

Usé la respuesta de Benjamin Kindle como punto de partida, pero tenía algunos problemas que quería abordar.

  1. La mayoría de las respuestas aquí no se refieren al cambio de marco del teclado, por lo que se rompen si el usuario gira el dispositivo con el teclado en pantalla. Agregar keyboardWillChangeFrameNotificationa la lista de notificaciones procesadas soluciona este problema.
  2. No quería varios editores con cierres de mapas similares pero diferentes, así que encadené las tres notificaciones del teclado en un solo editor. Es cierto que es una cadena larga, pero cada paso es bastante sencillo.
  3. Proporcioné la initfunción que acepta una @ViewBuilderpara que pueda usar la KeyboardHostvista como cualquier otra Vista y simplemente pasar su contenido en un cierre final, en lugar de pasar la vista de contenido como un parámetro a init.
  4. Como sugirieron Tae y fdelafuente en los comentarios, cambié el Rectanglepara ajustar el acolchado inferior.
  5. En lugar de utilizar la cadena "UIKeyboardFrameEndUserInfoKey" codificada de forma rígida, quería usar las cadenas proporcionadas en UIWindowas UIWindow.keyboardFrameEndUserInfoKey.

Tirando de todo eso tengo:

struct KeyboardHost<Content>: View  where Content: View {
    var content: Content

    /// The current height of the keyboard rect.
    @State private var keyboardHeight = CGFloat(0)

    /// A publisher that combines all of the relevant keyboard changing notifications and maps them into a `CGFloat` representing the new height of the
    /// keyboard rect.
    private let keyboardChangePublisher = NotificationCenter.Publisher(center: .default,
                                                                       name: UIResponder.keyboardWillShowNotification)
        .merge(with: NotificationCenter.Publisher(center: .default,
                                                  name: UIResponder.keyboardWillChangeFrameNotification))
        .merge(with: NotificationCenter.Publisher(center: .default,
                                                  name: UIResponder.keyboardWillHideNotification)
            // But we don't want to pass the keyboard rect from keyboardWillHide, so strip the userInfo out before
            // passing the notification on.
            .map { Notification(name: $0.name, object: $0.object, userInfo: nil) })
        // Now map the merged notification stream into a height value.
        .map { ($0.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height }
        // If you want to debug the notifications, swap this in for the final map call above.
//        .map { (note) -> CGFloat in
//            let height = (note.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height
//
//            print("Received \(note.name.rawValue) with height \(height)")
//            return height
//    }

    var body: some View {
        content
            .onReceive(keyboardChangePublisher) { self.keyboardHeight = $0 }
            .padding(.bottom, keyboardHeight)
            .animation(.default)
    }

    init(@ViewBuilder _ content: @escaping () -> Content) {
        self.content = content()
    }
}

struct KeyboardHost_Previews: PreviewProvider {
    static var previews: some View {
        KeyboardHost {
            TextField("TextField", text: .constant("Preview text field"))
        }
    }
}

Timothy Sanders
fuente
esta solución no funciona, aumenta la Keyboardaltura
GSerjo
¿Puede explicar los problemas que está viendo @GSerjo? Estoy usando este código en mi aplicación y me funciona bien.
Timothy Sanders
¿Podría activarlo Pridictiveen iOS keyboard? Settings-> General-> Keyboard-> Pridictive. en este caso, no corrige calclate y agrega relleno al teclado
GSerjo
@GSerjo: Tengo habilitado el texto predictivo en un iPad Touch (séptima generación) con iOS 13.1 beta. Agrega correctamente relleno para la altura de la fila de predicción. (Es importante tener en cuenta que no estoy ajustando la altura del teclado aquí, lo estoy agregando al relleno de la vista en sí). Intente intercambiar en el mapa de depuración que está comentado y juegue con los valores que obtiene para el predictivo teclado. Publicaré un registro en otro comentario.
Timothy Sanders
Con el mapa de "depuración" sin comentar, puede ver el valor que se le asigna keyboardHeight. En mi iPod Touch (en vertical), un teclado con predictivo es de 254 puntos. Sin ella son 216 puntos. Incluso puedo desactivar la función predictiva con un teclado en pantalla y el relleno se actualiza correctamente. Agregar un teclado con predictivo: Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 cuando desactivo el texto predictivo:Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders
4

Esto está adaptado de lo que construyó @kontiki. Lo tengo ejecutándose en una aplicación bajo beta 8 / GM seed, donde el campo que necesita desplazarse es parte de un formulario dentro de NavigationView. Aquí está KeyboardGuardian:

//
//  KeyboardGuardian.swift
//
//  /programming/56491881/move-textfield-up-when-thekeyboard-has-appeared-by-using-swiftui-ios
//

import SwiftUI
import Combine

/// The purpose of KeyboardGuardian, is to keep track of keyboard show/hide events and
/// calculate how much space the view needs to be shifted.
final class KeyboardGuardian: ObservableObject {
    let objectWillChange = ObservableObjectPublisher() // PassthroughSubject<Void, Never>()

    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    private var keyboardIsHidden = true

    var slide: CGFloat = 0 {
        didSet {
            objectWillChange.send()
        }
    }

    public var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)

    }

    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            slide = -keyboardRect.size.height
        }
    }
}

Luego, usé una enumeración para rastrear las ranuras en la matriz de rects y el número total:

enum KeyboardSlots: Int {
    case kLogPath
    case kLogThreshold
    case kDisplayClip
    case kPingInterval
    case count
}

KeyboardSlots.count.rawValuees la capacidad necesaria de la matriz; los otros como rawValue dan el índice apropiado que usará para las llamadas .background (GeometryGetter).

Con esa configuración, las vistas llegan al KeyboardGuardian con esto:

@ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: SettingsFormBody.KeyboardSlots.count.rawValue)

El movimiento real es así:

.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1))

adjunto a la vista. En mi caso, está adjunto a todo NavigationView, por lo que el ensamblaje completo se desliza hacia arriba a medida que aparece el teclado.

No he resuelto el problema de obtener una barra de herramientas Listo o una tecla de retorno en un teclado decimal con SwiftUI, así que en su lugar estoy usando esto para ocultarlo en un toque en otro lugar:

struct DismissingKeyboard: ViewModifier {
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                keyWindow?.endEditing(true)                    
        }
    }
}

Lo adjunta a una vista como

.modifier(DismissingKeyboard())

A algunas vistas (por ejemplo, los selectores) no les gusta tener eso adjunto, por lo que es posible que deba ser algo granular en la forma en que adjunta el modificador en lugar de simplemente colocarlo en la vista más externa.

Muchas gracias a @kontiki por el arduo trabajo. Todavía necesitará su GeometryGetter arriba (no, no hice el trabajo de convertirlo para usar preferencias tampoco) como lo ilustra en sus ejemplos.

Feldur
fuente
1
Para la persona que votó negativamente: ¿por qué? Intenté agregar algo útil, así que me gustaría saber cómo, en su opinión, me equivoqué
Feldur
4

Algunas de las soluciones anteriores tenían algunos problemas y no eran necesariamente el enfoque "más limpio". Debido a esto, modifiqué algunas cosas para la implementación a continuación.

extension View {
    func onKeyboard(_ keyboardYOffset: Binding<CGFloat>) -> some View {
        return ModifiedContent(content: self, modifier: KeyboardModifier(keyboardYOffset))
    }
}

struct KeyboardModifier: ViewModifier {
    @Binding var keyboardYOffset: CGFloat
    let keyboardWillAppearPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    let keyboardWillHidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)

    init(_ offset: Binding<CGFloat>) {
        _keyboardYOffset = offset
    }

    func body(content: Content) -> some View {
        return content.offset(x: 0, y: -$keyboardYOffset.wrappedValue)
            .animation(.easeInOut(duration: 0.33))
            .onReceive(keyboardWillAppearPublisher) { notification in
                let keyWindow = UIApplication.shared.connectedScenes
                    .filter { $0.activationState == .foregroundActive }
                    .map { $0 as? UIWindowScene }
                    .compactMap { $0 }
                    .first?.windows
                    .filter { $0.isKeyWindow }
                    .first

                let yOffset = keyWindow?.safeAreaInsets.bottom ?? 0

                let keyboardFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? .zero

                self.$keyboardYOffset.wrappedValue = keyboardFrame.height - yOffset
        }.onReceive(keyboardWillHidePublisher) { _ in
            self.$keyboardYOffset.wrappedValue = 0
        }
    }
}
struct RegisterView: View {
    @State var name = ""
    @State var keyboardYOffset: CGFloat = 0

    var body: some View {

        VStack {
            WelcomeMessageView()
            TextField("Type your name...", text: $name).bordered()
        }.onKeyboard($keyboardYOffset)
            .background(WelcomeBackgroundImage())
            .padding()
    }
}

Me hubiera gustado un enfoque más limpio y trasladar la responsabilidad a la vista construida (no al modificador) sobre cómo compensar el contenido, pero parece que no puedo hacer que los editores activen correctamente al mover el código de compensación a la vista. ...

También tenga en cuenta que los Publishers tuvieron que usarse en esta instancia ya que final classactualmente causa bloqueos de excepción desconocidos (aunque cumple con los requisitos de interfaz) y un ScrollView en general es el mejor enfoque al aplicar código de compensación.

El Arte De Codificación
fuente
Muy buena solución, ¡muy recomendable! Agregué un bool para indicar si el teclado estaba activo actualmente.
Peanutsmasher
4

Muchas de estas respuestas parecen realmente infladas para ser honesto. Si está utilizando SwiftUI, también puede utilizar Combine.

Cree un KeyboardRespondercomo se muestra a continuación, luego puede usarlo como se demostró anteriormente.

Actualizado para iOS 14.

import Combine
import UIKit

final class KeyboardResponder: ObservableObject {

    @Published var keyboardHeight: CGFloat = 0

    init() {
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .compactMap { notification in
                (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height
            }
            .receive(on: DispatchQueue.main)
            .assign(to: \.keyboardHeight)
    }
}


struct ExampleView: View {
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    @State private var text: String = ""

    var body: some View {
        VStack {
            Text(text)
            Spacer()
            TextField("Example", text: $text)
        }
        .padding(.bottom, keyboardResponder.keyboardHeight)
    }
}
Eduardo
fuente
Agregué una .animación (.easeIn) para que coincida con la animación con la que aparece el teclado
Michiel Pelt
(Para iOS 13, vaya al historial de esta respuesta)
Loris Foe
Hola, .assign (to: \ .keyboardHeight) está dando este error "No se puede inferir el tipo de ruta de clave del contexto; considere especificar explícitamente un tipo de raíz". Por favor, avíseme la solución adecuada y limpia tanto para ios 13 como para ios 14.
Shahbaz Sajjad
3

No estoy seguro de si la API de transición / animación para SwiftUI está completa, pero podría usarla CGAffineTransformcon.transformEffect

Cree un objeto de teclado observable con una propiedad publicada como esta:

    final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published var readyToAppear = false

    init(center: NotificationCenter = .default) {
        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        readyToAppear = true
    }

    @objc func keyBoardWillHide(notification: Notification) {
        readyToAppear = false
    }

}

entonces podría usar esa propiedad para reorganizar su vista de esta manera:

    struct ContentView : View {
    @State var textfieldText: String = ""
    @ObservedObject private var keyboard = KeyboardResponder()

    var body: some View {
        return self.buildContent()
    }

    func buildContent() -> some View {
        let mainStack = VStack {
            TextField("TextField1", text: self.$textfieldText)
            TextField("TextField2", text: self.$textfieldText)
            TextField("TextField3", text: self.$textfieldText)
            TextField("TextField4", text: self.$textfieldText)
            TextField("TextField5", text: self.$textfieldText)
            TextField("TextField6", text: self.$textfieldText)
            TextField("TextField7", text: self.$textfieldText)
        }
        return Group{
            if self.keyboard.readyToAppear {
                mainStack.transformEffect(CGAffineTransform(translationX: 0, y: -200))
                    .animation(.spring())
            } else {
                mainStack
            }
        }
    }
}

o más simple

VStack {
        TextField("TextField1", text: self.$textfieldText)
        TextField("TextField2", text: self.$textfieldText)
        TextField("TextField3", text: self.$textfieldText)
        TextField("TextField4", text: self.$textfieldText)
        TextField("TextField5", text: self.$textfieldText)
        TextField("TextField6", text: self.$textfieldText)
        TextField("TextField7", text: self.$textfieldText)
    }.transformEffect(keyboard.readyToAppear ? CGAffineTransform(translationX: 0, y: -50) : .identity)
            .animation(.spring())
blacktiago
fuente
Me encanta esta respuesta, pero no estoy muy seguro de dónde viene 'ScreenSize.portrait'.
Misha Stone
Hola @MishaStone, gracias por elegir mi enfoque. ScreenSize.portrait es una clase que hice para obtener medidas de la base de la pantalla en Orientación y porcentaje ... pero puede reemplazarlo con cualquier valor que necesite para su traducción
blacktiago
3

Xcode 12 beta 4 agrega un nuevo modificador de vista ignoresSafeAreaque ahora puede usar para evitar el teclado.

.ignoresSafeArea([], edges: [])

Esto evita el teclado y todos los bordes del área segura. Puede establecer el primer parámetro en .keyboardsi no desea evitarlo. Hay algunas peculiaridades, al menos en mi configuración de jerarquía de vista, pero parece que esta es la forma en que Apple quiere que evitemos el teclado.

Mark Krenek
fuente
2

Respuesta copiada desde aquí: TextField siempre en la parte superior del teclado con SwiftUI

Probé diferentes enfoques y ninguno funcionó para mí. Este a continuación es el único que funcionó para diferentes dispositivos.

Agregue esta extensión en un archivo:

import SwiftUI
import Combine

extension View {
    func keyboardSensible(_ offsetValue: Binding<CGFloat>) -> some View {
        
        return self
            .padding(.bottom, offsetValue.wrappedValue)
            .animation(.spring())
            .onAppear {
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
                    
                    let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                    
                    let bottom = keyWindow?.safeAreaInsets.bottom ?? 0
                    
                    let value = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
                    let height = value.height
                    
                    offsetValue.wrappedValue = height - bottom
                }
                
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                    offsetValue.wrappedValue = 0
                }
        }
    }
}

En su opinión, necesita una variable para vincular offsetValue:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

  var body: some View { 
    
    VStack {
     //...       
    }
    .keyboardSensible($offsetValue)
  }
}
VSMelo
fuente
2
Solo un FYI, usted es dueño de los objetos cuando llama NotificationCenter.default.addObserver... necesita almacenarlos y eliminar los observadores en el momento apropiado ...
TheCodingArt
Hola @TheCodingArt, así es, he intentado hacer eso así ( oleb.net/blog/2018/01/notificationcenter-removeobserver ) pero no parece funcionar para mí, ¿alguna idea?
Ray
2

Como han señalado Mark Krenek y Heiko, Apple parecía estar abordando este problema por fin en Xcode 12 beta 4. Las cosas se están moviendo rápidamente. Según las notas de la versión de Xcode 12 beta 5 publicadas el 18 de agosto de 2020, "Form, List y TextEditor ya no ocultan el contenido detrás del teclado. (66172025)". Simplemente lo descargué y le di una prueba rápida en el simulador beta 5 (iPhone SE2) con un contenedor de formularios en una aplicación que comencé hace unos días.

Ahora "simplemente funciona" para un TextField . SwiftUI proporcionará automáticamente el acolchado inferior apropiado al formulario encapsulado para dejar espacio para el teclado. Y automáticamente desplazará el formulario hacia arriba para mostrar el campo de texto justo encima del teclado. El contenedor ScrollView ahora también se comporta bien cuando aparece el teclado.

Sin embargo, como señaló Андрей Первушин en un comentario, existe un problema con TextEditor . Beta 5 y 6 proporcionarán automáticamente el acolchado inferior apropiado al formulario encapsulado para dejar espacio para el teclado. Pero NO desplazará automáticamente el formulario hacia arriba. El teclado cubrirá el TextEditor. Entonces, a diferencia de TextField, el usuario tiene que desplazarse por el formulario para hacer visible TextEditor. Presentaré un informe de error. Quizás Beta 7 lo arregle. Tan cerca …

https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-beta-release-notes/

Positrón
fuente
Veo notas de la versión de Apple, probadas en beta5 y beta6, TextField funciona, TextEditor NO, ¿qué me pierdo? @State var text = "" var body: some View {Form {Section {Text (text) .frame (height: 500)} Section {TextField ("5555", text: $ text) .frame (height: 50)} Sección {TextEditor (texto: $ texto) .frame (altura: 120)}}}
Андрей Первушин
2

Uso:

import SwiftUI

var body: some View {
    ScrollView {
        VStack {
          /*
          TextField()
          */
        }
    }.keyboardSpace()
}

Código:

import SwiftUI
import Combine

let keyboardSpaceD = KeyboardSpace()
extension View {
    func keyboardSpace() -> some View {
        modifier(KeyboardSpace.Space(data: keyboardSpaceD))
    }
}

class KeyboardSpace: ObservableObject {
    var sub: AnyCancellable?
    
    @Published var currentHeight: CGFloat = 0
    var heightIn: CGFloat = 0 {
        didSet {
            withAnimation {
                if UIWindow.keyWindow != nil {
                    //fix notification when switching from another app with keyboard
                    self.currentHeight = heightIn
                }
            }
        }
    }
    
    init() {
        subscribeToKeyboardEvents()
    }
    
    private let keyboardWillOpen = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .map { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { $0.height - (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0) }
    
    private let keyboardWillHide =  NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in CGFloat.zero }
    
    private func subscribeToKeyboardEvents() {
        sub?.cancel()
        sub = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
            .subscribe(on: RunLoop.main)
            .assign(to: \.self.heightIn, on: self)
    }
    
    deinit {
        sub?.cancel()
    }
    
    struct Space: ViewModifier {
        @ObservedObject var data: KeyboardSpace
        
        func body(content: Content) -> some View {
            VStack(spacing: 0) {
                content
                
                Rectangle()
                    .foregroundColor(Color(.clear))
                    .frame(height: data.currentHeight)
                    .frame(maxWidth: .greatestFiniteMagnitude)

            }
        }
    }
}

extension UIWindow {
    static var keyWindow: UIWindow? {
        let keyWindow = UIApplication.shared.connectedScenes
            .filter({$0.activationState == .foregroundActive})
            .map({$0 as? UIWindowScene})
            .compactMap({$0})
            .first?.windows
            .filter({$0.isKeyWindow}).first
        return keyWindow
    }
}
8suhas
fuente
probé su solución ... la vista se desplaza solo a la mitad del campo de texto. Probé todo lo anterior la solución. Tengo el mismo problema. ¡¡¡Por favor ayuda!!!
Sona
@Zeona, prueba con una aplicación sencilla, es posible que estés haciendo algo diferente. Además, intente eliminar '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)' si está utilizando un área segura.
8suhas
eliminado (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0) entonces aparece un espacio en blanco sobre el teclado
Sona
1

Manejo TabViewde

Me gusta la respuesta de Benjamin Kindle pero no es compatible con TabViews. Aquí está mi ajuste a su código para manejar TabViews:

  1. Agregue una extensión para UITabViewalmacenar el tamaño de tabView cuando se establece su marco. Podemos almacenar esto en una variable estática porque generalmente solo hay un tabView en un proyecto (si el tuyo tiene más de uno, entonces tendrás que ajustarlo).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. Deberá cambiar la suya onReceiveen la parte inferior de la KeyboardHostvista para tener en cuenta la altura de la barra de pestañas:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. ¡Y eso es! Súper simple 🎉.
Ben Patch
fuente
1

Tomé un enfoque totalmente diferente, extendiendo UIHostingControllery ajustando su additionalSafeAreaInsets:

class MyHostingController<Content: View>: UIHostingController<Content> {
    override init(rootView: Content) {
        super.init(rootView: rootView)
    }

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, 
                                               selector: #selector(keyboardDidShow(_:)), 
                                               name: UIResponder.keyboardDidShowNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self, 
                                               selector: #selector(keyboardWillHide), 
                                               name: UIResponder.keyboardWillHideNotification, 
                                               object: nil)
    }       

    @objc func keyboardDidShow(_ notification: Notification) {
        guard let info:[AnyHashable: Any] = notification.userInfo,
            let frame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
                return
        }

        // set the additionalSafeAreaInsets
        let adjustHeight = frame.height - (self.view.safeAreaInsets.bottom - self.additionalSafeAreaInsets.bottom)
        self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: adjustHeight, right: 0)

        // now try to find a UIResponder inside a ScrollView, and scroll
        // the firstResponder into view
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { 
            if let firstResponder = UIResponder.findFirstResponder() as? UIView,
                let scrollView = firstResponder.parentScrollView() {
                // translate the firstResponder's frame into the scrollView's coordinate system,
                // with a little vertical padding
                let rect = firstResponder.convert(firstResponder.frame, to: scrollView)
                    .insetBy(dx: 0, dy: -15)
                scrollView.scrollRectToVisible(rect, animated: true)
            }
        }
    }

    @objc func keyboardWillHide() {
        self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
}

/// IUResponder extension for finding the current first responder
extension UIResponder {
    private struct StaticFirstResponder {
        static weak var firstResponder: UIResponder?
    }

    /// find the current first responder, or nil
    static func findFirstResponder() -> UIResponder? {
        StaticFirstResponder.firstResponder = nil
        UIApplication.shared.sendAction(
            #selector(UIResponder.trap),
            to: nil, from: nil, for: nil)
        return StaticFirstResponder.firstResponder
    }

    @objc private func trap() {
        StaticFirstResponder.firstResponder = self
    }
}

/// UIView extension for finding the receiver's parent UIScrollView
extension UIView {
    func parentScrollView() -> UIScrollView? {
        if let scrollView = self.superview as? UIScrollView {
            return scrollView
        }

        return superview?.parentScrollView()
    }
}

Luego cambie SceneDelegatepara usar en MyHostingControllerlugar de UIHostingController.

Una vez hecho esto, no necesito preocuparme por el teclado dentro de mi código SwiftUI.

(Nota: ¡todavía no he usado esto lo suficiente para comprender completamente las implicaciones de hacer esto!)

Mateo
fuente
1

Esta es la forma en que manejo el teclado en SwiftUI. Lo que hay que recordar es que está realizando los cálculos en el VStack al que está adjunto.

Lo usa en una Vista como Modificador. De esta manera:

struct LogInView: View {

  var body: some View {
    VStack {
      // Your View
    }
    .modifier(KeyboardModifier())
  }
}

Entonces, para llegar a este modificador, primero, cree una extensión de UIResponder para obtener la posición TextField seleccionada en el VStack:

import UIKit

// MARK: Retrieve TextField first responder for keyboard
extension UIResponder {

  private static weak var currentResponder: UIResponder?

  static var currentFirstResponder: UIResponder? {
    currentResponder = nil
    UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder),
                                    to: nil, from: nil, for: nil)
    return currentResponder
  }

  @objc private func findFirstResponder(_ sender: Any) {
    UIResponder.currentResponder = self
  }

  // Frame of the superview
  var globalFrame: CGRect? {
    guard let view = self as? UIView else { return nil }
    return view.superview?.convert(view.frame, to: nil)
  }
}

Ahora puede crear KeyboardModifier usando Combine para evitar que un teclado oculte un TextField:

import SwiftUI
import Combine

// MARK: Keyboard show/hide VStack offset modifier
struct KeyboardModifier: ViewModifier {

  @State var offset: CGFloat = .zero
  @State var subscription = Set<AnyCancellable>()

  func body(content: Content) -> some View {
    GeometryReader { geometry in
      content
        .padding(.bottom, self.offset)
        .animation(.spring(response: 0.4, dampingFraction: 0.5, blendDuration: 1))
        .onAppear {

          NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .handleEvents(receiveOutput: { _ in self.offset = 0 })
            .sink { _ in }
            .store(in: &self.subscription)

          NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .map(\.userInfo)
            .compactMap { ($0?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.size.height }
            .sink(receiveValue: { keyboardHeight in
              let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
              let textFieldBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
              self.offset = max(0, textFieldBottom - keyboardTop * 2 - geometry.safeAreaInsets.bottom) })
        .store(in: &self.subscription) }
        .onDisappear {
          // Dismiss keyboard
          UIApplication.shared.windows
            .first { $0.isKeyWindow }?
            .endEditing(true)

          self.subscription.removeAll() }
    }
  }
}
Roland Lariotte
fuente
1

En cuanto a iOS 14 (beta 4), funciona bastante simple:

var body: some View {
    VStack {
        TextField(...)
    }
    .padding(.bottom, 0)
}

Y el tamaño de la vista se ajusta a la parte superior del teclado. Ciertamente, hay más refinamientos posibles con frame (.maxHeight: ...), etc. Ya lo resolverá.

Desafortunadamente, el teclado flotante del iPad todavía causa problemas cuando se mueve. Pero las soluciones mencionadas también lo harían, y todavía es beta, espero que lo resuelvan.

¡Gracias Apple, finalmente!

heiko
fuente
Esto no funciona en absoluto (14.1). Cual es la idea?
Burgler-dev
0

Mi vista:

struct AddContactView: View {
    
    @Environment(\.presentationMode) var presentationMode : Binding<PresentationMode>
    
    @ObservedObject var addContactVM = AddContactVM()
    
    @State private var offsetValue: CGFloat = 0.0
    
    @State var firstName : String
    @State var lastName : String
    @State var sipAddress : String
    @State var phoneNumber : String
    @State var emailID : String
    
  
    var body: some View {
        
        
        VStack{
            
            Header(title: StringConstants.ADD_CONTACT) {
                
                self.presentationMode.wrappedValue.dismiss()
            }
            
           ScrollView(Axis.Set.vertical, showsIndicators: false){
            
            Image("contactAvatar")
                .padding(.top, 80)
                .padding(.bottom, 100)
                //.padding(.vertical, 100)
                //.frame(width: 60,height : 60).aspectRatio(1, contentMode: .fit)
            
            VStack(alignment: .center, spacing: 0) {
                
                
                TextFieldBorder(placeHolder: StringConstants.FIRST_NAME, currentText: firstName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.LAST_NAME, currentText: lastName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.SIP_ADDRESS, currentText: sipAddress, imageName: "sipPhone")
                TextFieldBorder(placeHolder: StringConstants.PHONE_NUMBER, currentText: phoneNumber, imageName: "phoneIcon")
                TextFieldBorder(placeHolder: StringConstants.EMAILID, currentText: emailID, imageName: "email")
                

            }
            
           Spacer()
            
        }
        .padding(.horizontal, 20)
        
            
        }
        .padding(.bottom, self.addContactVM.bottomPadding)
        .onAppear {
            
            NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
            
             NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        }
        
    }
}

Mi VM:

class AddContactVM : ObservableObject{
    
    @Published var contact : Contact = Contact(id: "", firstName: "", lastName: "", phoneNumbers: [], isAvatarAvailable: false, avatar: nil, emailID: "")
    
    @Published var bottomPadding : CGFloat = 0.0
    
    @objc  func keyboardWillShow(_ notification : Notification){
        
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardRectangle = keyboardFrame.cgRectValue
            let keyboardHeight = keyboardRectangle.height
            self.bottomPadding = keyboardHeight
        }
        
    }
    
    @objc  func keyboardWillHide(_ notification : Notification){
        
        
        self.bottomPadding = 0.0
        
    }
    
}

Básicamente, administrar el acolchado inferior en función de la altura del teclado.

Tushar Sharma
fuente
-3

La respuesta más elegante que he logrado para esto es similar a la solución de rraphael. Crea una clase para escuchar eventos de teclado. Sin embargo, en lugar de usar el tamaño del teclado para modificar el relleno, devuelva un valor negativo del tamaño del teclado y use el modificador .offset (y :) para ajustar el desplazamiento de los contenedores de vista más externos. Anima bastante bien y funciona con cualquier vista.

pcallycat
fuente
¿Cómo conseguiste que esto se animara? Sí .offset(y: withAnimation { -keyboard.currentHeight }), pero el contenido salta en lugar de animarse.
jjatie
Hace algunas betas hice un error con este código, pero en el momento de mi comentario anterior, todo lo que se requería era modificar el desplazamiento de un vstack durante el tiempo de ejecución, SwiftUI animaba el cambio por usted.
pcallycat