Mova TextField para cima quando o teclado aparecer no SwiftUI

100

Eu tenho sete TextFielddentro do meu principal ContentView. Quando o usuário abre o teclado, alguns TextFielddeles ficam ocultos sob a moldura do teclado. Portanto, quero mover tudo para TextFieldcima, respectivamente, quando o teclado aparecer.

Usei o código abaixo para adicionar TextFieldna 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:

Resultado

Hitesh Surani
fonte
Você pode usar ScrollView. developer.apple.com/documentation/swiftui/scrollview
Prashant Tukadiya
1
@PrashantTukadiya Obrigado pela resposta rápida. Eu adicionei TextField dentro de Scrollview, mas ainda enfrentando o mesmo problema.
Hitesh Surani
1
@DimaPaliychuk Isso não funcionará. é SwiftUI
Prashant Tukadiya
28
A exibição do teclado e seu conteúdo obscurecendo na tela existem desde o primeiro aplicativo Objective C para iPhone? Esse é um problema que está constantemente sendo resolvido. Eu, pelo menos, estou desapontado com o fato de a Apple não ter resolvido isso com o SwiftUi. Sei que esse comentário não é útil para ninguém, mas queria levantar a questão de que realmente deveríamos pressionar a Apple para fornecer uma solução e não depender da comunidade para sempre fornecer esse problema mais comum.
P. Ent
3
Há um artigo muito bom de Vadim vadimbulavin.com/…
Sudara

Respostas:

64

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)

Quando o teclado é aberto, os 3 campos de texto são movidos para cima o suficiente para manter todos visíveis

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)

Quando cada campo de texto é clicado, a visualização só é movida para cima o suficiente para tornar o campo de texto clicado visível.

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)
            }

        }
    }
}
kontiki
fonte
1
É possível anexar GeometryGettercomo um modificador de visualização do que um plano de fundo, tornando-o conforme o ViewModifierprotocolo?
Sudara
2
É possível, mas qual é o ganho? Você o anexaria assim: em .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).
kontiki
3
Em algumas situações, você poderia obter um SIGNAL ABORT do programa dentro do GeometryGetter ao atribuir o novo retângulo, se você estiver navegando para fora desta tela. Se isso acontecer com você, basta adicionar algum código para verificar se o tamanho da geometria é maior que zero (geometry.size.width> 0 && geometry.size.height> 0) antes de atribuir um valor a self.rect
Julio Bailon
1
@JulioBailon Não sei porque mas ao geometry.framesair DispatchQueue.main.asyncajudou com o SIGNAL ABORT, agora vou testar a sua solução. Atualização: if geometry.size.width > 0 && geometry.size.height > 0antes de atribuir self.rectajudado.
Roman Vasilyev
2
isso quebra para mim também em self.rect = geometry.frame (em: .global) obtendo SIGNAL ABORT e tentei todas as soluções propostas para resolver esse erro
Marwan Roushdy
55

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 currentHeightirá 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.

Michael Neas
fonte
6
Gosto dessa resposta por sua simplicidade. Eu adicionei .animation(.easeOut(duration: 0.16))para tentar igualar a velocidade do teclado deslizando para cima.
Mark Moeykens
Por que você definiu uma altura máxima de 340 para o teclado?
Daniel Ryan
1
@DanielRyan Às vezes, a altura do teclado estava retornando valores incorretos no simulador. Não consigo descobrir uma maneira de definir o problema atualmente
Michael Neas
1
Eu não vi esse problema sozinho. Talvez esteja corrigido nas versões mais recentes. Eu não queria bloquear o tamanho no caso de haver (ou haver) teclados maiores.
Daniel Ryan
1
Você pode tentar keyboardFrameEndUserInfoKey. Isso deve conter o quadro final do teclado.
Mathias Claassen
51

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())
    }
}
Predrag Samardzic
fonte
7
Uau, essa é a versão mais SwiftUI de todas, com GeometryReader e ViewModifier. Adoro.
Mateusz
3
Isso é tão útil e elegante. Muito obrigado por escrever isso.
Danilo Campos
2
Estou vendo uma pequena tela em branco sobre meu teclado. Esta visualização é GeometryReader View, eu confirmei mudando a cor de fundo. Alguma ideia de por que GeometryReader está aparecendo entre minha visão real e o teclado.
user832
6
Estou recebendo o erro Thread 1: signal SIGABRTon-line rect.height - geometry.safeAreaInsets.bottomquando vou para a visualização com o teclado uma segunda vez e clico em TextField. Não importa se clico TextFieldna primeira vez ou não. O aplicativo ainda trava.
JLively
2
Finalmente algo que funcione! Por que a Apple não quer fazer isso para nós é loucura!
Dave Kozikowski
35

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 viewvez de colocá-lo em um VStack com um retângulo.

Benjamin Kindle
fonte
6
Acho que essa é a resposta certa. Fiz apenas um pequeno ajuste: em vez de um retângulo, estou apenas modificando o preenchimento self.viewe funciona muito bem. Sem problemas com a animação
Tae
5
Obrigado! Funciona perfeitamente. Como disse @Taed, é melhor usar uma abordagem de preenchimento. O resultado final seriavar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente
1
Apesar de votos menores, esta é a resposta mais rápida. E a abordagem anterior usando AnyView, quebra a ajuda de aceleração de metal.
Nelson Cardaci
4
É uma ótima solução, mas o principal problema aqui é que você perde a capacidade de mover para cima a visualização apenas se o teclado estiver ocultando o campo de texto que você está editando. Quer dizer: se você tiver um formulário com vários campos de texto e começar a editar o primeiro no topo, provavelmente não vai querer que ele se mova para cima porque sairia da tela.
matteopuc
Eu realmente gosto da resposta, mas como todas as outras respostas, ela não funciona se sua visualização estiver dentro de uma TabBar ou se a visualização não estiver alinhada com a parte inferior da tela.
Ben Patch
25

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:

.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
fonte
2
Seria legal, se fosse apenas compensado, se necessário (ou seja, não role, se o teclado não cobrir o elemento de entrada). Bom ter ...
décadas
Isso funciona muito bem, obrigado. Implementação muito limpa também, e para mim, só rola se necessário.
Joshua
Impressionante! Por que você não fornece isso no Github ou em outro lugar? :) Ou você pode sugerir isso para github.com/hackiftekhar/IQKeyboardManager, pois eles ainda não têm um suporte completo para SwiftUI
Schnodderbalken
Não vai jogar bem com mudanças de orientação e irá compensar independentemente de ser necessário ou não.
GrandSteph
Um problema aqui é que isso não está animando nada ... cria um movimento muito nervoso j
Zorayr
22

Xcode 12 - Código de uma linha

Adicione este modificador ao TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Demo

A Apple adicionou o teclado como uma região para a área segura, então você pode usá-lo para mover qualquer coisaView com o teclado como outras regiões.

Mojtaba Hosseini
fonte
Funciona em Any View , incluindo o TextEditor.
Mojtaba Hosseini
@MojtabaHosseini e se eu quiser evitar esse comportamento? Tenho uma imagem que agora está sendo movida para cima quando abro o teclado, que não quero mover.
kyrers
1
Você pode fazer uma pergunta e vinculá-la aqui. Então, posso dar uma olhada em seu código reproduzível e ver se posso ajudar :) @kyrers
Mojtaba Hosseini
2
Eu descobri sozinho. Adicione .ignoresSafeArea(.keyboard)à sua visualização.
leonboe1
3
infelizmente isso é apenas iOS 14 ...
Xaxxus
16

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
Amit Samant
fonte
Este é realmente o caminho (inesperado) para mim também. Sólido.
HelloTimo
Essa estrutura funcionou ainda melhor do que o esperado. Obrigado por compartilhar!
Richard Poutier
1
Funcionando bem para mim no SwiftUI - obrigado @DominatorVbN - no modo paisagem do iPad, precisei aumentar IQKeyboardManager.shared.keyboardDistanceFromTextFieldpara 40 para obter um intervalo confortável.
Richard Groves
Também tive que definir IQKeyboardManager.shared.enable = truepara 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.
MisterEd
12

Você precisa adicionar um ScrollViewe 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 NotificationCenterpara 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 BindableObjectconformidade permitirá que você use essa classe como uma Statee acione a atualização da visualização. Se necessário, consulte o tutorial para BindableObject: Tutorial do SwiftUI

Ao conseguir isso, você precisa configurar um ScrollViewpara reduzir seu tamanho quando o teclado aparecer. Por conveniência, envolvi isso ScrollViewem 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 paddingpara ajustar o tamanho da visualização da rolagem.

rraphael
fonte
Ei, parece que você tem experiência em objetos vinculáveis. Não consigo fazer funcionar como desejo. Seria bom se você pudesse consultar: stackoverflow.com/questions/56500147/…
SwiftiSwift
Por que você não está usando @ObjectBinding
SwiftiSwift
3
Com o BindableObjectuso suspenso, isso não está mais funcionando, infelizmente.
LinusGeffarth
2
@LinusGeffarth Por que vale a pena, BindableObjectfoi apenas renomeado para ObservableObject, e didChangepara objectWillChange. O objeto atualiza a visualização muito bem (embora eu tenha testado usando em @ObservedObjectvez de @State)
SeizeTheDay
Olá, esta solução está rolando o conteúdo, mas mostra uma área em branco acima do teclado que esconde metade do campo de texto. Por favor, deixe-me saber como podemos remover a área branca.
Shahbaz Sajjad
6

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())
    }
}
Ralf Ebert
fonte
Acabei de ver a metade da altura do textview. você sabe como resolver isso?
Matrosov Alexander
Boa. isso economizou meu tempo. Antes de usar isso, sabemos o preenchimento inferior da visualização do identificador deste modificador.
Brownsoo Han,
5

Usei a resposta de Benjamin Kindle como ponto de partida, mas tive alguns problemas que queria resolver.

  1. A maioria das respostas aqui não lida com a mudança de moldura do teclado, portanto, elas são interrompidas se o usuário girar o dispositivo com o teclado na tela. Adicionar keyboardWillChangeFrameNotificationà lista de notificações processadas resolve isso.
  2. Eu não queria vários editores com fechamentos de mapa semelhantes, mas diferentes, então acorrentei todas as três notificações de teclado em um único editor. É reconhecidamente uma cadeia longa, mas cada etapa é bastante direta.
  3. Eu forneci a initfunção que aceita um @ViewBuilderpara que você possa usar a KeyboardHostvisualizaçã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 para init.
  4. Como Tae e fdelafuente sugeriram nos comentários, troquei o Rectanglepara ajustar o enchimento inferior.
  5. Em vez de usar a string "UIKeyboardFrameEndUserInfoKey" codificada, eu queria usar as strings fornecidas UIWindowcomo UIWindow.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"))
        }
    }
}

Timothy Sanders
fonte
esta solução não funciona, ela aumenta a Keyboardaltura
GSerjo
Você pode explicar os problemas que está vendo @GSerjo? Estou usando este código em meu aplicativo e está funcionando bem para mim.
Timothy Sanders
Você poderia ligar Pridictiveno iOS keyboard. Settings-> General-> Keyboard-> Pridictive. neste caso ele não corrige calclate e adiciona preenchimento ao teclado
GSerjo
@GSerjo: Eu tenho texto preditivo habilitado em um iPad Touch (7ª geração) executando o iOS 13.1 beta. Ele adiciona preenchimento corretamente para a altura da linha de previsão. (Importante observar, não estou ajustando a altura do teclado aqui, estou adicionando ao preenchimento da própria visualização.) Tente trocar no mapa de depuração que está comentado e brinque com os valores que você obtém para o prognóstico teclado. Vou postar um log em outro comentário.
Timothy Sanders
Com o mapa de "depuração" descomentado, você pode ver o valor que está sendo atribuído keyboardHeight. 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
Timothy Sanders
5

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)
    }
}
Edward
fonte
Eu adicionei um .animation (.easeIn) para combinar com a animação com a qual o teclado aparece
Michiel Pelt
(Para iOS 13, vá para o histórico desta resposta)
Loris Foe
1
Olá, .assign (to: \ .keyboardHeight) está apresentando este erro "Não é possível inferir o tipo de caminho de chave do contexto; considere especificar explicitamente um tipo de raiz". Informe-me sobre a solução adequada e limpa para iOS 13 e iOS 14.
Shahbaz Sajjad
Tive que adicionar outro ouvinte para UIResponder.keyboardWillHideNotification. Fora isso - esta é a única solução que funcionou para mim. Obrigado!
Antonín Karásek
4

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.

Feldur
fonte
1
Para o indivíduo que votou contra: por quê? Tentei adicionar algo útil, então gostaria de saber como, na sua opinião, errei
Feldur
4

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

TheCodingArt
fonte
Solução muito boa, recomendo! Eu adicionei um Bool para indicar se o teclado estava ativo no momento.
Peanutsmasher de
3

Não tenho certeza se a API de transição / animação para SwiftUI está completa, mas você pode usar CGAffineTransformcom.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())
Blacktiago
fonte
Adorei essa resposta, mas não tenho certeza de onde 'ScreenSize.portrait' está vindo.
Misha Stone
Olá @MishaStone, obrigado por escolher minha abordagem. ScreenSize.portrait é uma aula que fiz para obter medidas de tela com base em Orientação e porcentagem .... mas você pode substituí-la por qualquer valor que desejar para sua tradução
blacktiago 15/11/19
3

O Xcode 12 beta 4 adiciona um novo modificador de visualização ignoresSafeAreaque 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 .keyboardse 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.

Mark Krenek
fonte
2

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)
  }
}
VSMelo
fonte
2
Apenas para sua informação, você possui os objetos ao chamar NotificationCenter.default.addObserver... você precisa armazená-los e remover os observadores em um momento apropriado ...
TheCodingArt
Olá @TheCodingArt, é verdade, tentei fazer assim ( oleb.net/blog/2018/01/notificationcenter-removeobserver ) mas não parece funcionar para mim, alguma ideia?
Ray
2

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/

Positron
fonte
vejo notas de lançamento da apple, testadas em beta5 e beta6, TextField funciona, TextEditor NÃO, o que eu perdi? @State var text = "" var body: some Ver {Formulário {Section {Text (text) .frame (height: 500)} Section {TextField ("5555", text: $ text) .frame (height: 50)} Seção {TextEditor (text: $ text) .frame (height: 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
fonte
tentei sua solução ... a visualização é rolada apenas para a metade do campo de texto. Tentei todas as soluções acima. Tenho o mesmo problema. Por favor ajude!!!
Sona
@Zeona, tente em um aplicativo simples, você pode estar fazendo algo diferente. Além disso, tente remover '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)' se você estiver usando uma área segura.
8suhas de
removido (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0), estou recebendo um espaço em branco acima do teclado
Sona
1

Tratamento TabViewde

Eu 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:

  1. Adicione uma extensão para UITabViewpara 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
        }
    }
}
  1. Você precisará alterar o dele onReceivena parte inferior da KeyboardHostvisualização para considerar a altura da Barra de guias:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. E é isso! Super simples 🎉.
Ben Patch
fonte
1

Adotei uma abordagem totalmente diferente, estendendo UIHostingControllere ajustando 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()
    }
}

Em seguida, mude SceneDelegatepara usar em MyHostingControllervez de UIHostingController.

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!)

Mateus
fonte
1

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() }
    }
  }
}
Roland Lariotte
fonte
1

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!

heiko
fonte
Isso não funciona de jeito nenhum (14.1). Qual é a ideia?
Burgler-dev
0

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.

Tushar Sharma
fonte
-3

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.

pcallycat
fonte
Como você conseguiu animar isso? Sim .offset(y: withAnimation { -keyboard.currentHeight }), mas o conteúdo salta em vez de ser animado.
jjatie
Alguns betas atrás eu muck com este código, mas no momento do meu comentário anterior, modificar o deslocamento de um vstack durante o tempo de execução era tudo o que era necessário, o SwiftUI animaria a mudança para você.
pcallycat