キーボードがSwiftUIに表示されたら、TextFieldを上に移動します


100

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

出力:

出力



1
@PrashantTukadiya迅速な対応ありがとうございます。Scrollview内にTextFieldを追加しましたが、それでも同じ問題に直面しています。
HiteshSurani19年

1
@DimaPaliychukこれは機能しません。それはSwiftUIです
PrashantTukadiya19年

28
キーボードの表示と画面上のコンテンツの不明瞭化は、最初のObjective C iPhoneアプリから何が起こったのでしょうか?これは常に解決されている問題です。私は、AppleがSwiftUiでこれに対処していないことに失望している。私はこのコメントが誰にとっても役に立たないことを知っていますが、私はこの問題を提起したいと思いました。私たちは本当にAppleに解決策を提供するよう圧力をかけるべきであり、この最も一般的な問題を常に提供するためにコミュニティに依存するべきではありません。
P.Ent19年

3
ヴァディムによって非常に良い記事がありvadimbulavin.com/...
Sudara

回答:


64

Xcode、ベータ7用に更新されたコード。

これを実現するために、パディング、ScrollViews、またはリストは必要ありません。このソリューションは彼らにもうまく機能しますが。ここに2つの例を含めます。

最初のものは、キーボードがそれらのいずれかに表示される場合、すべてのtextFieldを上に移動ます。ただし、必要な場合のみ。キーボードがテキストフィールドを非表示にしない場合、テキストフィールドは移動しません。

2番目の例では、アクティブなテキストフィールドが非表示にならないようにするためだけにビューが移動します。

どちらの例でも、最後にある同じ共通コードGeometryGetterKeyboardGuardianを使用しています。

最初の例(すべてのテキストフィールドを表示)

キーボードを開くと、3つのテキストフィールドが上に移動して、すべてが表示されたままになります。

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

}

2番目の例(アクティブフィールドのみを表示)

各テキストフィールドがクリックされると、ビューはクリックされたテキストフィールドが表示されるのに十分なだけ上に移動します。

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

これは、親ビューのサイズと位置を吸収するビューです。これを実現するために、.background修飾子内で呼び出されます。これは非常に強力なモディファイアであり、ビューの背景を装飾する方法だけではありません。ビューを.background(MyView())に渡すと、MyViewは変更されたビューを親として取得します。GeometryReaderを使用すると、ビューが親のジオメトリを知ることができます。

例:Text("hello").background(GeometryGetter(rect: $bounds))テキストビューのサイズと位置で、グローバル座標空間を使用して、変数の境界を埋めます。

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

更新レンダリング中にビューの状態が変更される可能性を回避するために、DispatchQueue.main.asyncを追加しました。***

KeyboardGuardian

KeyboardGuardianの目的は、キーボードの表示/非表示イベントを追跡し、ビューをシフトする必要があるスペースの量を計算することです。

更新: ユーザーが1つのフィールドから別のフィールドにタブ移動したときに、スライドを更新するようにKeyboardGuardianを変更しました

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

        }
    }
}

1
プロトコルGeometryGetterに準拠させることで、背景よりもビューモディファイアとしてアタッチすることはできViewModifierますか?
Sudara

2
それは可能ですが、利益は何ですか?次のように添付します:の.modifier(GeometryGetter(rect: $kGuardian.rects[1]))代わりに.background(GeometryGetter(rect: $kGuardian.rects[1]))。大きな違いはありません(2文字少ない)。
コンティキ

3
状況によっては、この画面から移動している場合、新しい長方形を割り当てるときに、GeometryGetter内のプログラムからSIGNALABORTを取得できます。その場合は、self.rectに値を割り当てる前に、ジオメトリのサイズがゼロより大きいことを確認するコードを追加するだけです(geometry.size.width> 0 && shape.size.height> 0)
Julio Bailon

1
@JulioBailon理由はわかりませんが、SIGNAL ABORTのサポートgeometry.frameから抜け出しDispatchQueue.main.async、ソリューションをテストします。更新:ヘルプをif geometry.size.width > 0 && geometry.size.height > 0割り当てる前にself.rect
RomanVasilyev20年

2
これは私にとっても壊れます。self.rect= geometry.frame(in:.global)がSIGNAL ABORTを取得し、このエラーに対処するために提案されたすべてのソリューションを試しました
MarwanRoushdy20年

55

@rraphaelのソリューションを構築するために、今日のxcode11swiftUIサポートで使用できるように変換しました。

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

使用法:

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

公開currentHeightすると、UIの再レンダリングがトリガーされ、キーボードが表示されたときにTextFieldが上に移動し、閉じたときに下に戻ります。ただし、ScrollViewは使用しませんでした。


6
私はその単純さのためにこの答えが好きです。.animation(.easeOut(duration: 0.16))キーボードが上にスライドする速度に合わせるために追加しました。
MarkMoeykens19年

キーボードの最大高さを340に設定したのはなぜですか?
ダニエルライアン

1
@DanielRyan時々、キーボードの高さがシミュレーターで誤った値を返していました。現在、問題を
特定

1
私はその問題を自分で見たことがありません。たぶんそれは最新バージョンで修正されています。より大きなキーボードがある(またはそうなる)場合に備えて、サイズを固定したくありませんでした。
ダニエルライアン

1
で試すことができkeyboardFrameEndUserInfoKeyます。これで、キーボードの最終フレームが保持されます。
MathiasClaassen19年

51

私は提案された解決策の多くを試しましたが、ほとんどの場合は機能しますが、主に安全な領域に問題がありました(TabViewのタブ内にフォームがあります)。

最終的に、いくつかの異なるソリューションを組み合わせ、GeometryReaderを使用して、特定のビューの安全領域の下部の挿入図を取得し、それをパディングの計算に使用しました。

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

使用法:

struct MyView: View {
    var body: some View {
        Form {...}
        .modifier(AdaptsToKeyboard())
    }
}

7
うわー、これはGeometryReaderとViewModifierを備えたそれらすべての中で最もSwiftUIバージョンです。大好きです。
マテウス

3
これはとても便利でエレガントです。これを書いてくれてありがとう。
DaniloCampos20年

2
キーボードの上に小さな空白の白いビューが表示されています。このビューはGeometryReaderビューです。背景色を変更して確認しました。GeometryReaderが実際のビューとキーボードの間に表示されている理由。
user8 3220

6
もう一度キーボードを使用してビューに移動し、をクリックすると、Thread 1: signal SIGABRTオンラインでエラーが発生しrect.height - geometry.safeAreaInsets.bottomますTextFieldTextField初めてクリックするかどうかは関係ありません。それでもアプリはクラッシュします。
JLively

2
最後にうまくいく何か!Appleが私たちのためにこれに傾倒する理由はナッツです!!
DaveKozikowski20年

35

キーボードが表示されたときに他のビューを折り返して縮小できるビューを作成しました。

とても簡単です。キーボードの表示/非表示イベントのパブリッシャーを作成し、を使用してそれらをサブスクライブしますonReceive。その結果を使用して、キーボードの後ろにキーボードサイズの長方形を作成します。

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

その後、次のようにビューを使用できます。

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

ビューのコンテンツを縮小するのではなく上に移動するにはview、長方形のVStackに配置するのではなく、パディングまたはオフセットを追加します。


6
これが正解だと思います。ちょっとした調整をしました。長方形の代わりに、のパディングを変更するだけでself.view、うまく機能します。アニメーションで全く問題

5
ありがとう!完璧に動作します。@Taedが言ったように、パディングアプローチを使用する方が良いです。最終的な結果は次のようになりますvar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
投票数が少ないにもかかわらず、これは最も迅速な回答です。そして、AnyViewを使用した以前のアプローチは、メタルアクセラレーションのヘルプを壊します。
NelsonCardaci19年

4
これは素晴らしい解決策ですが、ここでの主な問題は、キーボードが編集中のテキストフィールドを非表示にしている場合にのみ、ビューを上に移動する機能が失われることです。つまり、複数のテキストフィールドがあるフォームがあり、最初のテキストフィールドの編集を開始した場合、画面の外に移動するため、フォームを上に移動したくないでしょう。
matteopuc

私はその答えが本当に好きですが、他のすべての答えと同様に、ビューがTabBar内にあるか、ビューが画面の下部と同じ高さでない場合は機能しません。
ベン・パッチ

25

非常に使いやすいビュー修飾子を作成しました。

以下のコードを使用してSwiftファイルを追加し、この修飾子をビューに追加するだけです。

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


いいね。共有していただきありがとうございます。
数十年20年

2
必要に応じてオフセットするだけなら(つまり、キーボードが入力要素をカバーしていない場合はスクロールしないでください)、かっこいいでしょう。ニース持っている...
十年

これはうまくいきます、ありがとう。非常にクリーンな実装でもあり、私にとっては、必要な場合にのみスクロールします。
ジョシュア

驚くばかり!これをGithubや他の場所で提供してみませんか?:)それとも、これを提案することができgithub.com/hackiftekhar/IQKeyboardManager彼らはまだフルSwiftUIをサポートしていないよう
Schnodderbalken

向きの変更ではうまく機能せず、必要かどうかに関係なくオフセットされます。
GrandSteph

21

Xcode 12-1行のコード

この修飾子をに追加します TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

デモ

Appleは、キーボードを安全領域の領域として追加したので、他の領域と同じように、キーボードを使用してキーボードを移動できViewます。


を含むAny Viewで動作しTextEditorます。
MojtabaHosseini20年

@MojtabaHosseiniこの動作を防止したい場合はどうすればよいですか?キーボードを開いたときに上に移動している画像がありますが、移動したくありません。
kyrers

1
質問をして、ここにリンクすることができます。私はあなたの再現可能コードを見て、私は助けることができるかどうか:) @kyrersを見ることができるように
モタバ・ホセイーニ

2
私はそれを自分で理解しました。.ignoresSafeArea(.keyboard)ビューに追加します。
leonboe1

1
残念ながら、iOSの14だということだけ...
Xaxxus

16

または、IQKeyBoardManagerSwiftを使用することもできます

オプションで、これをアプリデリゲートに追加して、ツールバーを非表示にし、キーボード以外のビューをクリックしたときにキーボードを非表示にできるようにすることができます。

        IQKeyboardManager.shared.enableAutoToolbar = false
        IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide

これは確かに私にとっても(予期しない)方法です。固体。
HelloTimo

このフレームワークは、予想以上にうまく機能しました。共有していただきありがとうございます!
RichardPoutier20年

1
SwiftUIで問題なく動作しています-@ DominatorVbNに感謝します-iPadランドスケープモードではIQKeyboardManager.shared.keyboardDistanceFromTextField、快適なギャップを得るために40に増やす必要がありました。
リチャードグローブ

またIQKeyboardManager.shared.enable = true、キーボードがテキストフィールドを非表示にしないように設定する必要がありました。いずれにせよ、これが最善の解決策です。4つのフィールドが垂直に配置されており、他のソリューションは一番下のフィールドで機能しますが、一番上のフィールドは見えなくなります。
MisterEd

12

ScrollViewキーボードが表示されたときにコンテンツがスクロールできるように、キーボードのサイズの下部パディングを追加して設定する必要があります。

キーボードサイズを取得するには、NotificationCenterを使用してキーボードイベントに登録する必要があります。これを行うには、カスタムクラスを使用できます。

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

BindableObject適合性は、あなたがこのクラスを使用できるようになりますStateと、ビューの更新をトリガします。必要に応じて、次のチュートリアルを参照してくださいBindableObjectSwiftUIチュートリアル

それを取得したらScrollView、キーボードが表示されたときにサイズを小さくするようにを構成する必要があります。便宜上、これScrollViewをある種のコンポーネントにラップしました。

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

今やらなければならないのは、カスタム内にコンテンツを埋め込むことだけです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)
    }
}

編集: キーボードが隠れているときのスクロール動作は本当に奇妙です。おそらく、アニメーションを使用してパディングを更新するとこれが修正されるかpadding、スクロールビューのサイズを調整するために以外のものを使用することを検討する必要があります。


ねえ、bindableobjectの経験があるようです。思い通りに動かせません。:あなたは目を通すことができればいいだろうstackoverflow.com/questions/56500147/...
SwiftiSwift

なぜあなたは@ObjectBindingを使用していない
SwiftiSwift

3
ではBindableObject非推奨、これは残念ながら、もはや機能していません。
LinusGeffarth

2
@LinusGeffarthその価値のために、名前を、、およびにBindableObject変更しました。(私が使用してテストしてもオブジェクトがうまくビューを更新の代わり)ObservableObjectdidChangeobjectWillChange@ObservedObject@State
SeizeTheDay

こんにちは、このソリューションはコンテンツをスクロールしていますが、キーボードの上にテキストフィールドの半分を隠す白い領域が表示されます。白い部分を取り除く方法を教えてください。
ShahbazSajjad20年

6

既存のソリューションを確認して、.keyboardAware()修飾子を提供する便利なSPMパッケージにリファクタリングしました。

KeyboardAwareSwiftUI

例:

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

    }
}

ソース:

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

テキストビューの高さの半分が表示されます。これを解決する方法を知っていますか?
マトロソフアレクサンダー

良い。これは私の時間を節約しました。これを使用する前に、このモディファイヤがビューの下部のパディングを処理することを知っています。
BrownsooHan20年

5

私はBenjaminKi​​ndleの回答を出発点として使用しましたが、対処したいいくつかの問題がありました。

  1. ここでの回答のほとんどは、キーボードがフレームを変更することを扱っていないため、ユーザーが画面上のキーボードを使用してデバイスを回転させると壊れます。keyboardWillChangeFrameNotification処理された通知のリストに追加すると、これに対処します。
  2. マップクロージャーが似ているが異なる複数のパブリッシャーが必要なかったため、3つのキーボード通知すべてを1つのパブリッシャーにチェーンしました。確かに長いチェーンですが、各ステップは非常に簡単です。
  3. コンテンツビューをパラメータとしてに渡すのではなく、他のビューと同じようにビューを使用して、コンテンツを末尾のクロージャで渡すことができるように、initを受け入れる関数を提供しました。@ViewBuilderKeyboardHostinit
  4. Taeとfdelafuenteがコメントで示唆したように、私Rectangleは下部のパディングを調整するためにを交換しました。
  5. ハードコードされた「UIKeyboardFrameEndUserInfoKey」文字列を使用する代わりに、UIWindowとして提供されている文字列を使用したいと思いましたUIWindow.keyboardFrameEndUserInfoKey

それをすべてまとめると、私は次のようになります。

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


このソリューションは、それが増大し、動作しないKeyboard高さ
GSerjo

@GSerjoで発生している問題について詳しく説明していただけますか?私は自分のアプリでこのコードを使用していますが、正常に機能しています。
ティモシーサンダース

PridictiveiOSでオンにしていただけませんかkeyboardSettings-> General-> Keyboard-> Pridictive。この場合にはそれが正しいcalclateせず、キーボードにパディングを追加
GSerjo

@GSerjo:iOS13.1ベータ版を実行しているiPadTouch(第7世代)で予測テキストを有効にしました。予測行の高さのパディングが正しく追加されます。(ここではキーボードの高さを調整していません。ビュー自体のパディングに追加しています。)コメントアウトされているデバッグマップを入れ替えて、予測用に取得した値を試してみてください。キーボード。別のコメントでログを投稿します。
ティモシーサンダース

「デバッグ」マップのコメントを外すと、に割り当てられている値を確認できますkeyboardHeight。私のiPodTouch(縦向き)では、予測がオンになっているキーボードは254ポイントです。それなしでは216ポイントです。画面上のキーボードで予測をオフにすることもでき、パディングが適切に更新されます。予測とキーボードを追加する:Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 私は予測テキストをオフにすると:Received UIKeyboardWillChangeFrameNotification with height 216.0
ティモシー・サンダース

4

これは、@ kontikiが構築したものから適応されています。ベータ8 / GMシードのアプリで実行しています。スクロールが必要なフィールドは、NavigationView内のフォームの一部です。これが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
        }
    }
}

次に、列挙型を使用して、rects配列のスロットと総数を追跡しました。

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

KeyboardSlots.count.rawValue必要なアレイ容量です。rawValueとしての他のものは、.background(GeometryGetter)呼び出しに使用する適切なインデックスを提供します。

この設定により、ビューは次のようにKeyboardGuardianに到達します。

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

実際の動きは次のようになります。

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

ビューに添付されています。私の場合、NavigationView全体にアタッチされているため、キーボードが表示されると、アセンブリ全体が上にスライドします。

SwiftUIを使用して10進キーボードでDoneツールバーまたはリターンキーを取得する問題を解決していないので、代わりにこれを使用して他の場所のタップで非表示にします。

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

あなたはそれをビューに添付します

.modifier(DismissingKeyboard())

一部のビュー(ピッカーなど)は、それをアタッチすることを好まないため、モディファイヤを最も外側のビューに叩くだけでなく、アタッチする方法をある程度細かくする必要がある場合があります。

大変な努力をしてくれた@kontikiに感謝します。彼が例で示しているように、上記のGeometryGetterが必要です(いいえ、設定を使用するように変換する作業も行いませんでした)。


1
反対票を投じた個人へ:なぜですか?私はあなたのビューでは、私が間違っていたか知りたいので、有益な何かを追加しようとした
Feldur

4

上記の解決策のいくつかにはいくつかの問題があり、必ずしも「最もクリーンな」アプローチではありませんでした。このため、以下の実装のためにいくつかの変更を加えました。

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

よりクリーンなアプローチと、コンテンツをオフセットする方法について(修飾子ではなく)構築されたビューに責任を移したかったのですが、オフセットコードをビューに移動するときに、パブリッシャーに適切にトリガーさせることができなかったようです。 ..。。

また、final class現在不明な例外クラッシュが発生しているため(インターフェイス要件を満たしている場合でも)、このインスタンスではパブリッシャーを使用する必要があり、オフセットコードを適用する場合は全体的にScrollViewが最善のアプローチであることに注意してください。


非常に素晴らしい解決策、強くお勧めします!キーボードが現在アクティブであるかどうかを示すブール値を追加しました。
ピーナッツマッシャー

4

これらの答えの多くは、正直に言うと本当に肥大化しているようです。SwiftUIを使用している場合は、Combineも使用できます。

KeyboardResponder以下に示すようにを作成すると、前に示したように使用できます。

iOS14用に更新されました。

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

キーボードが表示されるアニメーションに一致するように.animation(.easeIn)を追加しました
Michiel Pelt

(iOS 13の場合は、この回答の履歴に移動してください)
LorisFoe20年

1
こんにちは、.assign(to:\ .keyboardHeight)は、「コンテキストからキーパスタイプを推測できません。ルートタイプを明示的に指定することを検討してください」というエラーを出します。私は、IOS 13とiOSの両方の14のための適切かつクリーンな解決策を教えてください
Shahbaz SAJJAD

3

SwiftUIのトランジション/アニメーションAPIが完了しているかどうかはわかりませんがCGAffineTransform.transformEffect

次のような公開プロパティを使用して、監視可能なキーボードオブジェクトを作成します。

    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
    }

}

次に、そのプロパティを使用して、次のようにビューを再配置できます。

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

またはより単純な

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

私はこの答えが大好きですが、「ScreenSize.portrait」がどこから来ているのかよくわかりません。
ミシャストーン

こんにちは@MishaStoneありがとうpor私のアプローチを選択してください。ScreenSize.portraitは、私はオリエンテーションとパーセンテージの画面ベースの測定値を得るために作られたことをクラスである....しかし、あなたがあなたの翻訳に必要と任意の値に置き換えることができます
blacktiago

3

Xcode 12ベータ4はignoresSafeArea、キーボードを回避するために使用できる新しいビュー修飾子を追加します。

.ignoresSafeArea([], edges: [])

これにより、キーボードとすべての安全な領域の端が回避されます。.keyboard回避したくない場合は、最初のパラメーターをに設定できます。少なくとも私のビュー階層の設定では、いくつかの癖がありますが、これはAppleがキーボードを避けてほしいと望んでいる方法のようです。


2

ここからコピーされた回答:SwiftUIで常にキーボード上部にあるTextField

私はさまざまなアプローチを試しましたが、どれもうまくいきませんでした。以下のこれは、さまざまなデバイスで機能した唯一のものです。

この拡張子をファイルに追加します。

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

ビューでは、offsetValueをバインドするための変数が必要です。

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

  var body: some View { 
    
    VStack {
     //...       
    }
    .keyboardSensible($offsetValue)
  }
}

2
ただ、呼び出すときFYI、あなたがオブジェクトを所有してNotificationCenter.default.addObserver...あなたがそれらを保存し、適切な時期にオブザーバーを削除する必要があります...
TheCodingArt

こんにちは@TheCodingArt、そうです、私はこのようにしようとしました(oleb.net/blog/2018/01/notificationcenter-removeobserver)が、私にはうまくいかないようです、何かアイデアはありますか?
レイ

2

Mark KrenekとHeikoが指摘しているように、AppleはついにXcode12ベータ4でこの問題に取り組んでいるようでした。物事は急速に進んでいます。2020年8月18日に公開されたXcode12ベータ5のリリースノートによると、「フォーム、リスト、およびTextEditorは、キーボードの背後にコンテンツを非表示にしなくなりました。(66172025)」。ダウンロードして、ベータ5シミュレーター(iPhone SE2)で、数日前に開始したアプリのフォームコンテナーを使用して簡単なテストを行いました。

これで、TextFieldに対して「正しく機能」します。SwiftUIは、キーボード用のスペースを確保するために、カプセル化フォームに適切な下部パディングを自動的に提供します。また、フォームを自動的に上にスクロールして、キーボードのすぐ上にTextFieldを表示します。ScrollViewコンテナーは、キーボードが起動したときにも適切に動作するようになりました。

ただし、コメントでАндрейПервушинが指摘しているように、TextEditorに問題があります。Beta 5&6は、キーボード用のスペースを確保するために、カプセル化フォームに適切な下部パディングを自動的に提供します。ただし、フォームを自動的に上にスクロールすることはありません。キーボードはTextEditorをカバーします。したがって、TextFieldとは異なり、ユーザーはフォームをスクロールしてTextEditorを表示する必要があります。バグレポートを提出します。おそらくベータ7はそれを修正するでしょう。とても近い…

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


アップルのリリースノートが表示され、beta5とbeta6でテストされていますが、TextFieldは機能しますが、TextEditorは機能しません。何が欠けていますか?@State var text = "" var body:some View {Form {Section {Text(text).frame(height:500)} Section {TextField( "5555"、text:$ text).frame(height:50)}セクション{テキストエディタ(テキスト:$テキスト).frame(高さ:120)}}}
АндрейПервушин

2

使用法:

import SwiftUI

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

コード:

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

解決策を試しました...ビューはテキストフィールドの半分までしかスクロールされません。上記のすべての解決策を試しました。同じ問題が発生しました。助けてください!!!
ソナ

@Zeona、シンプルなアプリで試してみてください。何か違うことをしているかもしれません。また、セーフエリアを使用している場合は、「-(UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0)」を削除してみてください。
8suhas

削除(UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0)すると、キーボードの上に空白が表示されます
Sona

1

取り扱いTabViewさん

私はBenjaminKi​​ndleの答えが好きですが、TabViewsをサポートしていません。TabViewを処理するための彼のコードに対する私の調整は次のとおりです。

  1. UITabViewフレームが設定されているときにtabViewのサイズを保存するための拡張機能を追加します。 通常、プロジェクトにはtabViewが1つしかないため、これを静的変数に格納できます(複数ある場合は、調整する必要があります)。
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. タブバーの高さを考慮してonReceiveKeyboardHostビューの下部で彼を変更する必要があります。
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. 以上です!超シンプル🎉。

1

私は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コード内のキーボードについて心配する必要はありません。

(注:これを行うことの意味を完全に理解するには、まだこれを十分に使用していません!)


1

これは私が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)
  }
}

キーボードがTextFieldを非表示にしないように、Combineを使用してKeyboardModifierを作成できるようになりました。

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

1

iOS 14(ベータ4)に関しては、非常に簡単に機能します。

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

また、ビューのサイズはキーボードの上部に合わせて調整されます。frame(.maxHeight:...)などで可能なより多くの改良が確かにあります。あなたはそれを理解するでしょう。

残念ながら、iPadのフローティングキーボードは移動しても問題が発生します。しかし、上記のソリューションもそうだろうし、それはまだベータ版なので、彼らがそれを理解してくれることを願っている。

Thx Apple、ついに!


これはまったく機能しません(14.1)。アイデアは何ですか?
Burgler-devの

0

私の見解:

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

私の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
        
    }
    
}

基本的に、キーボードの高さに基づいて下部のパディングを管理します。


-3

私がこれに対して管理した最もエレガントな答えは、rraphaelのソリューションに似ています。キーボードイベントをリッスンするクラスを作成します。ただし、キーボードサイズを使用してパディングを変更する代わりに、キーボードサイズの負の値を返し、.offset(y :)修飾子を使用して最も外側のビューコンテナのオフセットを調整します。十分にアニメーション化され、どのビューでも機能します。


どうやってこれをアニメートさせたのですか?持っていますが.offset(y: withAnimation { -keyboard.currentHeight })、アニメーションではなくコンテンツがジャンプします。
jjatie

このコードをいじくりまわしたのは数ベータ前のことですが、以前のコメントの時点で、実行時にvstackのオフセットを変更するだけで、SwiftUIが変更をアニメーション化してくれました。
pcallycat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.