J'ai sept TextField
dans mon ContentView
principal. Lorsque l'utilisateur ouvre le clavier, certains des TextField
sont masqués sous le cadre du clavier. Je veux donc déplacer tous les TextField
vers le haut respectivement lorsque le clavier est apparu.
J'ai utilisé le code ci-dessous pour ajouter TextField
à l'écran.
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"))
}
}
}
Sortie:
J'ai adopté une approche totalement différente, en étendant UIHostingController
et en ajustant son 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()
}
}
Modifiez ensuite SceneDelegate
pour utiliser MyHostingController
au lieu de UIHostingController
.
Une fois cela fait, je n'ai pas à me soucier du clavier dans mon code SwiftUI.
(Remarque: je ne l'ai pas encore suffisamment utilisé pour comprendre pleinement les implications de cette opération!)
La réponse la plus élégante à laquelle j'ai réussi est similaire à la solution de Rraphael. Créez une classe pour écouter les événements du clavier. Au lieu d'utiliser la taille du clavier pour modifier le remplissage, renvoyez une valeur négative de la taille du clavier et utilisez le modificateur .offset (y :) pour régler le décalage du conteneur le plus extérieur. Il s'anime assez bien et fonctionne avec n'importe quelle vue.
C'est ainsi que je gère le clavier dans SwiftUI. La chose à retenir est qu'il fait les calculs sur le VStack auquel il est attaché.
Vous l'utilisez sur une vue comme modificateur. Par ici:
struct LogInView: View {
var body: some View {
VStack {
// Your View
}
.modifier(KeyboardModifier())
}
}
Donc, pour en venir à ce modificateur, créez d'abord une extension de UIResponder pour obtenir la position TextField sélectionnée dans le 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)
}
}
Vous pouvez maintenant créer le KeyboardModifier en utilisant Combine pour éviter qu'un clavier ne masque un TextField:
import SwiftUI
import Combine
// MARK: Keyboard show/hide VStack offset modifier
struct KeyboardModifier: ViewModifier {
@State var offset: CGFloat = .zero
@State var subscription = Set<AnyCancellable>()
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, self.offset)
.animation(.spring(response: 0.4, dampingFraction: 0.5, blendDuration: 1))
.onAppear {
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.handleEvents(receiveOutput: { _ in self.offset = 0 })
.sink { _ in }
.store(in: &self.subscription)
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
.map(\.userInfo)
.compactMap { ($0?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.size.height }
.sink(receiveValue: { keyboardHeight in
let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
let textFieldBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
self.offset = max(0, textFieldBottom - keyboardTop * 2 - geometry.safeAreaInsets.bottom) })
.store(in: &self.subscription) }
.onDisappear {
// Dismiss keyboard
UIApplication.shared.windows
.first { $0.isKeyWindow }?
.endEditing(true)
self.subscription.removeAll() }
}
}
}