Pindahkan TextField ke atas saat keyboard muncul di SwiftUI


100

Saya memiliki tujuh TextFielddi dalam utama saya ContentView. Saat pengguna membuka keyboard, beberapa di TextFieldantaranya tersembunyi di bawah bingkai keyboard. Jadi saya ingin TextFieldmenaikkan semuanya masing-masing saat keyboard telah muncul.

Saya telah menggunakan kode di bawah ini untuk ditambahkan TextFielddi layar.

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

Keluaran:

Keluaran



1
@PrashantTukadiya Terima kasih atas tanggapan yang cepat. Saya telah menambahkan TextField di dalam Scrollview tetapi masih menghadapi masalah yang sama.
Hitesh Surani

1
@DimaPaliychuk Ini tidak akan berhasil. itu adalah SwiftUI
Prashant Tukadiya

20
Munculnya keyboard dan itu mengaburkan konten di layar telah ada sejak apa, aplikasi iPhone Objective C pertama? Ini adalah masalah yang terus - menerus dipecahkan. Saya salah satunya kecewa karena Apple tidak membahas ini dengan SwiftUi. Saya tahu komentar ini tidak membantu siapa pun, tetapi saya ingin mengangkat masalah ini bahwa kami benar-benar harus menekan Apple untuk memberikan solusi dan tidak bergantung pada komunitas untuk selalu menyediakan masalah yang paling umum ini.
P. Ent

1
Ada artikel yang sangat bagus oleh Vadim vadimbulavin.com/…
Sudara

Jawaban:


64

Kode diperbarui untuk Xcode, beta 7.

Anda tidak perlu padding, ScrollView atau List untuk mencapai ini. Meskipun solusi ini akan menyenangkan mereka juga. Saya memasukkan dua contoh di sini.

Yang pertama memindahkan semua textField ke atas, jika keyboard muncul untuk salah satu dari mereka. Tetapi hanya jika diperlukan. Jika keyboard tidak menyembunyikan bidang teks, mereka tidak akan bergerak.

Pada contoh kedua, tampilan hanya bergerak cukup untuk menghindari persembunyian bidang teks aktif.

Kedua contoh menggunakan kode umum yang sama yang ditemukan di bagian akhir: GeometryGetter dan KeyboardGuardian

Contoh Pertama (tampilkan semua bidang teks)

Saat keyboard dibuka, 3 bidang teks digerakkan ke atas agar semuanya tetap terlihat

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

}

Contoh Kedua (hanya menampilkan bidang aktif)

Ketika setiap bidang teks diklik, tampilan hanya bergerak ke atas untuk membuat bidang teks yang diklik terlihat.

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

Ini adalah tampilan yang menyerap ukuran dan posisi tampilan induknya. Untuk mencapainya, ini dipanggil di dalam pengubah .background. Ini adalah pengubah yang sangat kuat, bukan hanya cara untuk menghias latar belakang tampilan. Saat meneruskan tampilan ke .background (MyView ()), MyView mendapatkan tampilan yang dimodifikasi sebagai induk. Penggunaan GeometryReader memungkinkan tampilan mengetahui geometri induknya.

Misalnya: Text("hello").background(GeometryGetter(rect: $bounds))akan mengisi batas variabel, dengan ukuran dan posisi tampilan Teks, dan menggunakan ruang koordinat 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)
            }
        }
    }
}

Pembaruan Saya menambahkan DispatchQueue.main.async, untuk menghindari kemungkinan mengubah status tampilan saat sedang dirender. ***

Penjaga Keyboard

Tujuan KeyboardGuardian, adalah untuk melacak acara keyboard / menyembunyikan acara dan menghitung berapa banyak ruang yang perlu digeser tampilan.

Pembaruan: Saya memodifikasi KeyboardGuardian untuk menyegarkan slide, ketika pengguna tab dari satu bidang ke bidang lainnya

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
Apakah mungkin untuk melampirkan GeometryGettersebagai pengubah tampilan daripada latar belakang dengan membuatnya sesuai dengan ViewModifierprotokol?
Sudara

2
Itu mungkin, tapi apa untungnya? Anda akan melampirkannya seperti ini: .modifier(GeometryGetter(rect: $kGuardian.rects[1]))bukan .background(GeometryGetter(rect: $kGuardian.rects[1])). Tidak banyak perbedaan (hanya kurang 2 karakter).
kontiki

3
Dalam beberapa situasi Anda bisa mendapatkan SIGNAL ABORT dari program di dalam GeometryGetter saat menetapkan persegi panjang baru jika Anda menavigasi keluar dari layar ini. Jika itu terjadi pada Anda, cukup tambahkan beberapa kode untuk memverifikasi bahwa ukuran geometri lebih besar dari nol (geometry.size.width> 0 && geometry.size.height> 0) sebelum menetapkan nilai ke self.rect
Julio Bailon

1
@JulioBailon Saya tidak tahu mengapa tetapi geometry.framekeluar dari bantuan DispatchQueue.main.asyncdengan SIGNAL ABORT, sekarang akan menguji solusi Anda. Pembaruan: if geometry.size.width > 0 && geometry.size.height > 0sebelum menugaskan self.rectmembantu.
Roman Vasilyev

2
ini juga berlaku untuk saya di self.rect = geometry.frame (dalam: .global) mendapatkan SIGNAL ABORT dan mencoba semua solusi yang diusulkan untuk mengatasi kesalahan ini
Marwan Roushdy

55

Untuk membangun solusi @rraphael, saya mengubahnya menjadi dapat digunakan dengan dukungan swiftUI xcode11 hari ini.

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

Pemakaian:

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

Terbit currentHeightakan memicu UI merender ulang dan memindahkan TextField Anda ke atas saat keyboard muncul, dan mundur saat ditutup. Namun saya tidak menggunakan ScrollView.


6
Saya suka jawaban ini karena kesederhanaannya. Saya menambahkan .animation(.easeOut(duration: 0.16))untuk mencoba mencocokkan kecepatan keyboard meluncur ke atas.
Mark Moeykens

Mengapa Anda menyetel tinggi maksimal 340 untuk keyboard?
Daniel Ryan

1
@DanielRyan Terkadang ketinggian keyboard mengembalikan nilai yang salah di simulator. Saya tidak bisa menemukan cara untuk menemukan masalah saat ini
Michael Neas

1
Saya sendiri belum pernah melihat masalah itu. Mungkin sudah diperbaiki di versi terbaru. Saya tidak ingin mengunci ukuran jika ada (atau akan) keyboard yang lebih besar.
Daniel Ryan

1
Anda bisa mencoba dengan keyboardFrameEndUserInfoKey. Itu harus menahan bingkai terakhir untuk keyboard.
Mathias Claassen

50

Saya mencoba banyak solusi yang diusulkan, dan meskipun berfungsi dalam banyak kasus, saya memiliki beberapa masalah - terutama dengan area aman (Saya memiliki Formulir di dalam tab TabView).

Saya akhirnya menggabungkan beberapa solusi berbeda, dan menggunakan GeometryReader untuk mendapatkan inset bawah area aman tampilan tertentu dan menggunakannya dalam perhitungan padding:

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

Pemakaian:

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

7
Wow, ini adalah versi paling SwiftUI dari semuanya, dengan GeometryReader dan ViewModifier. Suka.
Mateusz

3
Ini sangat berguna dan elegan. Terima kasih banyak telah menulis ini.
Danilo Campos

2
Saya melihat tampilan putih kosong kecil di atas keyboard saya. Tampilan ini adalah Tampilan GeometryReader, saya mengonfirmasi dengan mengubah warna latar belakang. Tahu mengapa GeometryReader ditampilkan antara Tampilan dan keyboard saya yang sebenarnya.
pengguna832

6
Saya mendapatkan kesalahan saat Thread 1: signal SIGABRTonline rect.height - geometry.safeAreaInsets.bottomsaat membuka tampilan dengan keyboard untuk kedua kalinya dan mengeklik TextField. Tidak masalah jika saya mengklik TextFieldpertama kali atau tidak. Aplikasi masih macet.
JLively

2
Akhirnya Sesuatu yang berhasil! Mengapa Apple tidak bisa melakukan ini bagi kami adalah gila !!
Dave Kozikowski

35

Saya membuat Tampilan yang dapat membungkus tampilan lain untuk mengecilkannya saat keyboard muncul.

Sangat sederhana. Kami membuat penerbit untuk acara keyboard / menyembunyikan acara dan kemudian berlangganan menggunakan onReceive. Kami menggunakan hasil itu untuk membuat persegi panjang seukuran keyboard di belakang keyboard.

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

Anda kemudian dapat menggunakan tampilan seperti ini:

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

Untuk memindahkan konten tampilan ke atas daripada mengecilkannya, padding atau offset dapat ditambahkan viewdaripada meletakkannya di VStack dengan persegi panjang.


6
Saya rasa ini adalah jawaban yang benar. Hanya sedikit perubahan yang saya lakukan: alih-alih persegi panjang, saya hanya memodifikasi padding self.viewdan itu berfungsi dengan baik. Tidak ada masalah sama sekali dengan animasinya
Tae

5
Terima kasih! Bekerja dengan sempurna. Seperti yang dikatakan @Taed, lebih baik menggunakan pendekatan padding. Hasil akhirnya adalahvar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
Meskipun ada suara yang lebih sedikit, ini adalah jawaban yang paling cepat. Dan pendekatan sebelumnya menggunakan AnyView, memecah bantuan akselerasi logam.
Nelson Cardaci

4
Ini adalah solusi yang bagus, tetapi masalah utama di sini adalah Anda kehilangan kemampuan untuk menaikkan tampilan hanya jika keyboard menyembunyikan bidang teks yang Anda edit. Maksud saya: jika Anda memiliki formulir dengan beberapa bidang teks dan Anda mulai mengedit yang pertama di atas, Anda mungkin tidak ingin formulir itu naik karena akan keluar dari layar.
superpuccio

Saya sangat suka jawabannya, tetapi seperti semua jawaban lainnya, itu tidak berfungsi jika tampilan Anda ada di dalam TabBar atau Tampilan tidak rata dengan bagian bawah layar.
Ben Patch

25

Saya telah membuat pengubah tampilan yang sangat mudah digunakan.

Tambahkan file Swift dengan kode di bawah ini dan cukup tambahkan pengubah ini ke tampilan Anda:

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


Bagus. Terima kasih telah berbagi.
dekade

2
Akan keren, jika hanya akan mengimbangi, jika perlu (yaitu jangan menggulir, jika keyboard tidak menutupi elemen input). Senang memiliki ...
dekade

Ini bekerja dengan baik, terima kasih. Implementasinya sangat bersih juga, dan bagi saya, hanya scroll jika diperlukan.
Joshua

Hebat! Mengapa Anda tidak memberikan ini di Github atau di tempat lain? :) Atau Anda dapat menyarankan ini ke github.com/hackiftekhar/IQKeyboardManager karena mereka belum memiliki dukungan penuh SwiftUI
Schnodderbalken

Tidak akan menyenangkan dengan perubahan orientasi dan akan mengimbangi terlepas dari apakah itu diperlukan atau tidak.
GrandSteph

16

Atau Anda bisa menggunakan IQKeyBoardManagerSwift

dan secara opsional dapat menambahkan ini ke delegasi aplikasi Anda untuk menyembunyikan bilah alat dan mengaktifkan penyembunyian keyboard saat diklik pada tampilan apa pun selain keyboard.

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

Ini memang cara (tak terduga) bagi saya juga. Padat.
HelloTimo

Kerangka kerja ini bekerja lebih baik dari yang diharapkan. Terima kasih sudah berbagi!
Richard Poutier

1
Bekerja dengan baik untuk saya di SwiftUI - terima kasih @DominatorVbN - Saya pada mode lansekap iPad, saya perlu meningkatkan IQKeyboardManager.shared.keyboardDistanceFromTextFieldhingga 40 untuk mendapatkan celah yang nyaman.
Richard Groves

Juga harus diatur IQKeyboardManager.shared.enable = trueagar keyboard tidak menyembunyikan bidang teks saya. Bagaimanapun ini adalah solusi terbaik. Saya memiliki 4 bidang yang diatur secara vertikal dan solusi lainnya akan bekerja untuk bidang saya yang paling bawah, tetapi akan mendorong tampilan paling atas.
MisterEd

12

Anda perlu menambahkan ScrollViewdan mengatur bantalan bawah dari ukuran keyboard sehingga konten akan dapat bergulir saat keyboard muncul.

Untuk mendapatkan ukuran keyboard, Anda harus menggunakan NotificationCenteruntuk mendaftar ke acara keyboard. Anda dapat menggunakan kelas khusus untuk melakukannya:

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

The BindableObjectkesesuaian akan memungkinkan Anda untuk menggunakan kelas ini sebagai Statedan memicu pandangan pembaruan. Jika perlu, lihat tutorial untuk BindableObject: Tutorial SwiftUI

Saat Anda mendapatkannya, Anda perlu mengkonfigurasi a ScrollViewuntuk mengurangi ukurannya saat keyboard muncul. Untuk kenyamanan saya membungkus ini ScrollViewmenjadi beberapa jenis komponen:

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

Yang harus Anda lakukan sekarang adalah menyematkan konten Anda di dalam 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)
    }
}

Sunting: Perilaku gulir sangat aneh saat keyboard bersembunyi. Mungkin menggunakan animasi untuk memperbarui padding akan memperbaiki hal ini, atau Anda harus mempertimbangkan untuk menggunakan sesuatu selain paddinguntuk menyesuaikan ukuran tampilan gulir.


hei, sepertinya Anda memiliki pengalaman dalam objek bindable. Saya tidak bisa membuatnya bekerja seperti yang saya inginkan. Akan lebih baik jika Anda dapat melihat-lihat: stackoverflow.com/questions/56500147/…
SwiftiSwift

Mengapa Anda tidak menggunakan @ObjectBinding
SwiftiSwift

3
BindableObjectSayangnya, dengan deprecated, ini tidak berfungsi lagi.
LinusGeffarth

2
@LinusGeffarth Untuk apa nilainya, BindableObjectbaru saja diganti namanya menjadi ObservableObject, dan didChangemenjadi objectWillChange. Objek memperbarui tampilan dengan baik (meskipun saya menguji menggunakan @ObservedObjectalih-alih @State)
SeizeTheDay

Hai, solusi ini menggulir konten, tetapi menunjukkan beberapa area putih di atas keyboard yang menyembunyikan setengah dari bidang teks. Tolong beri tahu saya bagaimana kami dapat menghapus area putih.
Shahbaz Sajjad

12

Xcode 12 - Kode satu baris

Tambahkan pengubah ini ke TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Demo

Apple menambahkan keyboard sebagai region untuk safe area, sehingga Anda dapat menggunakannya untuk berpindah -View pindah dengan keyboard seperti region lainnya.


ini berfungsi untuk TextField, tetapi bagaimana dengan TextEditor?
Андрей Первушин

Ia bekerja di Any View , termasuk TextEditor.
Mojtaba Hosseini

3
baru saja diuji, Xcode beta 6, TextEditor tidak berfungsi
Андрей Первушин

1
Anda dapat mengajukan pertanyaan dan menautkannya di sini. Jadi saya dapat melihat kode Anda yang dapat direproduksi dan melihat apakah saya dapat membantu :) @kyrers
Mojtaba Hosseini

2
Saya menemukan jawabannya sendiri. Tambahkan .ignoresSafeArea(.keyboard)ke Tampilan Anda.
leonboe1

6

Saya meninjau dan memperbaiki solusi yang ada menjadi paket SPM praktis yang menyediakan .keyboardAware()pengubah:

KeyboardAwareSwiftUI

Contoh:

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

    }
}

Sumber:

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

Saya hanya melihat setengah dari tinggi textview. apakah Anda tahu bagaimana mengatasi ini?
Matrosov Alexander

Baik. ini menghemat waktu saya. Sebelum menggunakan ini, kita tahu bantalan bawah tampilan gagang pengubah ini.
Brownsoo Han

5

Saya menggunakan jawaban Benjamin Kindle sebagai titik awal, tetapi saya memiliki beberapa masalah yang ingin saya atasi.

  1. Sebagian besar jawaban di sini tidak berurusan dengan keyboard yang mengubah bingkainya, sehingga akan rusak jika pengguna memutar perangkat dengan keyboard di layar. Menambahkan keyboardWillChangeFrameNotificationke daftar pemberitahuan diproses alamat ini.
  2. Saya tidak ingin banyak penerbit dengan penutupan peta yang serupa-tetapi-berbeda, jadi saya merangkai ketiga notifikasi keyboard menjadi satu penerbit. Memang rantai panjang tetapi setiap langkah cukup mudah.
  3. Saya menyediakan initfungsi yang menerima a @ViewBuildersehingga Anda dapat menggunakan KeyboardHosttampilan seperti View lainnya dan hanya meneruskan konten Anda di trailing closure, sebagai lawan meneruskan tampilan konten sebagai parameter ke init.
  4. Seperti yang disarankan Tae dan fdelafuente dalam komentar, saya menukar Rectangleuntuk menyesuaikan bantalan bawah.
  5. Alih-alih menggunakan string "UIKeyboardFrameEndUserInfoKey" hard-code, saya ingin menggunakan string yang disediakan UIWindowsebagai UIWindow.keyboardFrameEndUserInfoKey.

Menarik semua itu saya miliki:

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


solusi ini tidak berhasil, itu meningkatkan Keyboardtinggi
GSerjo

Bisakah Anda menjelaskan masalah yang Anda lihat di @GSerjo? Saya menggunakan kode ini di aplikasi saya dan berfungsi dengan baik untuk saya.
Timothy Sanders

Bisakah Anda mengaktifkan Pridictivedi iOS keyboard. Settings-> General-> Keyboard-> Pridictive. dalam hal ini tidak mengoreksi calclate dan menambahkan padding ke keyboard
GSerjo

@GSerjo: Saya mengaktifkan teks Prediktif pada iPad Touch (generasi ke-7) yang menjalankan iOS 13.1 beta. Ini menambahkan bantalan dengan benar untuk ketinggian baris prediksi. (Penting untuk diperhatikan, saya tidak menyesuaikan ketinggian keyboard di sini, saya menambahkan padding tampilan itu sendiri.) Coba tukar di peta debugging yang diberi komentar dan mainkan dengan nilai yang Anda dapatkan untuk prediksi papan ketik. Saya akan memposting log di komentar lain.
Timothy Sanders

Dengan peta "debugging" tidak diberi komentar, Anda dapat melihat nilai yang ditugaskan keyboardHeight. Di iPod Touch saya (dalam potret), keyboard dengan prediksi aktif adalah 254 poin. Tanpanya 216 poin. Saya bahkan dapat mematikan prediktif dengan keyboard di layar dan pembaruan padding dengan benar. Menambahkan keyboard dengan prediksi: Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 Saat saya menonaktifkan teks prediksi:Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders

4

Ini diadaptasi dari apa yang dibangun @kontiki. Saya menjalankannya di aplikasi di bawah beta 8 / GM seed, di mana bidang yang perlu digulir adalah bagian dari formulir di dalam NavigationView. Berikut 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
        }
    }
}

Kemudian, saya menggunakan enum untuk melacak slot dalam array rects dan jumlah totalnya:

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

KeyboardSlots.count.rawValueadalah kapasitas array yang diperlukan; yang lainnya sebagai rawValue memberikan indeks yang sesuai yang akan Anda gunakan untuk panggilan .background (GeometryGetter).

Dengan pengaturan itu, pandangan masuk ke KeyboardGuardian dengan ini:

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

Gerakan sebenarnya seperti ini:

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

dilampirkan ke tampilan. Dalam kasus saya, itu dilampirkan ke seluruh NavigationView, sehingga perakitan lengkap meluncur saat keyboard muncul.

Saya belum memecahkan masalah mendapatkan toolbar Selesai atau tombol kembali pada keyboard desimal dengan SwiftUI, jadi saya menggunakan ini untuk menyembunyikannya dengan satu ketukan di tempat lain:

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

Anda melampirkannya ke tampilan sebagai

.modifier(DismissingKeyboard())

Beberapa tampilan (mis., Alat pilih) tidak suka dilampirkan, jadi Anda mungkin perlu lebih terperinci dalam cara memasang pengubah daripada hanya menamparnya di tampilan terluar.

Terima kasih banyak kepada @kontiki atas kerja kerasnya. Anda masih membutuhkan GeometryGetter-nya di atas (tidak, saya tidak melakukan pekerjaan untuk mengubahnya menjadi menggunakan preferensi juga) seperti yang dia ilustrasikan dalam contoh-contohnya.


1
Kepada individu yang memberikan suara negatif: mengapa? Saya mencoba menambahkan sesuatu yang berguna, jadi saya ingin tahu bagaimana menurut Anda kesalahan saya
Feldur

4

Beberapa solusi di atas memiliki beberapa masalah dan belum tentu merupakan pendekatan yang "paling bersih". Karena itu, saya telah memodifikasi beberapa hal untuk penerapan di bawah ini.

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

Saya akan menyukai pendekatan yang lebih bersih dan untuk memindahkan tanggung jawab ke tampilan yang dibangun (bukan pengubah) dalam cara mengimbangi konten, tetapi tampaknya saya tidak bisa membuat penerbit memicu dengan benar saat memindahkan kode offset ke tampilan. ...

Perhatikan juga bahwa Publisher harus digunakan dalam contoh ini karena final classsaat ini menyebabkan error pengecualian yang tidak diketahui (meskipun memenuhi persyaratan antarmuka) dan ScrollView secara keseluruhan adalah pendekatan terbaik saat menerapkan kode offset.


Solusi yang sangat bagus, sangat direkomendasikan! Saya menambahkan Bool untuk menunjukkan apakah keyboard saat ini aktif.
Penghancur kacang

4

Sejujurnya, banyak dari jawaban ini sepertinya sangat membengkak. Jika Anda menggunakan SwiftUI, Anda juga dapat menggunakan Gabungkan.

Buat KeyboardResponderseperti gambar di bawah ini, kemudian Anda dapat menggunakan seperti yang ditunjukkan sebelumnya.

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

Saya menambahkan .animation (.easeIn) agar sesuai dengan animasi yang muncul saat keyboard muncul
Michiel Pelt

(Untuk iOS 13, lihat riwayat jawaban ini)
Loris Foe

Hai, .assign (to: \ .keyboardHeight) memberikan kesalahan ini "Tidak dapat menyimpulkan jenis jalur kunci dari konteks; pertimbangkan untuk menentukan jenis root secara eksplisit". Tolong beri tahu saya solusi yang tepat dan bersih untuk iOS 13 dan iOS 14.
Shahbaz Sajjad

3

Saya tidak yakin apakah API transisi / animasi untuk SwiftUI sudah selesai, tetapi Anda dapat menggunakannya CGAffineTransformdengan.transformEffect

Buat objek keyboard yang dapat diamati dengan properti yang diterbitkan seperti ini:

    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
    }

}

lalu Anda dapat menggunakan properti itu untuk mengatur ulang tampilan Anda seperti ini:

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

atau lebih sederhana

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

Saya suka jawaban ini, tapi saya tidak yakin dari mana 'ScreenSize.portrait' berasal.
Misha Stone

Hai @MishaStone, terima kasih telah memilih pendekatan saya. ScreenSize.portrait adalah kelas yang saya buat untuk mendapatkan ukuran layar berdasarkan Orientasi dan persentase .... tetapi Anda dapat menggantinya dengan nilai apa pun yang Anda perlukan untuk terjemahan Anda
blacktiago

3

Xcode 12 beta 4 menambahkan pengubah tampilan baru ignoresSafeAreayang sekarang dapat Anda gunakan untuk menghindari keyboard.

.ignoresSafeArea([], edges: [])

Ini menghindari keyboard dan semua tepi area aman. Anda dapat mengatur parameter pertama ke .keyboardjika Anda tidak ingin dihindari. Ada beberapa keanehan di dalamnya, setidaknya dalam pengaturan hierarki tampilan saya, tetapi tampaknya inilah cara Apple ingin kita menghindari keyboard.


2

Jawaban disalin dari sini: TextField selalu di atas keyboard dengan SwiftUI

Saya telah mencoba pendekatan yang berbeda, dan tidak ada yang berhasil untuk saya. Yang di bawah ini adalah satu-satunya yang berfungsi untuk perangkat yang berbeda.

Tambahkan ekstensi ini di file:

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

Dalam tampilan Anda, Anda memerlukan variabel untuk mengikat offsetValue:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

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

2
Sekadar informasi, Anda memiliki objek saat memanggil NotificationCenter.default.addObserver... Anda perlu menyimpannya dan menghapus pengamat pada waktu yang tepat ...
TheCodingArt

Hai @TheCodingArt, benar. Saya telah mencoba melakukan itu seperti ini ( oleb.net/blog/2018/01/notificationcenter-removeobserver ) tetapi tampaknya tidak berhasil untuk saya, ada ide?
Ray

2

Seperti yang telah ditunjukkan oleh Mark Krenek dan Heiko, Apple tampaknya menangani masalah ini pada akhirnya di Xcode 12 beta 4. Segalanya bergerak dengan cepat. Menurut catatan rilis untuk Xcode 12 beta 5 yang diterbitkan 18 Agustus 2020 "Formulir, Daftar, dan TextEditor tidak lagi menyembunyikan konten di balik keyboard. (66172025)". Saya baru saja mengunduhnya dan mencobanya dengan cepat di simulator beta 5 (iPhone SE2) dengan wadah Formulir di aplikasi yang saya mulai beberapa hari yang lalu.

Sekarang "hanya bekerja" untuk TextField . SwiftUI secara otomatis akan memberikan bantalan bawah yang sesuai ke Formulir enkapsulasi untuk memberi ruang bagi keyboard. Dan secara otomatis akan menggulir Form ke atas untuk menampilkan TextField tepat di atas keyboard. Container ScrollView sekarang berfungsi dengan baik saat keyboard muncul juga.

Namun, seperti yang Андрей Первушин tunjukkan dalam komentar, ada masalah dengan TextEditor . Beta 5 & 6 secara otomatis akan memberikan bantalan bawah yang sesuai ke Formulir enkapsulasi untuk memberi ruang bagi keyboard. Tetapi TIDAK akan secara otomatis menggulir Formulir ke atas. Keyboard akan menutupi TextEditor. Jadi tidak seperti TextField, pengguna harus menggulir Formulir untuk membuat TextEditor terlihat. Saya akan mengajukan laporan bug. Mungkin Beta 7 akan memperbaikinya. Sangat dekat…

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


saya melihat catatan rilis apel, diuji pada beta5 dan beta6, TextField berfungsi, TextEditor TIDAK, apa yang saya lewatkan? @State var text = "" var body: some Tampilan {Formulir {Bagian {Teks (teks) .frame (tinggi: 500)} Bagian {BidangTeks ("5555", teks: $ text) .frame (tinggi: 50)} Bagian {TextEditor (teks: $ teks) .frame (tinggi: 120)}}}
Андрей Первушин

2

Pemakaian:

import SwiftUI

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

Kode:

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

mencoba solusi Anda ... tampilan hanya digulir ke setengah dari bidang teks. Mencoba semua solusi di atas. Punya masalah yang sama. Tolong bantu!!!
Sona

@Zeona, coba di aplikasi sederhana, Anda mungkin melakukan sesuatu yang berbeda. Selain itu, coba hapus '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)' jika Anda menggunakan area aman.
8suhas

dihapus (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0) maka saya mendapatkan ruang putih di atas keyboard
Sona

1

Penanganan TabView's

Saya suka jawaban Benjamin Kindle tetapi tidak mendukung TabView. Berikut adalah penyesuaian saya pada kodenya untuk menangani TabView:

  1. Tambahkan ekstensi ke UITabViewuntuk menyimpan ukuran tabView saat bingkainya disetel. Kita dapat menyimpan ini dalam variabel statis karena biasanya hanya ada satu tabView dalam sebuah proyek (jika milik Anda memiliki lebih dari satu, Anda harus menyesuaikannya).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. Anda harus mengubah miliknya onReceivedi bagian bawah KeyboardHosttampilan untuk memperhitungkan tinggi Tab Bar:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. Dan itu dia! Sangat sederhana 🎉.

1

Saya mengambil pendekatan yang sama sekali berbeda, dengan memperluas UIHostingControllerdan menyesuaikannya 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()
    }
}

Kemudian ubah SceneDelegateuntuk menggunakan, MyHostingControllerbukanUIHostingController .

Setelah selesai, saya tidak perlu khawatir tentang keyboard di dalam kode SwiftUI saya.

(Catatan: Saya belum cukup menggunakan ini, untuk sepenuhnya memahami implikasi apa pun dari melakukan ini!)


1

Ini adalah cara saya menangani keyboard di SwiftUI. Hal yang perlu diingat adalah bahwa itu membuat perhitungan pada VStack yang dilampirkan.

Anda menggunakannya di Tampilan sebagai Pengubah. Cara ini:

struct LogInView: View {

  var body: some View {
    VStack {
      // Your View
    }
    .modifier(KeyboardModifier())
  }
}

Jadi untuk sampai ke pengubah ini, pertama, buat ekstensi UIResponder untuk mendapatkan posisi TextField yang dipilih di 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)
  }
}

Anda sekarang dapat membuat KeyboardModifier menggunakan Gabungkan untuk menghindari keyboard menyembunyikan BidangTeks:

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

Adapun iOS 14 (beta 4) berfungsi cukup sederhana:

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

Dan ukuran tampilan menyesuaikan dengan bagian atas keyboard. Tentunya ada lebih banyak perbaikan yang mungkin dilakukan dengan frame (.maxHeight: ...) dll. Anda akan mengetahuinya.

Sayangnya floating keyboard di iPad masih menimbulkan masalah saat dipindahkan. Tetapi solusi yang disebutkan di atas juga akan, dan ini masih beta, saya harap mereka akan mengetahuinya.

Terima kasih Apple, akhirnya!


Ini tidak bekerja sama sekali (14.1). Apa idenya?
Burgler-dev

0

Pandangan ku:

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

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

Pada dasarnya, Mengelola bantalan bawah berdasarkan ketinggian keyboard.


-3

Jawaban paling elegan yang pernah saya lakukan ini mirip dengan solusi rraphael. Buat kelas untuk mendengarkan acara keyboard. Alih-alih menggunakan ukuran keyboard untuk memodifikasi padding, kembalikan nilai negatif dari ukuran keyboard, dan gunakan pengubah .offset (y :) untuk menyesuaikan offset penampung tampilan paling luar. Ini beranimasi dengan cukup baik, dan berfungsi dengan tampilan apa pun.


Bagaimana Anda membuat ini beranimasi? Saya punya .offset(y: withAnimation { -keyboard.currentHeight }), tapi kontennya melompat, bukan animasi.
jjatie

Sudah beberapa beta yang lalu saya mengacaukan kode ini, tetapi pada saat komentar saya sebelumnya, memodifikasi offset vstack selama runtime adalah semua yang diperlukan, SwiftUI akan menganimasikan perubahan untuk Anda.
pcallycat
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.