Eu tenho sete TextField
dentro do meu principal ContentView
. Quando o usuário abre o teclado, alguns TextField
deles ficam ocultos sob a moldura do teclado. Portanto, quero mover tudo para TextField
cima, respectivamente, quando o teclado aparecer.
Usei o código abaixo para adicionar TextField
na tela.
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"))
}
}
}
Resultado:
Respostas:
Código atualizado para o Xcode, beta 7.
Você não precisa de preenchimento, ScrollViews ou Lists para fazer isso. Embora esta solução funcione bem com eles também. Estou incluindo dois exemplos aqui.
O primeiro move tudo textField para cima, se o teclado aparecer para algum deles. Mas apenas se necessário. Se o teclado não ocultar os campos de texto, eles não se moverão.
No segundo exemplo, a visualização se move apenas o suficiente para evitar ocultar o campo de texto ativo.
Ambos os exemplos usam o mesmo código comum encontrado no final: GeometryGetter e KeyboardGuardian
Primeiro exemplo (mostrar todos os campos de texto)
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 exemplo (mostrar apenas o campo ativo)
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() } }
GeometryGetter
Esta é uma visualização que absorve o tamanho e a posição de sua visualização pai. Para conseguir isso, ele é chamado dentro do modificador .background. Este é um modificador muito poderoso, não apenas uma forma de decorar o plano de fundo de uma visualização. Ao passar uma visualização para .background (MyView ()), MyView está obtendo a visualização modificada como pai. Usar GeometryReader é o que possibilita que a vista conheça a geometria do pai.
Por exemplo:
Text("hello").background(GeometryGetter(rect: $bounds))
irá preencher os limites das variáveis, com o tamanho e a posição da visualização do Texto, e usando o espaço 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) } } } }
Atualizar Eu adicionei o DispatchQueue.main.async, para evitar a possibilidade de modificar o estado da visualização enquanto ela está sendo renderizada. ***
KeyboardGuardian
O objetivo do KeyboardGuardian é acompanhar os eventos de exibição / ocultação do teclado e calcular quanto espaço a visualização precisa ser deslocada.
Atualizar: modifiquei o KeyboardGuardian para atualizar o slide, quando o usuário passa de um campo para outro
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) } } } }
fonte
GeometryGetter
como um modificador de visualização do que um plano de fundo, tornando-o conforme oViewModifier
protocolo?.modifier(GeometryGetter(rect: $kGuardian.rects[1]))
vez de.background(GeometryGetter(rect: $kGuardian.rects[1]))
. Não há muita diferença (apenas 2 caracteres a menos).geometry.frame
sairDispatchQueue.main.async
ajudou com o SIGNAL ABORT, agora vou testar a sua solução. Atualização:if geometry.size.width > 0 && geometry.size.height > 0
antes de atribuirself.rect
ajudado.Para construir a partir da solução de @rraphael, eu a converti para ser utilizável pelo suporte xcode11 swiftUI de hoje.
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)) } }
O publicado
currentHeight
irá acionar uma nova renderização da IU e mover seu TextField para cima quando o teclado mostrar, e para baixo quando for dispensado. No entanto, não usei um ScrollView.fonte
.animation(.easeOut(duration: 0.16))
para tentar igualar a velocidade do teclado deslizando para cima.keyboardFrameEndUserInfoKey
. Isso deve conter o quadro final do teclado.Tentei muitas das soluções propostas e, embora funcionem na maioria dos casos, tive alguns problemas - principalmente com a área segura (tenho um Form dentro da aba TabView).
Acabei combinando algumas soluções diferentes e usando o GeometryReader para obter a inserção inferior da área segura da visualização específica e usá-la no cálculo do preenchimento:
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()) } }
fonte
Thread 1: signal SIGABRT
on-linerect.height - geometry.safeAreaInsets.bottom
quando vou para a visualização com o teclado uma segunda vez e clico emTextField
. Não importa se clicoTextField
na primeira vez ou não. O aplicativo ainda trava.Eu criei uma visão que pode envolver qualquer outra visão para reduzi-la quando o teclado aparecer.
É muito simples. Criamos editores para eventos de exibição / ocultação de teclado e, em seguida, os assinamos usando
onReceive
. Usamos o resultado disso para criar um retângulo do tamanho de um teclado atrás do 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 } } }
Você pode então usar a visualização da seguinte forma:
var body: some View { KeyboardHost { viewIncludingKeyboard() } }
Para mover o conteúdo da visualização para cima em vez de reduzi-lo, pode-se adicionar preenchimento ou deslocamento em
view
vez de colocá-lo em um VStack com um retângulo.fonte
self.view
e funciona muito bem. Sem problemas com a animaçãovar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
Eu criei um modificador de exibição realmente simples de usar.
Adicione um arquivo Swift com o código abaixo e simplesmente adicione este modificador às suas visualizações:
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()) } }
fonte
Xcode 12 - Código de uma linha
Adicione este modificador ao
TextField
A Apple adicionou o teclado como uma região para a área segura, então você pode usá-lo para mover qualquer coisa
View
com o teclado como outras regiões.fonte
View
, incluindo oTextEditor
..ignoresSafeArea(.keyboard)
à sua visualização.Ou você pode apenas usar IQKeyBoardManagerSwift
e pode opcionalmente adicionar isso ao seu aplicativo delegado para ocultar a barra de ferramentas e ativar a ocultação do teclado ao clicar em qualquer visualização diferente do teclado.
IQKeyboardManager.shared.enableAutoToolbar = false IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false IQKeyboardManager.shared.shouldResignOnTouchOutside = true IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide
fonte
IQKeyboardManager.shared.keyboardDistanceFromTextField
para 40 para obter um intervalo confortável.IQKeyboardManager.shared.enable = true
para evitar que o teclado escondesse meus campos de texto. Em qualquer caso, esta é a melhor solução. Eu tenho 4 campos dispostos verticalmente e as outras soluções funcionariam para o meu campo mais inferior, mas empurrariam o mais superior para fora da vista.Você precisa adicionar um
ScrollView
e definir um preenchimento inferior do tamanho do teclado para que o conteúdo possa rolar quando o teclado aparecer.Para obter o tamanho do teclado, você precisará usar o
NotificationCenter
para se registrar no evento de teclados. Você pode usar uma classe personalizada para fazer isso: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 } }
A
BindableObject
conformidade permitirá que você use essa classe como umaState
e acione a atualização da visualização. Se necessário, consulte o tutorial paraBindableObject
: Tutorial do SwiftUIAo conseguir isso, você precisa configurar um
ScrollView
para reduzir seu tamanho quando o teclado aparecer. Por conveniência, envolvi issoScrollView
em algum 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) } }
Tudo que você precisa fazer agora é incorporar seu conteúdo dentro do custom
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) } }
Edit: O comportamento de rolagem é realmente estranho quando o teclado está escondido. Talvez o uso de uma animação para atualizar o preenchimento conserte isso, ou você deve considerar o uso de algo diferente do
padding
para ajustar o tamanho da visualização da rolagem.fonte
BindableObject
uso suspenso, isso não está mais funcionando, infelizmente.BindableObject
foi apenas renomeado paraObservableObject
, edidChange
paraobjectWillChange
. O objeto atualiza a visualização muito bem (embora eu tenha testado usando em@ObservedObject
vez de@State
)Eu revisei e refatorei as soluções existentes em um pacote SPM útil que fornece um
.keyboardAware()
modificador:KeyboardAwareSwiftUI
Exemplo:
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") } } }
Fonte:
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()) } }
fonte
Usei a resposta de Benjamin Kindle como ponto de partida, mas tive alguns problemas que queria resolver.
keyboardWillChangeFrameNotification
à lista de notificações processadas resolve isso.init
função que aceita um@ViewBuilder
para que você possa usar aKeyboardHost
visualização como qualquer outra visualização e simplesmente passar seu conteúdo em um encerramento final, em vez de passar a visualização do conteúdo como um parâmetro parainit
.Rectangle
para ajustar o enchimento inferior.UIWindow
comoUIWindow.keyboardFrameEndUserInfoKey
.Juntando tudo isso, tenho:
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")) } } }
fonte
Keyboard
alturaPridictive
no iOSkeyboard
.Settings
->General
->Keyboard
->Pridictive
. neste caso ele não corrige calclate e adiciona preenchimento ao tecladokeyboardHeight
. No meu iPod Touch (no retrato), um teclado com preditivo ligado tem 254 pontos. Sem ele, são 216 pontos. Posso até desligar o preditivo com um teclado na tela e as atualizações de preenchimento corretamente. Adicionando um teclado com previsão:Received UIKeyboardWillChangeFrameNotification with height 254.0
Received UIKeyboardWillShowNotification with height 254.0
Quando eu desligo a previsão de texto:Received UIKeyboardWillChangeFrameNotification with height 216.0
Muitas dessas respostas parecem muito inchadas para ser honesto. Se você estiver usando o SwiftUI, você também pode usar o Combine.
Criar uma
KeyboardResponder
como mostrado abaixo, então você pode usar conforme demonstrado anteriormente.Atualizado 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) } }
fonte
Isto é adaptado do que @kontiki construiu. Eu o tenho em execução em um aplicativo em beta 8 / GM seed, onde o campo que precisa ser rolado faz parte de um formulário dentro de um NavigationView. Aqui está o 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 } } }
Em seguida, usei um enum para rastrear os slots na matriz rects e o número total:
enum KeyboardSlots: Int { case kLogPath case kLogThreshold case kDisplayClip case kPingInterval case count }
KeyboardSlots.count.rawValue
é a capacidade necessária do array; os outros como rawValue fornecem o índice apropriado que você usará para chamadas .background (GeometryGetter).Com essa configuração, as visualizações chegam ao KeyboardGuardian com o seguinte:
@ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: SettingsFormBody.KeyboardSlots.count.rawValue)
O movimento real é assim:
.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1))
anexado à vista. No meu caso, ele é anexado a toda NavigationView, de modo que a montagem completa desliza conforme o teclado aparece.
Eu não resolvi o problema de obter uma barra de ferramentas Concluída ou uma tecla de retorno em um teclado decimal com SwiftUI, então, em vez disso, estou usando isso para ocultá-lo em um toque em outro 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) } } }
Você o anexa a uma visualização como
.modifier(DismissingKeyboard())
Algumas visualizações (por exemplo, selecionadores) não gostam de ter isso anexado, então você pode precisar ser um pouco granular em como anexar o modificador ao invés de apenas colocá-lo na visão externa.
Muito obrigado a @kontiki pelo trabalho árduo. Você ainda precisará de seu GeometryGetter acima (não, também não fiz o trabalho de convertê-lo para usar preferências), como ele ilustra em seus exemplos.
fonte
Algumas das soluções acima apresentavam alguns problemas e não eram necessariamente a abordagem "mais limpa". Por causa disso, modifiquei algumas coisas para a implementação abaixo.
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() } }
Eu teria gostado de uma abordagem mais limpa e de transferir a responsabilidade para a visualização construída (não o modificador) sobre como compensar o conteúdo, mas parece que não consegui fazer com que os editores acionassem corretamente ao mover o código de deslocamento para a visualização. ...
Observe também que Publishers teve que ser usado nesta instância, pois
final class
atualmente causa travamentos de exceção desconhecida (embora atenda aos requisitos de interface) e um ScrollView geral é a melhor abordagem ao aplicar o código de deslocamento.fonte
Não tenho certeza se a API de transição / animação para SwiftUI está completa, mas você pode usar
CGAffineTransform
com.transformEffect
Crie um objeto de teclado observável com uma propriedade 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 } }
então você pode usar essa propriedade para reorganizar sua visualização da seguinte maneira:
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 } } } }
ou mais simples
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())
fonte
O Xcode 12 beta 4 adiciona um novo modificador de visualização
ignoresSafeArea
que agora você pode usar para evitar o teclado..ignoresSafeArea([], edges: [])
Isso evita o teclado e todas as bordas da área de segurança. Você pode definir o primeiro parâmetro como
.keyboard
se não quiser que seja evitado. Existem algumas peculiaridades, pelo menos na minha configuração de hierarquia de visão, mas parece que é assim que a Apple quer que evitemos o teclado.fonte
Resposta copiada daqui: TextField sempre na parte superior do teclado com SwiftUI
Tentei abordagens diferentes e nenhuma delas funcionou para mim. Este abaixo é o único que funcionou para dispositivos diferentes.
Adicione esta extensão em um arquivo:
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 } } } }
Em sua opinião, você precisa de uma variável para vincular offsetValue:
struct IncomeView: View { @State private var offsetValue: CGFloat = 0.0 var body: some View { VStack { //... } .keyboardSensible($offsetValue) } }
fonte
NotificationCenter.default.addObserver
... você precisa armazená-los e remover os observadores em um momento apropriado ...Como Mark Krenek e Heiko apontaram, a Apple parecia estar tratando desse problema há muito tempo no Xcode 12 beta 4. As coisas estão acontecendo rapidamente. De acordo com as notas de lançamento do Xcode 12 beta 5 publicado em 18 de agosto de 2020 "Form, List e TextEditor não escondem mais o conteúdo atrás do teclado. (66172025)". Acabei de fazer o download e fiz um teste rápido no simulador beta 5 (iPhone SE2) com um contêiner Form em um aplicativo que comecei há alguns dias.
Agora "simplesmente funciona" para um TextField . O SwiftUI fornecerá automaticamente o preenchimento inferior apropriado para o Formulário de encapsulamento para abrir espaço para o teclado. E irá rolar automaticamente o Form para cima para exibir o TextField logo acima do teclado. O contêiner ScrollView agora se comporta bem quando o teclado também é ativado.
No entanto, como Андрей Первушин apontou em um comentário, há um problema com o TextEditor . As versões Beta 5 e 6 fornecerão automaticamente o preenchimento inferior apropriado para o Formulário de encapsulamento para abrir espaço para o teclado. Mas NÃO rolará automaticamente o formulário para cima. O teclado cobrirá o TextEditor. Portanto, ao contrário de TextField, o usuário precisa rolar o Form para tornar o TextEditor visível. Vou registrar um relatório de bug. Talvez o Beta 7 resolva isso. Tão perto …
https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-beta-release-notes/
fonte
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 } }
fonte
Tratamento
TabView
deEu gosto da resposta de Benjamin Kindle, mas ele não oferece suporte a TabViews. Aqui está meu ajuste em seu código para lidar com TabViews:
UITabView
para armazenar o tamanho de tabView quando seu quadro for definido. Podemos armazenar isso em uma variável estática porque geralmente há apenas um tabView em um projeto (se o seu tiver mais de um, você precisará ajustar).extension UITabBar { static var size: CGSize = .zero open override var frame: CGRect { get { super.frame } set { UITabBar.size = newValue.size super.frame = newValue } } }
onReceive
na parte inferior daKeyboardHost
visualização para considerar a altura da Barra de guias:.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = max(height - UITabBar.size.height, 0) }
fonte
Adotei uma abordagem totalmente diferente, estendendo
UIHostingController
e ajustandoadditionalSafeAreaInsets
: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() } }
Em seguida, mude
SceneDelegate
para usar emMyHostingController
vez deUIHostingController
.Quando isso for feito, não preciso me preocupar com o teclado dentro do meu código SwiftUI.
(Observação: ainda não usei isso o suficiente para entender totalmente as implicações de fazer isso!)
fonte
Esta é a maneira como manuseio o teclado no SwiftUI. É importante lembrar que ele está fazendo os cálculos no VStack ao qual está anexado.
Você o usa em uma Visualização como um Modificador. Deste jeito:
struct LogInView: View { var body: some View { VStack { // Your View } .modifier(KeyboardModifier()) } }
Então, para chegar a esse modificador, primeiro crie uma extensão de UIResponder para obter a posição TextField selecionada no 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) } }
Agora você pode criar o KeyboardModifier usando Combine para evitar que um teclado oculte um 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() } } } }
fonte
Quanto ao iOS 14 (beta 4), funciona de forma bastante simples:
var body: some View { VStack { TextField(...) } .padding(.bottom, 0) }
E o tamanho da visualização se ajusta à parte superior do teclado. Certamente, existem mais refinamentos possíveis com o frame (.maxHeight: ...) etc. Você vai descobrir.
Infelizmente, o teclado flutuante do iPad ainda causa problemas quando movido. Mas as soluções mencionadas acima também, e ainda é beta, espero que eles descubram.
Thx Apple, finalmente!
fonte
Minha visão:
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) } } }
Minha 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 } }
Basicamente, Gerenciando o preenchimento inferior com base na altura do teclado.
fonte
A resposta mais elegante que consegui para isso é semelhante à solução de rraphael. Crie uma classe para ouvir eventos de teclado. Em vez de usar o tamanho do teclado para modificar o preenchimento, retorne um valor negativo do tamanho do teclado e use o modificador .offset (y :) para ajustar o deslocamento dos contêineres de visualização mais externos. Ele anima bem o suficiente e funciona com qualquer visualização.
fonte
.offset(y: withAnimation { -keyboard.currentHeight })
, mas o conteúdo salta em vez de ser animado.