メインTextField
の中に7つのContentView
があります。ユーザーがキーボードを開くと、TextField
の一部がキーボードフレームの下に隠れています。キーボードが表示されたら、それぞれTextField
を上に移動したいと思います。
以下のコードを使用して、画面にTextField
を追加しました。
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"))
}
}
}
出力:
私はUIHostingController
を拡張し、その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()
}
}
次に、SceneDelegate
をMyHostingController
ではなくUIHostingController
を使用するように変更します。
それが終わったら、SwiftUIコード内のキーボードについて心配する必要はありません。
(注:これを行うことの影響を完全に理解するために、これをまだ十分に使用していません!)
私がこれに対処した最もエレガントな答えは、ラファエルの解決策に似ています。キーボードイベントをリッスンするクラスを作成します。ただし、キーボードサイズを使用してパディングを変更する代わりに、キーボードサイズの負の値を返し、.offset(y :)修飾子を使用して、最も外側のビューコンテナーのオフセットを調整します。十分にアニメーション化され、どのビューでも機能します。
これがSwiftUIでキーボードを処理する方法です。覚えておくべきことは、それが接続されているVStackで計算を行っているということです。
ビューでモディファイアとして使用します。こちらです:
struct LogInView: View {
var body: some View {
VStack {
// Your View
}
.modifier(KeyboardModifier())
}
}
したがって、この修飾子を取得するには、最初にUIResponderの拡張を作成して、VStackで選択されたTextFieldの位置を取得します。
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)
}
}
これで、Combineを使用してKeyboardModifierを作成し、キーボードが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() }
}
}
}